Debugging tools overview
The NDK provides several debugging tools:- Android Studio Debugger - Integrated debugging with source-level stepping
- LLDB - Command-line debugger for native code
- ndk-stack - Symbolicate native stack traces from logcat
- GDB - Alternative debugger (deprecated, use LLDB instead)
Preparing your build for debugging
Enable debuggable builds
In yourAndroidManifest.xml:
build.gradle:
Include debug symbols
For CMake builds, inCMakeLists.txt:
Android.mk:
Debug builds with symbols are significantly larger. Use separate debug and release configurations.
Debugging with Android Studio
Setting up native debugging
1
Configure debug type
In Run/Debug Configuration, set Debug type to Dual (Java + Native) or Native.
2
Set breakpoints in native code
Open your C/C++ source file and click in the gutter next to the line number to set a breakpoint.
3
Start debugging
Click the Debug button or press Shift+F9. The debugger will attach to your app and pause at breakpoints.
4
Inspect variables and step through code
Use the debugger controls:
- Step Over (F8) - Execute current line
- Step Into (F7) - Step into function calls
- Step Out (Shift+F8) - Return from current function
- Resume (F9) - Continue execution
Evaluating expressions
In the Variables pane, right-click and select Evaluate Expression or press Alt+F8:Conditional breakpoints
Right-click a breakpoint and add a condition:Using LLDB from command line
Attach LLDB to running app
Setting breakpoints in LLDB
Inspecting variables
Stepping through code
Examining the call stack
Analyzing crashes with ndk-stack
ndk-stack symbolizes native stack traces from logcat output.Capture crash log
Symbolicate with ndk-stack
Understanding tombstones
When native code crashes, Android creates a tombstone file with detailed crash information.Retrieve tombstones
Tombstone contents
A tombstone includes:- Signal that caused the crash (SIGSEGV, SIGABRT, etc.)
- Registers at time of crash
- Stack trace
- Memory near the crash address
- Thread information
Common crash signals
- SIGSEGV - Segmentation fault (null pointer, invalid memory access)
- SIGABRT - Abort called (often from assertions or memory corruption)
- SIGILL - Illegal instruction (corrupted code or wrong architecture)
- SIGFPE - Floating point exception (division by zero)
- SIGBUS - Bus error (misaligned memory access)
Common debugging scenarios
Debugging JNI calls
Enable CheckJNI to catch JNI errors:Debugging memory issues
Use AddressSanitizer to detect memory errors: Inbuild.gradle:
Debugging threading issues
Use Thread Sanitizer for race conditions:Sanitizers increase memory usage and reduce performance. Use only in debug builds.
Logging from native code
Using Android log
CMakeLists.txt:
Viewing logs
Best practices
- Use debug builds - Include symbols and disable optimizations for debugging
- Log strategically - Add logs at key points, but not in tight loops
- Validate pointers - Check for null before dereferencing
- Enable sanitizers - Catch memory and threading issues during development
- Save crash logs - Always capture logcat when debugging crashes
- Test on real devices - Emulators may not reproduce device-specific issues
- Use assertions - Add
assert()calls to catch invalid assumptions early
Additional resources
- LLDB Documentation - Official LLDB tutorial
- Android Debug Guide - Official NDK debugging guide
- Tombstone Analysis - Understanding crash dumps
- AddressSanitizer - Detecting memory errors