adb logcat 抓崩溃日志、adb bugreport 取 tombstone:Android 崩溃排查
本页目录
结论先行:崩溃之后马上执行 adb logcat -b crash -d。它会把 Android 的 crash 缓冲区一次性打印出来:Java/Kotlin 崩溃在里面是 FATAL EXCEPTION,原生崩溃则有信号、出错地址和调用栈。如果崩溃已经被挤出缓冲区,或者你要完整的 tombstone 文件,就执行 adb bugreport,到 zip 里的 FS/data/tombstones/ 去找。原生帧最后用 ndk-stack 加上未 strip 的 .so 对应到源码行。
下面的输出全部来自我为本文写的一个小测试 App(com.example.crashlab,一个 Java bug、一个原生 bug),运行在 Android 14 arm64 模拟器上(google_apis 镜像,userdebug 构建),adb 37.0.1,NDK r28。
第 1 步:复现崩溃,读 crash 缓冲区
测试 App 的启动 Activity 对一个通常不存在的 Intent extra 调用了 .trim():
static int titleLength(String title) {
return title.trim().length(); // bug: title is null when the extra is missing
}
先清空旧日志,启动 App,再导出 crash 缓冲区:
adb logcat -c
adb shell am start -W -n com.example.crashlab/.MainActivity
adb logcat -b crash -d
-b crash 只看存放崩溃报告的那个缓冲区,不用在 main 里大海捞针;-d 表示打印完现有内容就退出,不加的话 logcat 会一直跟踪输出。结果(中间有删节):
--------- 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)
怎么读 FATAL EXCEPTION:
- FATAL EXCEPTION 后面的
main是线程名。 主线程在onCreate里崩溃,意味着界面根本没出来。 - 最上面的异常往往只是外壳。
RuntimeException: Unable to start activity是框架在报告你的 Activity 启动失败,真正的原因在最后一个Caused by:里。 - 找第一条属于你自己包名的帧。
MainActivity.titleLength(MainActivity.java:24)就是抛异常的那一行,onCreate(MainActivity.java:20)调用了它。 - PID 就在日志行里。 第三列(
4071)是进程 ID。
按 tag 或进程过滤
想看 App 临死前自己打了什么日志,就按 tag 过滤,*:S 会屏蔽所有没点名的 tag:
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
...
也可以用 --pid 只留一个进程。PID 要从崩溃日志里取,别从进程列表里找:我执行 adb shell pidof com.example.crashlab 时它什么也没输出、返回 1,进程早就没了。adb logcat -d --pid=4071 则列出了这个进程短暂的一生,结尾是:
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
重新启动会拿到新的 PID,所以要按你正在看的那一次的 PID 过滤。原生崩溃则不要按 PID 过滤,原因见下一步。
第 2 步:读原生崩溃
App 的 JNI 库里也有一个空指针 bug:
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;
}
带 --es mode native 启动时 Activity 会调用它:
adb logcat -c
adb shell am start -W -n com.example.crashlab/.MainActivity --es mode native
adb logcat -b crash -d
122 行报告的开头(寄存器部分省略):
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_MAPERR:代码读了一个没有映射的地址。fault addr 0x8而不是0x0,这是线索。arm64 上title位于struct note偏移 8 字节处,通过空指针n读n->title就会访问地址 8。出错地址很小,几乎总是“通过空结构体指针读字段”。#00是死掉的位置。pc(0x4668)是libcrashlab.so内的偏移,符号化就靠它。#00没有函数名,是因为 APK 里的库是 strip 过的,而title_len是static函数;#01是导出的 JNI 函数,名字保留了下来。
原生崩溃不要按 App 的 PID 过滤。 只有第一行 libc 来自 App 本身(PID 4169),所有 DEBUG 行都是系统崩溃处理程序 crash_dump 写的,它是一个独立进程(这里是 4197)。用 --pid=4169 会把整份报告都过滤掉。
行数别限得太少。 adb logcat -b crash -d -t 80 只保留最新的 80 行,这次崩溃里表头之后的第一行已经是 #16 帧,信号、出错地址和 #00 到 #15 全没了。读 crash 缓冲区直接用 -d、不加 -t 就好,它本来也不大:adb logcat -g 显示环形缓冲区 2 MiB,可读内容只有 24 KiB。
第 3 步:用 adb bugreport 拿 tombstone
crash 缓冲区是环形的,旧的崩溃会被挤掉。/data/tombstones 下的 tombstone 文件保留得更久,但普通 shell 读不了这个目录:
$ adb shell ls -l /data/tombstones
ls: /data/tombstones: Permission denied
不带 Google Play 的模拟器镜像可以 adb root,之后同一条 ls 列出了 tombstone_00 到 tombstone_13,每个都配一个 .pb。正式发售的手机不允许 adb root,这时官方支持的办法就是 bugreport:
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
模拟器上用了 44 秒,真机上要做好等几分钟的准备。7.4 MB 的 zip 里有 226 个文件(解压后 67 MB),其中 41 MB 的 bugreport-*.txt 是完整的 dumpstate 文本和各个日志缓冲区。列出崩溃相关文件:
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
...
挑对 tombstone。 包里还有这个模拟器镜像以前留下的 tombstone 和 ANR 记录,有的是空文件。取最新的一个,确认 pid: 和 Cmdline: 跟你的崩溃对得上:
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
PID 和出错地址都和 logcat 里的一致。.pb 是同一份 tombstone 的 protobuf 版本。文本版比日志多得多:这份有 3,897 行,包括每个寄存器附近的内存、2,766 条内存映射、打开的文件,以及 App 主日志的末尾。
看 logcat 还是 bugreport?
adb logcat -b crash -d |
adb bugreport |
|
|---|---|---|
| 速度 | 几秒 | 我的模拟器 44 秒,真机几分钟 |
| 内容 | 环形缓冲区里还留着的崩溃记录 | dumpstate、dumpsys、全部日志缓冲区、tombstone、ANR 记录 |
| 较早的崩溃 | 缓冲区写满后就没了 | tombstone 留在设备上 |
| 体积 | KB 级 | 这次是 7.4 MB 的 zip |
| 隐私 | 你的崩溃,以及别的崩溃 | 整台设备的数据,按敏感信息对待 |
先看 crash 缓冲区。原生崩溃、发生了有一阵子的崩溃,或者需要整台设备状态时,再抓 bugreport。
第 4 步:把帧对应回源码
原生帧。 把 tombstone 和崩溃 ABI 对应的未 strip .so 所在目录交给 ndk-stack。Gradle 项目里它们在 app/build/intermediates/ 下面,APK 里的那份一般是 strip 过的。
$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
(本地路径缩写成了 ...,#01 下面两行的缩进也缩短了。)
#00 的名字回来了:title_len,crashlab.c:8:15,就是那个 while 循环。只查单个地址的话,NDK 里的 llvm-addr2line 给出同样的结果:
$ 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 也能从标准输入读 logcat 文本(adb logcat -b crash -d | ndk-stack -sym ...)。我把第 2 步保存下来的 crash 缓冲区喂给它,同样解析出了这两帧。前提是 #00 那几帧还在缓冲区里。
Java 帧。 如果 release 包经过 R8 混淆,帧会长成 a.b.c(Unknown Source)。先用 retrace 配合这次构建的 mapping.txt 还原,或者粘贴到浏览器里的 R8 / ProGuard 堆栈还原,mapping 只在本地读取。细节见 还原混淆的 Android 堆栈。
排错
crash 缓冲区是空的。 可能还没崩,可能缓冲区被清过(adb logcat -c 会清空),也可能设备重启了。复现之后马上导出;较早的原生崩溃用 adb bugreport。
原生报告从调用栈中间开始。 你加了 -t 而且数字太小。读 crash 缓冲区时去掉 -t。
tombstone 解析不出行号。 -sym 指向了 strip 过的库,或者不是同一次构建。.so 必须来自崩溃的那个构建、对应崩溃的 ABI。
还没崩就加载库失败。 这通常是打包问题,比如缺 ABI 或 16 KB 页面对齐不对。浏览器里的 APK 分析器 能直接从 APK 读出原生库及其对齐情况,不上传文件。
可选:用 REA 采集同样的数据
如果想让 AI agent 来驱动采集,REA(第三方开源命令行工具和 MCP 服务器)把这些命令包了一层,并把每次结果记成 agent 可以引用的 Evidence。6.7.0 的写法:
export REA_ADB_PATH="$HOME/Library/Android/sdk/platform-tools/adb" # 必须是绝对路径
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
我之前用同一个 App 跑过一次,观察到:
read-adb-logcat执行的是logcat -d -b crash -t <count>,保留最新的行;只要返回的行数正好等于--count,结果就标为"coverage": "partial"。--count 80时原生报告同样从#16帧开始被截断,和上面的-t 80一样;--count 1000返回 146 行,"coverage": "complete"。- 只能按缓冲区和 PID 过滤,不支持 tag 过滤或实时跟踪;一次导出超过 4 MiB 时直接报资源错误,不会悄悄截断。
collect-adb-bugreport保存 zip 并记录大小和 SHA-256,但不解析 tombstone,unzip和ndk-stack还得你自己来。- 对
/data/tombstones执行list-adb-directory,返回unsupported_target,原因是设备给出的Permission denied。