rea-reverse

adb logcat 抓崩溃日志、adb bugreport 取 tombstone:Android 崩溃排查

基于 REA 6.7.0发布者 rea-reverse更新于 约 6 分钟读完
本页目录

结论先行:崩溃之后马上执行 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。