Home  /  Mobile OS & Software  /  Analysis
Mobile OS & Software

Mobile Crash Analysis: Reading Tombstones, Crash Logs and Stack Traces

By Jones Consortium Editorial — Mobile Network SpecialistAugust 15, 20269 min read

A native crash on Android or iOS leaves a forensic artifact, not just a dialog. On Android it's the tombstone; on iOS it's the Jetsam or exception crash log. Learn to read them and you can pinpoint a faulty JNI library, a stack overflow, or a use-after-free in minutes.

TL;DR: 90% of mobile native crashes are fatal signals (SIGSEGV, SIGABRT, SIGBUS). The tombstone decodes the signal, register state, and unwound stack. The trick is mapping program counters to symbols and understanding who installed the signal handler last.

1. What a Tombstone Actually Is

On Android, when a native process dies from an unhandled signal, debuggerd intercepts it. The kernel notifies debuggerd via a ptrace connection, which freezes the crashing threads, unwinds their stacks, dumps registers, memory maps, and open file descriptors, then writes /data/tombstones/tombstone_XX.

This is fundamentally different from a Java/Kotlin exception. A Java crash is an uncaught exception handled by Thread.UncaughtExceptionHandler and reported by logcat. A tombstone is a POSIX signal that bypassed every handler. It means memory corruption, ABI violation, or deliberate abort() — not a null pointer in Kotlin.

On iOS, the equivalent is the crash report in Settings > Privacy & Analytics or Xcode Organizer. ReportCrash and the kernel's exc_handler capture Mach exceptions, BSD signals, and Jetsam memory kills. The format differs, but the anatomy — exception type, thread state, binary images, and backtrace — is identical in purpose.

If you work with NDK, game engines, or chiplet-based mobile SoCs where drivers are tightly coupled to user space, tombstones are your primary debug interface. Without symbols, they look like hex soup. With symbols, they are precise.

2. Anatomy of an Android Tombstone

Every tombstone follows the same header. Learn it once and you can triage any crash without a debugger attached.

Mobile Connectivity Standards Comparison
Feature 5G NR Wi-Fi 7 Bluetooth 5.4
Peak Throughput20 Gbps46 Gbps2 Mbps
Latency1–4 ms1–2 ms3–6 ms
RangeSeveral km30–50 m100–400 m
Power DrawHighMediumUltra-low
Primary UseCellular dataLocal networkingPeripherals/IoT
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** Build fingerprint: 'google/raven/raven:14/UQ1A.240205.004/11269748:user/release-keys' Revision: '0' ABI: 'arm64' Timestamp: 2026-08-12 14:22:31.142 +0000 Process uptime: 12s Cmdline: com.jones.example/.MainActivity pid: 8421, tid: 8455, name: RenderThread >>> com.jones.example <<< uid: 10241 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000010 x0 0000007ff4a3e0a0 x1 0000000000000000 x2 0000000000000001 x3 0000007ff4a3e0c0 x4 0000000000000000 x5 0000000000000000 x6 0000000000000000 x7 00ff00ff00ff00ff pc 0000007c3e4123d4 sp 0000007ff4a3e0d0 pstate 0000000060001000 backtrace: #00 pc 00000000000123d4 /data/app/.../lib/arm64/libgame.so (RenderFrame+84) #01 pc 00000000000125a0 /data/app/.../lib/arm64/libgame.so (GameLoop+312) #02 pc 000000000008a1c8 /apex/com.android.runtime/lib64/libart.so (art::Thread::CreateCallback+...)

Decoding the header line by line

signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x10 — The most valuable line. Signal 11 is segmentation fault. SEGV_MAPERR means the address was not mapped. SEGV_ACCERR would mean permission fault. fault addr 0x10 is a classic null-pointer dereference with offset 16 (e.g., accessing field at offset 0x10 of a null object). If you see 0x00000000 with low offset, suspect a null this.

ABI: arm64, pc, sp, pstate — pc is the faulting instruction address. On ARM64, if pc is 4-byte aligned and points into executable .text, it's a real code fault. If pc is 0x0 or a heap address, you jumped to a corrupted function pointer.

backtrace — Already symbolized if libgame.so was built with -g and not stripped. RenderFrame+84 means 84 bytes into the function. This is accurate to the instruction. Compare this to a managed stack trace which only shows method names — here you get exact offsets.

