Inspect Object Symbols Safely with llvm-nm-18
Before you link a mystery object file into anything, llvm-nm-18 shows exactly what it exports. You inspect symbols in an object file, executable or archive, then narrow the output down to the names or definitions you actually need. The examples are read-only and take about ten minutes if the file is already on disk.
The route
Jump straight to the step you need, or tick off Done means at the end.
- 1. Confirm the installed command
- 2. Read a normal object or executable
- 3. Inspect an archive and keep the member names
- 4. Filter the report instead of scanning a wall of text
- 5. Demangle, sort and add context when needed
- 6. Read from standard input without changing the file
- 7. Diagnose failures and leave the input untouched
This guide uses Ubuntu's llvm-18 package, version 18.1.3. The installed command and its manual page are the authority for this machine. Most of this needs no elevated privileges. Do not add sudo just because a file came from a system directory: use it only when your account genuinely cannot read the input.
1. Confirm the installed command
Check that the versioned executable is the one your shell will run. This avoids silently using a different nm implementation with different options.
$ command -v llvm-nm-18
/usr/bin/llvm-nm-18
$ llvm-nm-18 --version
llvm-nm, compatible with GNU nm
Ubuntu LLVM version 18.1.3
Optimized build.
Checkpoint
If command -v prints nothing, install or enable the package through your normal system administration process. Do not substitute plain nm until you have checked its own manual page.
2. Read a normal object or executable
Pass one or more filenames after the options. The default output is BSD-style: an address when one is available, a one-character type code, and the symbol name. A type such as T means a global code symbol, while U means the name is undefined in that file. Lowercase and uppercase variants distinguish local from global symbols for several types.
$ llvm-nm-18 /path/to/program
/path/to/program: 0000000000001139 T main
/path/to/program: U puts
Exact names and addresses depend on the file. A stripped executable can contain no ordinary symbols at all, and that is not automatically a failure. This installed binary, for instance, has none:
$ llvm-nm-18 /usr/bin/true
/usr/bin/true: no symbols
Omit every filename and llvm-nm-18 quietly looks for a.out instead. That default is easy to trigger by accident, so make the input explicit in scripts and notes.
3. Inspect an archive and keep the member names
Archives contain several object files. The output inserts a member heading before that member's symbols, useful when a library exports a name but you need to find which object actually provides it.
$ llvm-nm-18 /usr/lib/x86_64-linux-gnu/libz.a | head -n 6
adler32.o:
00000000000006c0 T adler32
00000000000006d0 T adler32_combine
00000000000007b0 T adler32_combine64
0000000000000000 T adler32_z
Use --print-file-name, also spelled -A or -o, when several standalone files are being compared side by side. With an archive, member headings already do that job for you. Use --print-armap when you also need the archive's symbol index.
Do not treat an archive report as a link map. It tells you what symbol records exist, it does not explain every later linker decision or runtime lookup.
4. Filter the report instead of scanning a wall of text
Use the narrowest filter that answers the question:
- Only what is defined.
--defined-onlyor-Ukeeps symbols defined in the input. - Only what is missing.
--undefined-onlyor-ukeeps unresolved names. - Only what is externally visible.
--extern-onlyor-gkeeps externally accessible definitions. - Including debug symbols.
--debug-symsor-abrings back symbols normally suppressed. - Excluding weak symbols.
--no-weakor-Wremoves weak symbols from the report.
For a script that only needs names, use -j. It is an alias for the just-symbols format and is far easier to parse than BSD output:
$ llvm-nm-18 -j /usr/lib/x86_64-linux-gnu/libz.a | head -n 5
adler32.o:
adler32
adler32_combine
adler32_combine64
The blank line and archive member heading still matter here. If a machine-readable report feeds a larger tool, choose an explicit format such as --format=posix and test the parser against the actual file types you expect to see.
5. Demangle, sort and add context when needed
C++ names can be unreadable in mangled form. Add --demangle or -C when readable function and type names matter more than the linker spelling. The default is no demangling, so a strange-looking name is not evidence the symbol is missing.
By default, symbols come out sorted. Use --no-sort or -p to keep the order encountered in the file, --numeric-sort, -n or -v for address order, and --size-sort when symbol size is the measure you care about. --reverse-sort or -r reverses whichever ordering you picked. Add --print-size or -S to show sizes where the object format supports them.
For source locations, --line-numbers or -l needs usable debugging information. Undefined symbols are reported at their first relocation when that information exists; without debug data, do not expect source lines at all.
6. Read from standard input without changing the file
A filename of - tells llvm-nm-18 to read the object or bitcode stream from standard input, handy in a pipeline. Keep the producer's exit status in mind though: a successful llvm-nm-18 status only says it processed its input successfully, nothing more.
$ cat /usr/bin/true | llvm-nm-18 -
<stdin>: no symbols
LLVM bitcode normally has no addresses before it is linked or just-in-time compiled, so its symbol records never receive printed addresses. If you need to distinguish input formats while diagnosing a pipeline, save a copy in a temporary working directory and inspect that file directly. Do not overwrite the original stream source just to make it easier to look at.
7. Diagnose failures and leave the input untouched
llvm-nm-18 only ever reads its input. It never modifies object files, archives or executables, so there is no undo operation for any of these examples. Still, treat symbol output as potentially sensitive: names can reveal internal functions, build paths or interfaces you did not mean to publish. Avoid pasting full reports into public tickets when a filtered report will do the job.
A missing path produces a diagnostic and a non-zero status:
$ llvm-nm-18 /tmp/definitely-not-an-llvm-file
llvm-nm-18: error: /tmp/definitely-not-an-llvm-file: No such file or directory
$ printf 'exit status: %s\n' "$?"
exit status: 1
Check the path, permissions and file type before changing anything:
$ test -r /path/to/input.o && echo readable
$ file /path/to/input.o
If the tool reports no symbols, check whether the file was stripped and whether you actually meant to inspect dynamic symbols instead. --dynamic or -D selects dynamic symbols in place of normal ones. Do not infer that a stripped file has no callable code from this one report alone.
Done means
- Version confirmed. You confirmed
llvm-nm-18is version 18.1.3 from the installedllvm-18package. - Input made explicit. You passed an explicit input and know that an omitted filename means
a.out. - Type column understood. You can distinguish defined, undefined, weak and code symbols from the type column.
- Output filtered first. You used filtering or
-jbefore handing output to another tool. - Failures checked. You checked missing-file errors and kept the original input unchanged throughout.