Debug Android App Crashes with adb logcat and bugreport in REA
On this page
Your app closes on a tester’s phone and all you have is “it crashed”. Android already recorded what happened. A Java or Kotlin crash goes to the log as FATAL EXCEPTION. A native crash in C or C++ code produces a tombstone with the signal, the fault address and a backtrace. This guide shows how to collect both with REA, the open-source project by morluto, so that an AI agent can read them as Evidence and help you find the line of code at fault.
Every log line and file listing below is real. It came from a small test app we wrote for this guide, running on an Android 14 emulator, collected with REA 6.4.0.
Who this is for: Android developers who want an agent to help with crash triage, and anyone who has asked “logcat or bugreport?”.
What you will learn:
- Connect REA to
adband check the device - Reproduce a crash with
start-adb-appandstart-adb-activity - Read the
crashbuffer and understand aFATAL EXCEPTION - Read a native crash, and why the default line count can cut it off
- Collect a bugreport and find the tombstone inside it
- Map frames back to source, for Java and for native code
Authorization first. Only debug apps you build or are authorized to test, on devices you own or control.
start-adb-app,start-adb-activityandinstall-adb-packagechange device state. Logs can contain personal data from other apps, so treat a bugreport as sensitive. Read REA’s DISCLAIMER.md.
Prerequisites
- REA 6.4.0 (
npm install --global rea-agents). Setup for Claude Code, Cursor and Codex is in Install REA and connect it to your agent. - Android platform-tools
adb. REA never installs it. It usesadbfrom yourPATH, or the absolute path inREA_ADB_PATH. - A device with USB debugging enabled, or an emulator. REA does not start emulators.
- Your app’s debug build, plus its unstripped native libraries and R8
mapping.txtif it has them.
Step 1: Point REA at adb and pick a device
export REA_ADB_PATH="$HOME/Library/Android/sdk/platform-tools/adb" # macOS default SDK path
rea inspect-adb-client --json
rea list-adb-devices --json
inspect-adb-client only runs adb version. It never contacts a device or starts the adb server. Our run reported "version": "37.0.1-15733141", "path_source": "environment" and "contacted_adb_server": false. If adb cannot be found, the command fails with The selected adb binary was not found and tells you to set REA_ADB_PATH to an absolute path. A relative path is rejected.
Note: rea doctor in 6.4.0 checks engines such as Ghidra and IDA, not adb. inspect-adb-client is the adb check.
list-adb-devices returns the serials exactly as adb devices -l prints them. Ours was emulator-5560. Use that string as the first argument of every command below.
Step 2: Reproduce the crash
Our test app, com.example.crashlab, has a bug in its launcher activity. It reads an optional intent extra and calls .trim() on it without a null check:
static int titleLength(String title) {
return title.trim().length(); // bug: title is null when the extra is missing
}
Install and start it:
rea install-adb-package emulator-5560 ./crashlab.apk --json
rea start-adb-app emulator-5560 com.example.crashlab --json
start-adb-app resolves the launcher activity with cmd package resolve-activity --brief and starts it with am start -n. REA reported the resolved "component": "com.example.crashlab/.MainActivity".
Why REA starts the app rather than you tapping the icon: the start becomes part of the record. The agent knows exactly what was launched and when, so a log line from a few seconds later can be tied to that action.
Step 3: Read the crash buffer
Android keeps several log buffers. main is the general app log. crash holds crash reports only, so it is the fastest place to look:
rea read-adb-logcat emulator-5560 --buffer crash --count 200 --json
This runs logcat -d -b crash -t 200 on the device: a one-time dump of the newest 200 lines. It does not stream and never clears the buffer. The result came back with "coverage": "complete", and these lines (trimmed in the middle):
--------- beginning of crash
10-14 12:11:24.913 4084 4084 E AndroidRuntime: FATAL EXCEPTION: main
10-14 12:11:24.913 4084 4084 E AndroidRuntime: Process: com.example.crashlab, PID: 4084
10-14 12:11:24.913 4084 4084 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
10-14 12:11:24.913 4084 4084 E AndroidRuntime: at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3782)
...
10-14 12:11:24.913 4084 4084 E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String java.lang.String.trim()' on a null object reference
10-14 12:11:24.913 4084 4084 E AndroidRuntime: at com.example.crashlab.MainActivity.titleLength(MainActivity.java:24)
10-14 12:11:24.913 4084 4084 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 (
4084) is the process ID.
Filter by process ID, and its limits
--pid adds logcat --pid and keeps only one process’s lines. This is how you see what the app logged right before it died:
rea read-adb-logcat emulator-5560 --buffer main --pid 4084 --count 50 --json
The last lines were the app’s own log call and the shutdown:
10-14 12:11:24.911 4084 4084 I CrashLab: started, mode=null
10-14 12:11:24.911 4084 4084 D AndroidRuntime: Shutting down VM
10-14 12:11:24.923 4084 4084 I Process : Sending signal. PID: 4084 SIG: 9
Two things to know. First, the PID only exists while the process runs. We called list-adb-processes three seconds after the start, and com.example.crashlab was already gone from the ps -A list. Take the PID from the crash line rather than from the process list. Second, a relaunch gets a new PID, so filter on the PID of the run you are looking at.
Step 4: Read a native crash
Native crashes look different. 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;
}
We triggered it through an intent extra, using REA’s typed --extra format type:key:value:
rea start-adb-activity emulator-5560 android.intent.action.MAIN \
--component com.example.crashlab/.MainActivity \
--extra string:mode:native --json
Then we read the crash buffer with a small count:
rea read-adb-logcat emulator-5560 --buffer crash --count 80 --json
The result said "coverage": "partial", and it contained only frames #16 and later of a 96-frame backtrace. The interesting part was cut off. REA keeps the newest lines when the count limits the output, and it marks the result partial whenever it returns exactly the number of lines you asked for. A native crash report is long, so ask for more:
rea read-adb-logcat emulator-5560 --buffer crash --count 1000 --json
That returned 146 lines with "coverage": "complete". The top of the native report:
F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
F DEBUG : Build fingerprint: 'google/sdk_gphone64_arm64/emu64a:14/UE1A.230829.050/12077443:userdebug/dev-keys'
F DEBUG : ABI: 'arm64'
F DEBUG : Cmdline: com.example.crashlab
F DEBUG : pid: 4154, tid: 4154, name: xample.crashlab >>> com.example.crashlab <<<
F DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0000000000000008
F DEBUG : Cause: null pointer dereference
...
F DEBUG : backtrace:
F DEBUG : #00 pc 0000000000004668 /data/app/~~Pq2iQPV_V9juLGVrCkhp8w==/com.example.crashlab-jyUevmgde3WC0F0xFRoHNg==/lib/arm64/libcrashlab.so (title_len+20)
F DEBUG : #01 pc 0000000000004644 /data/app/~~Pq2iQPV_V9juLGVrCkhp8w==/com.example.crashlab-jyUevmgde3WC0F0xFRoHNg==/lib/arm64/libcrashlab.so (Java_com_example_crashlab_MainActivity_nativeCrash+28)
(Timestamps and the two PID columns are removed here for width.)
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,title_len+20inlibcrashlab.so. Thepcvalue is an offset inside that library, which is what you need for symbolization.
Do not filter native crashes by the app’s PID. These lines are written by Android’s crash handler, crash_dump, which runs as its own process. In our log the DEBUG lines carried PID 4181, while the crashed app was 4154. A --pid 4154 filter would have hidden the whole report.
Step 5: Collect a bugreport and find the tombstone
The log buffer is a ring buffer and older crashes are pushed out. The tombstone file under /data/tombstones lasts longer, but on a normal device you cannot read that directory. We tried:
rea list-adb-directory emulator-5560 /data/tombstones --json
REA returned an unsupported_target error with the device’s reason: ls: /data/tombstones: Permission denied. A bugreport is the supported way to get those files:
rea collect-adb-bugreport emulator-5560 --output-directory "$PWD/bugreports" --json
REA runs adb bugreport into that directory and returns the path, size and SHA-256 of the zip. Ours was 8,002,276 bytes. On the emulator it took seconds, but on a real phone it can take several minutes, and REA allows up to ten. REA does not parse the zip. It stores and fingerprints the archive the device produced, so the agent can refer to that exact file later.
List what is inside:
unzip -l bugreports/bugreport-*.zip | grep -E 'tombstone|anr'
Our archive held FS/data/tombstones/tombstone_13 and tombstone_13.pb, plus older tombstones from earlier sessions on the same emulator image (some of them empty files), and two files under FS/data/anr/. Pick the right tombstone. Open the newest one and check that its pid: and Cmdline: lines match the crash you are investigating. tombstone_13 started with the same pid: 4154 and fault addr 0x0000000000000008 as the logcat report. The .pb file is the same tombstone in protobuf form. The large bugreport-*.txt at the top of the zip is the full dumpstate text, which includes the log buffers.
logcat or bugreport?
read-adb-logcat |
collect-adb-bugreport |
|
|---|---|---|
| Speed | Seconds | Seconds to minutes |
| Contents | One buffer, newest N lines | Dumpstate, dumpsys, logs, tombstones, ANR traces |
| Older crashes | Lost when the buffer wraps | Tombstones kept on the device |
| Size | Kilobytes | Megabytes |
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 6: Map frames to source
Native frames. Feed the tombstone and your unstripped libraries to the NDK’s ndk-stack:
unzip -o bugreports/bugreport-*.zip 'FS/data/tombstones/tombstone_13' -d tomb
$ANDROID_NDK_HOME/ndk-stack -sym /path/to/unstripped/libs/arm64-v8a \
-i tomb/FS/data/tombstones/tombstone_13
Point -sym at the folder that holds 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. For our test build, ndk-stack resolved frame #00 to title_len at crashlab.c:8:15, the while loop, and frame #01 to crashlab.c:16:5. For a single address you can also run llvm-addr2line -f -C -e libcrashlab.so 0x4668, which gave the same answer.
Java frames. If R8 minified your release build, frames read like a.b.c(Unknown Source). Run the stack trace through retrace from the Android command-line tools with the build’s mapping.txt before you read it. Then open the class in a decompiler to confirm what the bytecode does. JADX MCP with Claude Code shows how an agent can do that for you.
With an agent:
Use REA. On emulator-5560, start com.example.crashlab, then read the crash
buffer with a count of 1000. Tell me the exception or signal, the first frame
in my own code, and the Evidence ID of the log read. If the result is partial,
read again with a larger count before you answer.
Troubleshooting
read-adb-logcat returns nothing from the crash buffer. The app may not have crashed yet, or the buffer was cleared. REA never clears it. Run start-adb-app again and read straight away.
The result is partial. You received exactly --count lines, so older lines may have been left out. Raise --count (the maximum is 10,000).
The read fails with a resource error. The dump was larger than REA’s 4 MiB output budget for one logcat read. REA returns an error instead of a truncated result. Lower --count, or read the smaller crash buffer instead of main.
start-adb-app fails. The package has no launcher activity. Use start-adb-activity with an explicit --component.
collect-adb-bugreport fails with “did not report an archive path”. REA expects adb bugreport to print Bug report copied to .... Very old Android builds that write plain text are not supported.
You need to check the build itself. A crash on load in a native library can come from packaging, for example a missing ABI or 16 KB page alignment. The browser-based APK analyzer reads native libraries and 16 KB alignment from the APK without uploading it.
FAQ
Can REA stream logcat live?
No. read-adb-logcat reads a bounded dump each time it is called. For a live view, run adb logcat in a terminal next to the agent.
Can REA filter by tag or priority, like *:E?
Not in 6.4.0. It filters by buffer and PID only. Read the crash buffer for crashes, or read main and let the agent filter the returned lines.
Does REA parse tombstones or ANR traces?
No. It collects and fingerprints the bugreport. Reading the tombstone text, and symbolizing it with ndk-stack, is up to you or the agent.
Next steps
- Pull and inspect the APK you are debugging: Android APK Analysis with REA.
- Check what ships inside the package, including native libraries: AAB to APK and APK Unpack.
- Decompile the class from your stack trace: JADX MCP Server with Claude Code.
- Read REA’s ADB device analysis reference.