Newer Android 12+ tombstones are protobuf-based and include open files, memory map, and tagged pointer info for MTE (Memory Tagging Extension). If MTE is enabled, you'll see SEGV_MTESERR or SEGV_MTEAERR — invaluable for detecting heap buffer overflows synchronously.

Tombstone Generation Pipeline — Android Native Crash Flow
App Process libgame.so SIGSEGV Kernel sends siginfo_t debuggerd ptrace + unwind tombstone_XX /data/tombstones/ Captured State Registers (x0-x30, pc, sp) Memory maps & build fingerprint Open FDs & thread list Stack Unwinding libunwindstack + .eh_frame .ARM.exidx (32-bit) / PAC MTE tags if enabled Triage Signals SIGSEGV → bad pointer SIGABRT → assert / abort() SIGBUS → alignment / mmap
Figure 1 — Signal delivery to tombstone. debuggerd unwinds using DWARF .eh_frame; failures here produce truncated <unknown> frames.

3. Signals: The Language of Native Crashes

Every native crash is a signal. Memorize the six you will see in production:

# Fatal signals you must know SIGSEGV (11) // Invalid memory access - null deref, use-after-free, overflow SIGABRT (6) // abort() - failed CHECK(), assertion, heap corruption detected by jemalloc/scudo SIGBUS (7) // Bus error - misaligned access, bad mmap, or MTE tag mismatch on ARM SIGFPE (8) // Arithmetic - integer divide by zero (ARM64 traps, x86 may not) SIGILL (4) // Illegal instruction - corrupted code, wrong ABI, or CFI violation SIGSTKFLT / SIGSYS (31) // seccomp / bad syscall - often seen with hardened WebView or sandbox # Non-fatal but visible in logs SIGPIPE (13) // Ignored by default but crashes if not handled during socket write

SIGABRT deserves special attention. It rarely means you called abort() directly. On Android, CHECK() from base/logging.h, failed FORTIFY checks, Scudo heap canaries, or __stack_chk_fail all raise SIGABRT and print a diagnostic in the tombstone's Abort message: field. That message often contains the exact C++ condition string — read it before the backtrace.

On modern ARM cores like the Cortex-X925, SIGBUS increasingly signals MTE violations. Synchronous MTE gives you the faulting address and allocation tag, letting you trace a heap overflow to the exact allocation site with HWASAN style reports. Asynchronous MTE only reports eventually — check Cause: [MTE] in the tombstone to distinguish.

4. Stack Traces: Unwinding Without Symbols

A raw backtrace is a list of program counters. Symbolication turns pc 00000000000123d4 into RenderFrame+84 at renderer.cpp:142. Three layers matter:

1. Unwinding: libunwindstack walks the stack using .eh_frame and .debug_frame (DWARF), or .ARM.exidx on 32-bit. If the stack is corrupted or you omitted -funwind-tables, unwinding stops early and you get a truncated trace. Always keep -funwind-tables even in release builds — cost is ~2% binary size.

2. Symbolization: Use ndk-stack or llvm-symbolizer with unstripped .so files:

ndk-stack -sym ./app/build/intermediates/stripped_native_libs/release/out/lib/arm64 \ -dump tombstone_09 # or directly: llvm-symbolizer --obj=libgame.so 0x123d4 0x125a0 renderer.cpp:142 RenderFrame(RenderContext*) game.cpp:88 GameLoop()

3. Inline frames and tail calls: At -O2 the compiler inlines hot functions. The tombstone may skip them. Rebuild that library with -fno-omit-frame-pointer for the suspect module or capture a perf profile to correlate. For cloud gaming and latency-sensitive engines, frame-pointer omission is common — expect noisier traces.

iOS symbolication is similar but uses atos and dSYMs. The crash log lists Binary Images: with load addresses and UUIDs. Without the matching dSYM, atos -o MyApp.app/MyApp -l 0x100000000 0x100023d4 is your manual fallback.

5. Crash Log vs Tombstone vs ANR

Teams confuse three artifacts. Using the wrong one wastes hours.

