Skip to main content
Debugging native code on Android requires specialized tools and techniques. This guide covers debugging crashes, setting breakpoints, and inspecting native code execution.

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)
For most developers, the Android Studio debugger provides the best debugging experience with native code support.

Preparing your build for debugging

Enable debuggable builds

In your AndroidManifest.xml:
Or in build.gradle:

Include debug symbols

For CMake builds, in CMakeLists.txt:
For ndk-build, in 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

In the LLDB prompt:

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

Output shows source file locations:
Use ndk-stack even when you don’t have the device physically available - just save the logcat output.

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)
Null pointer dereferences are the most common cause of native crashes. Always validate pointers before use.

Common debugging scenarios

Debugging JNI calls

Enable CheckJNI to catch JNI errors:
Common JNI errors:

Debugging memory issues

Use AddressSanitizer to detect memory errors: In build.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

Add to 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