Symbolizing Native Crashes: ndk-stack
When a native crash occurs and a tombstone is generated (or a crash dump is printed to Logcat), the stack trace usually contains raw hexadecimal addresses (Program Counters or PCs). To make this actionable, these addresses must be translated back into source code files, function names, and line numbers. This process is called symbolization.
Symbolizing Native Stack Traces
Consider a raw crash log from Logcat:
backtrace:
#00 pc 0000000000012348 /data/app/.../libnative-lib.so
#01 pc 0000000000012500 /data/app/.../libnative-lib.so
The system knows the crash happened inside libnative-lib.so at offset 0x12348, but the stripped library on the device doesn't contain the mapping back to your C++ code.
The ndk-stack tool, included in the Android NDK, automates the symbolization process. It parses the crash log, finds the corresponding unstripped shared libraries on your host machine, and replaces the hex addresses with human-readable symbols.
ndk-stack Usage with Logcat
The most common use case for ndk-stack is piping live Logcat output directly into it. You must provide the -sym argument, pointing to the directory containing your unstripped .so files.
# Using ndk-stack with live logcat
adb logcat | $NDK/ndk-stack -sym app/build/intermediates/cmake/debug/obj/arm64-v8a/
When a crash occurs, ndk-stack will intercept the dump and output a symbolized version:
********** Crash dump: **********
Build fingerprint: 'google/pixel_6/oriole:13/...'
...
backtrace:
#00 0x0000000000012348 /data/app/.../libnative-lib.so (MyClass::doWork()+80)
MyClass.cpp:42
#01 0x0000000000012500 /data/app/.../libnative-lib.so (Java_com_example_app_MainActivity_startWork+120)
native-lib.cpp:15
Now you know exactly that the crash happened on line 42 of MyClass.cpp.
Symbolizing Tombstones with addr2line
If you have pulled a tombstone file from the device (adb pull /data/tombstones/tombstone_00), you can pass it to ndk-stack using the -dump argument:
$NDK/ndk-stack -sym path/to/unstripped/libs -dump tombstone_00
Under the Hood: addr2line and llvm-symbolizer
ndk-stack is essentially a smart wrapper script. The actual heavy lifting of mapping addresses to lines of code is performed by underlying binary utilities.
Historically, this was addr2line (from GNU Binutils). In modern NDKs and AOSP builds, this is handled by llvm-symbolizer.
You can use llvm-symbolizer manually if you only need to look up a single address. You provide the unstripped library and the address offset:
# Manual symbolization using llvm-symbolizer
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-symbolizer \
--obj=path/to/unstripped/libnative-lib.so \
0x12348
This will output the function name, file, and line number for that specific memory offset.