Copy an address out of a crash report and llvm-addr2line-20 will tell you the exact file and line behind it. You will finish with a repeatable way to do that, then add the function name when the basic result is not enough. The examples use llvm-addr2line-20 from Debian package llvm-20, installed here at version 1:20.1.8~++20250804090239+87f0227cb601-1~exp1~20250804210352.139.
Allow about fifteen minutes. You need the executable or shared object that contains the address, plus matching debugging information. The workflow is read-only: it does not rewrite binaries, load modules, or need elevated privileges.
Warning: do not use sudo to compensate for a wrong executable or missing symbols. It will not fix either problem.
Confirm which binary will run and record its version. These are ordinary commands:
$ command -v llvm-addr2line-20
/usr/bin/llvm-addr2line-20
$ llvm-addr2line-20 --version
llvm-addr2line
Ubuntu LLVM version 20.1.8
Optimized build.
The command is an alias for LLVM's symbolizer with addr2line-style defaults. In particular, it treats input addresses as hexadecimal even when they do not start with 0x. That matters when you copy an address from a log: 1161 is read as hexadecimal here, not decimal.
Checkpoint: stop if the command is not the version you expected. A different LLVM installation can carry different defaults and diagnostics.
Use -e to name the executable or object file that contains the address. Replace the two placeholders below with values from the same build:
$ OBJECT='/path/to/application'
$ ADDRESS='1161'
$ printf '%s\n' "$ADDRESS" | llvm-addr2line-20 -e "$OBJECT"
A successful lookup prints a location such as /build/app/main.c:42. The exact path and line depend on the binary. With no function option, the normal output is just the source location. An address may also be written as 0x1161, the optional prefix is simply ignored.
Do not guess the object from the process name. A shared library, a relocated executable or a post-build copy can make an address meaningless when paired with the wrong file. If the program was stripped, keep its separate debug file to hand and use the debug-file options shown by llvm-addr2line-20 --help.
Need a known-good test? Compile a tiny program with debug information and pull an address from its symbol table. This only changes files in the current directory, so use a disposable one:
$ mkdir -p /tmp/addr2line-demo
$ printf '%s\n' '#include <stdio.h>' 'static int add(int a, int b) { return a + b; }' 'int main(void) { printf("%d\n", add(2, 3)); return 0; }' > /tmp/addr2line-demo/demo.c
$ cc -g -O0 -o /tmp/addr2line-demo/demo /tmp/addr2line-demo/demo.c
$ nm -an /tmp/addr2line-demo/demo | grep ' main$'
0000000000001161 T main
$ printf '%s\n' 1161 | llvm-addr2line-20 -e /tmp/addr2line-demo/demo
/tmp/addr2line-demo/demo.c:3
The address itself is just an example: compiler, linker and optimisation choices all change it. The useful check is that the result names demo.c and the line containing main. If cc or nm is not available, use an existing debug build and its own symbol inspection tools instead.
The temporary directory now holds generated source and a binary. Remove it once you no longer need it, but inspect the path first: deletion is irreversible.
Use -f to print the function, -C to demangle C++ names, -a to repeat the queried address, and -i to show inline frames:
$ printf '%s\n' 0x1149 | llvm-addr2line-20 -e /tmp/addr2line-demo/demo -f -C -a -i
0x1149
add
/tmp/addr2line-demo/demo.c:2
The order matters when you are processing several addresses: each result groups under its input address. -C makes no visible difference for the plain C function above, but it is the switch that matters for readable C++ names. The default leaves out function names, demangling and inline frames, so do not treat a plain file-and-line result as proof that no function information exists.
For a human-facing report, --basenames strips directory names from source paths. For a parser, ask for JSON explicitly:
$ printf '%s\n' 1161 | llvm-addr2line-20 --output-style=JSON -e /tmp/addr2line-demo/demo
{"Address":"0x1161","ModuleName":"/tmp/addr2line-demo/demo","Symbol":[{"Column":16,"Discriminator":0,"FileName":"/tmp/addr2line-demo/demo.c","FunctionName":"","Line":3,"StartAddress":"0x1161","StartFileName":"/tmp/addr2line-demo/demo.c","StartLine":3}]}
JSON is far more stable for automation than splitting the default text on newlines, especially once inline frames or several addresses are involved. Treat paths and symbol names as data, not shell code. If you only need a short display path, weigh the trade-off before using --basenames, because two different directories can contain files with the same basename.
If the result comes back ??:0 or similar, check the object first, then the build. The address may sit outside that file, the binary may lack usable line tables, or its debug information may not match the executable. Repeat the lookup against the exact unstripped or separate-debug build used for the crash.
A missing object gives a clearer failure:
$ printf '%s\n' 1161 | llvm-addr2line-20 -e /tmp/does-not-exist-addr2line
llvm-addr2line-20: error: '/tmp/does-not-exist-addr2line': No such file or directory
The command exits non-zero here. Check the path and read permission without touching ownership or permissions:
$ test -r "$OBJECT" && echo readable
readable
Do not invent a source location from a failed lookup. Record the binary build identifier, address, load offset and debug-file location, then chase down the symbol package or relocation details. Elevated privileges will not repair a mismatched address.
llvm-addr2line-20 version and executable.0x is optional.