Native crashes in Android NDK code can be challenging to debug. Understanding tombstone files, stack traces, and crash signals is essential for diagnosing and fixing issues.
Native crash basics
When native code crashes, Android generates a tombstone file containing detailed crash information. The crash also appears in logcat with a summary.
Common crash signals
Viewing tombstones
Tombstone files are stored on the device:
Tombstone files are rotated. Pull them immediately after a crash before they’re overwritten.
Logcat crash output
Crashes also appear in logcat:
Reading tombstones
A tombstone contains several sections with crucial debugging information.
Example tombstone
Key sections explained
- Signal 11 (SIGSEGV) - Segmentation fault
- Code 1 (SEGV_MAPERR) - Accessed unmapped memory
- Fault addr 0x0 - Attempted to access address 0 (null pointer)
CPU registers
- pc (program counter) - Where the crash occurred
- sp (stack pointer) - Current stack position
- lr (link register) - Return address
- x0-x30 - General purpose registers (may contain function arguments)
Stack trace (backtrace)
Each frame shows:
- Frame number
- Program counter offset
- Library path
- Function name and offset (if symbols available)
Analyzing stack traces
Using addr2line
Convert addresses to source code locations:
Output:
Using ndk-stack
Automatic symbolication of entire stack traces:
Output:
ndk-stack requires unstripped .so files with debug symbols. Never strip libraries during development.
Using Android Studio
Android Studio can automatically symbolicate crashes:
- Go to View > Tool Windows > Logcat
- Crashes with available symbols show as clickable links
- Click to jump to the source line
Common crash patterns
Null pointer dereference
Code example:
Fix: Always check pointers before dereferencing:
Use after free
Fault address is non-zero but unmapped.
Code example:
Detection: Use Address Sanitizer:
Stack overflow
Code example:
Fix:
- Limit recursion depth
- Use iteration instead of recursion
- Reduce stack-allocated buffer sizes
Buffer overflow
Code example:
Detection: Use Address Sanitizer or enable stack protector:
Assertion failures
Code example:
The abort message tells you exactly which assertion failed.
Invalid JNI references
Code example:
Prevention:
- Enable CheckJNI:
adb shell setprop debug.checkjni 1
- Never use JNI references after deleting them
- Don’t cache local references across JNI calls
Debugging techniques
Using debuggers
LLDB in Android Studio
- Set breakpoint in native code
- Run app in debug mode
- Use debug controls when breakpoint hits
Command-line debugging
Sanitizers
Enable runtime error detection:
Sanitizers significantly increase app size and reduce performance. Use only during development.
Logging and diagnostics
Add strategic logging:
Crash reporting services
Use crash reporting to collect crashes from users:
- Firebase Crashlytics - Google’s crash reporting
- Sentry - Open source crash reporting
- Bugsnag - Commercial crash reporting
These services automatically symbolicate and aggregate crashes.
Reproducing crashes
Make crashes consistent
-
Clear app data before each test:
-
Use same device/emulator - Different devices may behave differently
-
Test with sanitizers - Makes crashes more reliable:
-
Disable optimizations during debugging:
Minimize test case
Reduce the code to the smallest example that crashes:
- Remove unrelated code
- Simplify inputs
- Isolate the crashing function
- Create a standalone reproduction
Prevention strategies
Code review checklist
- Check all pointers for null before dereferencing
- Verify array bounds on all accesses
- Match every
new with delete, malloc with free
- Delete JNI local references in loops
- Use RAII (Resource Acquisition Is Initialization)
- Avoid manual memory management when possible
Defensive programming
Modern C++ practices
Additional resources
Always keep unstripped .so files for every release build. You’ll need them to symbolicate crashes from users.