ArtifactTriggerWhere to FindWhat It Tells You
TombstoneUnhandled native signal (C/C++/Rust)/data/tombstones/, Firebase Crashlytics NDK, Play ConsoleRegisters, fault addr, native backtrace, memory maps. Ground truth for memory errors.
Java Crash LogUncaught Java/Kotlin exceptionlogcat, Crashlytics, dropbox/SYSTEM_TOMBSTONEException type + managed stack trace. No registers. Symbolication via mapping.txt (R8).
ANR TraceInput or Broadcast timeout (5s/10s)/data/anr/traces.txtAll thread states + held locks. Shows main thread blocked, not crashed. Look for BLOCKED and waiting on <0x...>.
iOS Crash LogMach exception, signal, Jetsam, watchdogXcode Organizer, ips filesException codes, termination reason (Namespace JETSAM), thread backtraces, memory footprint.

A real-world tip: if you see both a Java stack trace and a tombstone for the same PID close in time, the Java crash likely triggered a native abort inside libart (e.g., JNI GetStringChars on a null jobject). Fix the JNI boundary first.

6. Field-Tested Reading Workflow

Use this checklist for every tombstone. It takes under 10 minutes once practiced:

Step 1 — Classify the signal and fault address. fault addr 0x10 = null+offset. 0x7f... near stack top = stack overflow. 0x78... = heap address — check memory map for [heap] or [anon:scudo]. If address is 0x5a5a5a5a or 0xaaaaaaaa, it's a use-after-free poison value from allocator debugging.

Step 2 — Check the faulting thread vs main thread. A RenderThread or Binder:xxx crash points to threading. If tid != pid, you are off main. Cross-reference with backtrace of all threads at the end of the tombstone for deadlocks.

Step 3 — Symbolicate the top 3 frames. You only need the crash site and its caller. Ignore libart.so or libc.so frames unless the fault is inside them — then suspect bad arguments passed from your code.

Step 4 — Inspect the maps and registers. Is pc inside r-xp (executable) or rw-p (writable)? Writable + executable = JIT or corrupted. Check x0 — on ARM64 the first argument is often the null pointer. If x0 == 0 and fault addr is x0 + offset, confirm in source that argument is not null-checked.

Step 5 — Reproduce with sanitizers. Build with -fsanitize=address,hwaddress (HWASAN on ARM64) or run under MTE in sync mode. HWASAN will abort at the overflow with allocation and free stack traces — far richer than a tombstone. For memory pressure issues, correlate with RAM and low-memory killer tests to rule out Jetsam/LMK before blaming code.

# Minimal repro build for Android ARM64 cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_CXX_FLAGS="-fsanitize=hwaddress -fno-omit-frame-pointer -g" \ -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=hwaddress" # Validate signal handler chain — your crash reporter may hide tombstones adb shell debuggerd -b 8421 # force tombstone for testing

Common pitfall: third-party crash SDKs install their own sigaction handlers and may swallow the signal before debuggerd. If tombstones disappear after adding a SDK, verify handler chaining with sigaction returning previous handler and calling it. Otherwise you lose register dumps forever.

Mastering tombstones turns native crashes from black boxes into surgical fixes. Keep symbols, keep unwind tables, enable MTE in testing, and always read the fault address before the stack.

Frequently Asked Questions

What is the difference between a tombstone and logcat crash?

Logcat shows Java/Kotlin uncaught exceptions with managed stack traces. A tombstone is generated by debuggerd for native POSIX signals (SIGSEGV, SIGABRT) and contains registers, memory maps, and unwound native backtraces. You need both: logcat for managed code, tombstones for NDK, drivers, and game engines.

Why does my tombstone show fault addr 0x00000000 or a small offset like 0x10?

This is a null-pointer dereference. Accessing a field at offset 0x10 of a null object yields fault addr 0x10. Check the register holding this or the first argument (x0 on ARM64) — it will be zero. Fix by null-checking at the JNI boundary or callee.

How do I symbolicate a stripped release build?

Keep unstripped .so files or breakpad symbols from your CI build. Use ndk-stack -sym <unstripped-dir> -dump tombstone or llvm-symbolizer --obj=unstripped_lib.so <pc>. For iOS, use the matching dSYM with atos -o MyApp -l <load_addr> <pc>. Without matching symbols you only get offsets.

Can a crash handler prevent tombstones from being written?

Yes. If an SDK installs a signal handler with sigaction and does not chain to the previous handler or re-raise, debuggerd never sees the signal. Always chain handlers or use debuggerd's tombstoned intercept. Test with kill -11 <pid> after integrating any crash reporter.

References & Standards: 3GPP specifications · Bluetooth SIG