adb logcat Crash Logs and bugreport Tombstones: Debug Android Crashes
On this page
Short answer: run adb logcat -b crash -d right after the crash. It dumps Android’s crash buffer, which holds the FATAL EXCEPTION of a Java or Kotlin crash and the signal, fault address and backtrace of a native one. For a crash that is no longer in the buffer, or to get the full tombstone file, run adb bugreport and look under FS/data/tombstones/ in the zip. Then map native frames to source lines with ndk-stack and your unstripped .so files.
Everything below is real output from a small test app I wrote for this guide (com.example.crashlab, one Java bug and one native bug), on an Android 14 arm64 emulator (google_apis, a userdebug build) with adb 37.0.1 and NDK r28.
Step 1: Reproduce the crash and read the crash buffer
The test app’s launcher activity calls .trim() on an intent extra that is usually missing:
static int titleLength(String title) {
return title.trim().length(); // bug: title is null when the extra is missing
}
Clear the old log, start the app, then dump the crash buffer:
adb logcat -c
adb shell am start -W -n com.example.crashlab/.MainActivity
adb logcat -b crash -d
-b crash selects the buffer that holds crash reports only, so you don’t have to dig through main. -d dumps what is there and exits. Without -d, logcat keeps streaming. The output, trimmed in the middle:
--------- beginning of crash
11-02 08:29:44.257 4071 4071 E AndroidRuntime: FATAL EXCEPTION: main
11-02 08:29:44.257 4071 4071 E AndroidRuntime: Process: com.example.crashlab, PID: 4071
11-02 08:29:44.257 4071 4071 E AndroidRuntime: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.crashlab/com.example.crashlab.MainActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.trim()' on a null object reference
11-02 08:29:44.257 4071 4071 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
...
11-02 08:29:44.257 4071 4071 E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.trim()' on a null object reference
11-02 08:29:44.257 4071 4071 E AndroidRuntime: at com.example.crashlab.MainActivity.titleLength(MainActivity.java:24)
11-02 08:29:44.257 4071 4071 E AndroidRuntime: at com.example.crashlab.MainActivity.onCreate(MainActivity.java:20)
How to read a FATAL EXCEPTION:
mainafter FATAL EXCEPTION is the thread name. A crash on the main thread duringonCreatemeans the screen never appeared.- The top exception is often a wrapper.
RuntimeException: Unable to start activityis the framework reporting that your activity failed. The real cause is the lastCaused by:block. - Read the first frame in your own package.
MainActivity.titleLength(MainActivity.java:24)is the line that threw.onCreate(MainActivity.java:20)called it. - The PID is in the line itself. Column three (
4071) is the process ID.
Filter by tag or by process
To see what the app logged just before it died, filter by tag. *:S silences everything you did not name:
adb logcat -d AndroidRuntime:E CrashLab:I '*:S'
--------- beginning of main
11-02 08:29:44.254 4071 4071 I CrashLab: started, mode=null
--------- beginning of crash
11-02 08:29:44.257 4071 4071 E AndroidRuntime: FATAL EXCEPTION: main
...
Or keep one process with --pid. Take the PID from the crash line, not from the process list: by the time I ran adb shell pidof com.example.crashlab, it printed nothing and exited with 1, because the process was already gone. adb logcat -d --pid=4071 then showed the whole short life of that process, ending with:
11-02 08:29:44.254 4071 4071 I CrashLab: started, mode=null
11-02 08:29:44.255 4071 4071 D AndroidRuntime: Shutting down VM
...
11-02 08:29:44.281 4071 4071 I Process : Sending signal. PID: 4071 SIG: 9
A relaunch gets a new PID, so filter on the PID of the run you are looking at. And don’t use a PID filter for native crashes, as the next step shows.
Step 2: Read a native crash
The app’s JNI library has a null pointer bug of its own:
struct note { int id; const char *title; };
static int title_len(const struct note *n) {
int len = 0;
while (n->title[len]) len++; /* bug: n is NULL */
return len;
}
The activity calls it when started with --es mode native:
adb logcat -c
adb shell am start -W -n com.example.crashlab/.MainActivity --es mode native
adb logcat -b crash -d
The top of the 122-line report (the register dump is cut here):
11-02 08:29:47.649 4169 4169 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x8 in tid 4169 (xample.crashlab), pid 4169 (xample.crashlab)
11-02 08:29:48.577 4197 4197 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
11-02 08:29:48.577 4197 4197 F DEBUG : Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.050/12077443:userdebug/dev-keys'
11-02 08:29:48.577 4197 4197 F DEBUG : ABI: 'arm64'
11-02 08:29:48.577 4197 4197 F DEBUG : Cmdline: com.example.crashlab
11-02 08:29:48.577 4197 4197 F DEBUG : pid: 4169, tid: 4169, name: xample.crashlab >>> com.example.crashlab <<<
11-02 08:29:48.577 4197 4197 F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000008
11-02 08:29:48.577 4197 4197 F DEBUG : Cause: null pointer dereference
...
11-02 08:29:48.577 4197 4197 F DEBUG : 96 total frames
11-02 08:29:48.577 4197 4197 F DEBUG : backtrace:
11-02 08:29:48.577 4197 4197 F DEBUG : #00 pc 0000000000004668 /data/app/~~w0h889adlwFt5yyytCrJJw==/com.example.crashlab-LtMJHkBm6Dq_fjb_skPayA==/lib/arm64/libcrashlab.so
11-02 08:29:48.577 4197 4197 F DEBUG : #01 pc 0000000000004644 /data/app/~~w0h889adlwFt5yyytCrJJw==/com.example.crashlab-LtMJHkBm6Dq_fjb_skPayA==/lib/arm64/libcrashlab.so (Java_com_example_crashlab_MainActivity_nativeCrash+28)
SIGSEGV,SEGV_MAPERRmeans the code read an address that is not mapped.fault addr 0x8rather than0x0is a clue. On arm64,titlesits 8 bytes intostruct note, so readingn->titlethrough a NULLntouches address 8. A small fault address almost always means a field read through a null struct pointer.#00is where it died. Thepcvalue (0x4668) is an offset insidelibcrashlab.so, which is what you need for symbolization. Frame#00has no function name because the APK ships a stripped library andtitle_lenis astaticfunction. The exported JNI function in#01keeps its name.
Do not filter native crashes by the app’s PID. Only the first libc line comes from the app (PID 4169). Everything tagged DEBUG is written by Android’s crash handler, crash_dump, which runs as its own process (4197 here). --pid=4169 would hide the whole report.
Don’t cap the line count too low. adb logcat -b crash -d -t 80 keeps the newest 80 lines, so on this crash its first line after the header was frame #16, and the signal, fault address and frames #00 to #15 were gone. Use -d without -t for the crash buffer, which is small anyway: adb logcat -g reported a 2 MiB ring buffer with 24 KiB readable.
Step 3: Get the tombstone with adb bugreport
The crash buffer is a ring buffer, so older crashes get pushed out. The tombstone file under /data/tombstones lasts longer, but a normal shell cannot read that directory:
$ adb shell ls -l /data/tombstones
ls: /data/tombstones: Permission denied
On an emulator image without Google Play, adb root works, and the same ls then listed tombstone_00 to tombstone_13, each with a .pb twin. Production phones don’t allow adb root. There, a bugreport is the supported way to get the files:
mkdir bugreports
adb bugreport bugreports/
/data/user_de/0/com.android.shell/files/bugreports/bugreport-sdk_gphone64_arm64-UE1A.230829.050-2026-11-02-08-30-26.zip: 1 file pulled, 0 skipped. 141.0 MB/s (7411260 bytes in 0.050s)
Bug report copied to bugreports//bugreport-sdk_gphone64_arm64-UE1A.230829.050-2026-11-02-08-30-26.zip
It took 44 seconds on the emulator. On a real phone, expect several minutes. The 7.4 MB zip held 226 files (67 MB unpacked), including a 41 MB bugreport-*.txt with the full dumpstate text and log buffers. List the crash files:
unzip -l bugreports/bugreport-*.zip | grep -E 'tombstone|/anr/'
40795 10-09-2026 01:38 FS/data/anr/anr_2026-10-09-01-38-02-618
225120 10-09-2026 01:29 FS/data/anr/anr_2026-10-09-01-29-02-830
251040 11-02-2026 08:29 FS/data/tombstones/tombstone_13.pb
424099 11-02-2026 08:29 FS/data/tombstones/tombstone_13
0 10-09-2026 11:53 FS/data/tombstones/tombstone_04.pb
449360 10-09-2026 01:38 FS/data/tombstones/tombstone_12
...
0 10-09-2026 01:30 FS/data/tombstones/tombstone_09
...
Pick the right tombstone. The archive also held tombstones and ANR traces from earlier sessions on the same emulator image, some of them empty files. Take the newest one and check that its pid: and Cmdline: lines match your crash:
unzip -o bugreports/bugreport-*.zip 'FS/data/tombstones/*' -d tomb
head -14 tomb/FS/data/tombstones/tombstone_13
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.050/12077443:userdebug/dev-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2026-11-02 08:29:47.724401727+0800
Process uptime: 1s
Cmdline: com.example.crashlab
pid: 4169, tid: 4169, name: xample.crashlab >>> com.example.crashlab <<<
uid: 10192
...
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000008
Cause: null pointer dereference
Same PID and fault address as the logcat report. The .pb file is the same tombstone in protobuf form. The text file has much more than the log lines: this one ran to 3,897 lines, with memory dumps near each register, a 2,766-entry memory map, open files and the tail of the app’s main log.
logcat or bugreport?
adb logcat -b crash -d |
adb bugreport |
|
|---|---|---|
| Speed | Seconds | 44 s on my emulator, minutes on a phone |
| Contents | Crash entries still in the ring buffer | Dumpstate, dumpsys, all log buffers, tombstones, ANR traces |
| Older crashes | Lost when the buffer wraps | Tombstones kept on the device |
| Size | Kilobytes | 7.4 MB zip here |
| Privacy | Your app’s crash, plus other crashes | Data from the whole device: treat it as sensitive |
Start with the crash buffer. Collect a bugreport when the crash is native, when it happened a while ago, or when you need the whole device state.
Step 4: Map frames to source
Native frames. Give ndk-stack the tombstone and the folder with your unstripped .so files for the crashing ABI. In a Gradle project they are under app/build/intermediates/. The copies inside the APK are usually stripped.
$ANDROID_NDK_HOME/ndk-stack -sym /path/to/unstripped/arm64-v8a \
-i tomb/FS/data/tombstones/tombstone_13
********** Crash dump: **********
Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.050/12077443:userdebug/dev-keys'
#00 0x0000000000004668 /data/app/~~w0h889adlwFt5yyytCrJJw==/com.example.crashlab-LtMJHkBm6Dq_fjb_skPayA==/lib/arm64/libcrashlab.so
title_len
.../jni/crashlab.c:8:15
#01 0x0000000000004644 /data/app/~~w0h889adlwFt5yyytCrJJw==/com.example.crashlab-LtMJHkBm6Dq_fjb_skPayA==/lib/arm64/libcrashlab.so (Java_com_example_crashlab_MainActivity_nativeCrash+28)
Java_com_example_crashlab_MainActivity_nativeCrash
.../jni/crashlab.c:15:12
(Local paths are shortened to ..., and the indentation of the #01 lines is reduced.)
Frame #00 now has its name back: title_len at crashlab.c:8:15, the while loop. For a single address, llvm-addr2line from the NDK gives the same answer:
$ llvm-addr2line -f -C -e libcrashlab.so 0x4668 0x4644
title_len
.../jni/crashlab.c:8
Java_com_example_crashlab_MainActivity_nativeCrash
.../jni/crashlab.c:15
ndk-stack also reads logcat text on standard input (adb logcat -b crash -d | ndk-stack -sym ...). Fed the saved crash-buffer dump from Step 2, it resolved the same two frames. That only works while the #00 frames are still in the buffer.
Java frames. If R8 minified your release build, frames read like a.b.c(Unknown Source). Run the trace through retrace with the build’s mapping.txt before you read it, or paste it into the browser-based R8 / ProGuard retrace, which reads the mapping locally. Deobfuscate an Android stack trace covers the details.
Troubleshooting
The crash buffer is empty. The app may not have crashed yet, the buffer was cleared (adb logcat -c clears it), or the device rebooted. Reproduce the crash and dump the buffer straight away, or use adb bugreport for an older native crash.
The native report starts in the middle of the backtrace. You used -t with a count that is too small. Dump the crash buffer without -t.
The tombstone has no line numbers. -sym points at stripped libraries, or at a different build. The .so files must come from the exact build that crashed, for the crashing ABI.
The library fails to load before anything crashes. That is often packaging, for example a missing ABI or 16 KB page alignment. The browser-based APK analyzer reads native libraries and their alignment from the APK without uploading it.
Optional: collect the same data with REA
If you want an AI agent to drive the collection, REA (a third-party open-source CLI and MCP server) wraps these commands and records each result as Evidence the agent can cite. In 6.7.0:
export REA_ADB_PATH="$HOME/Library/Android/sdk/platform-tools/adb" # must be absolute
rea start-adb-app <serial> com.example.crashlab --json
rea read-adb-logcat <serial> --buffer crash --count 1000 --json
rea collect-adb-bugreport <serial> --output-directory "$PWD/bugreports" --json
What I saw in an earlier run of the same app:
read-adb-logcatrunslogcat -d -b crash -t <count>, keeps the newest lines and marks the result"coverage": "partial"whenever it returns exactly--countlines. With--count 80the native report was cut off at frame#16, just like-t 80above.--count 1000returned 146 lines with"coverage": "complete".- It filters by buffer and PID only, has no tag filter or live streaming, and fails with a resource error rather than truncate when a dump exceeds 4 MiB.
collect-adb-bugreportstores the zip and records its size and SHA-256. It does not parse tombstones, sounzipandndk-stackare still your job.list-adb-directoryon/data/tombstonesreturnedunsupported_targetwith the device’sPermission denied.