5、Crash日志分析:Java Crash、Native Crash、信号处理、tombstone解读
Crash,说白了就是程序挂了。做Android系统调试,每天打交道最多的就是这玩意儿。我刚开始接触这一块的时候,看到满屏的十六进制地址和寄存器值,说实话,头都是大的。但后来你会发现,Crash日志其实是系统留给你的“遗书”,读懂了它,问题就解决了一半。
这一章,我带你系统地梳理一下Android世界里几种常见的Crash类型,以及我们该怎么去“验尸”。
5.1 Java Crash:最熟悉的陌生人
Java Crash,也就是我们常说的Java层异常。比如NullPointerException、IndexOutOfBoundsException这些。它的日志特征非常明显——你会看到一大串的调用栈,从异常发生点一直追溯到最顶层的入口。
举个例子,一个典型的Java Crash日志长这样:
--------- beginning of crash
E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.example.myapp, PID: 12345
E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke virtual method 'int java.lang.String.length()' on a null object reference
E AndroidRuntime: at com.example.myapp.MainActivity.onClick(MainActivity.java:25)
E AndroidRuntime: at android.view.View.performClick(View.java:7125)
E AndroidRuntime: at android.view.View$PerformClick.run(View.java:28257)
E AndroidRuntime: at android.os.Handler.handleCallback(Handler.java:938)
E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:99)
E AndroidRuntime: at android.os.Looper.loop(Looper.java:246)
E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8506)
E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:602)
E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1130)
解读这种日志,我个人的习惯是三步走:
- 看异常类型:NullPointerException,说明是空指针。
- 看异常描述:它告诉你是在调用一个null对象的length()方法时挂的。
- 看调用栈:从最上面一行开始,找到我们自己代码的位置——
MainActivity.java:25。这就是问题所在。
5.2 Native Crash:C/C++世界的“硬伤”
Native Crash比Java Crash要难搞一些。它发生在C/C++层,比如JNI代码、系统服务、或者底层库。日志里没有Java的异常类型,取而代之的是信号(Signal)和寄存器信息。
一个典型的Native Crash日志开头是这样的:
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/redfin/redfin:12/SP1A.210812.016/12345678:user/release-keys'
Revision: '0'
ABI: 'arm64'
Timestamp: 2024-01-15 10:30:45.123456789+0800
pid: 12345, tid: 12346, name: Binder:12345_1 >>> com.example.myapp <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
x0 0000000000000000 x1 0000007f8b4c2000 x2 0000000000000001
x3 0000000000000000 x4 0000000000000000 x5 0000000000000000
x6 0000000000000000 x7 0000000000000000
x8 0000000000000000 x9 0000000000000000 x10 0000000000000000
...
这里有几个关键信息:
- signal 11 (SIGSEGV):这是最常见的信号,表示段错误。说白了就是访问了不该访问的内存地址。
- fault addr 0x0:出错的地址是0x0,也就是空指针。这跟Java的NullPointerException是一个道理,只不过发生在Native层。
- Cause: null pointer dereference:系统直接告诉你原因了,省得你猜。
我记得有一次,我在调试一个多媒体播放器的Native Crash,信号是SIGFPE(浮点异常)。当时百思不得其解,明明没有浮点运算啊。后来才发现,是一个除数为0的整数除法,在ARM64架构下也会触发SIGFPE。嗯,这个坑我踩过。
5.3 信号处理:系统在跟你“喊话”
信号(Signal)是Linux内核用来通知进程发生了某种事件的机制。在Android里,常见的信号有:
| 信号 | 数值 | 含义 | 常见原因 |
|---|---|---|---|
| SIGSEGV | 11 | 段错误 | 访问非法内存、空指针解引用 |
| SIGABRT | 6 | 异常终止 | 主动调用abort()、断言失败 |
| SIGFPE | 8 | 浮点异常 | 除0、浮点溢出 |
| SIGBUS | 7 | 总线错误 | 未对齐的内存访问 |
| SIGILL | 4 | 非法指令 | CPU执行了无效指令、代码损坏 |
你想想看,系统为什么会发信号?说白了就是它发现进程干了不该干的事,或者进程自己活不下去了(比如调了abort())。
在调试时,看到信号号,你心里就要有个大概的方向。比如SIGSEGV,十有八九是指针问题;SIGABRT,多半是某个条件检查没通过,主动“自杀”了。
5.4 Tombstone解读:最完整的“尸检报告”
当Native Crash发生时,系统会生成一个tombstone文件。这玩意儿比logcat里的信息详细得多。它通常保存在/data/tombstones/目录下,文件名是tombstone_XX。
一个完整的tombstone包含:
- 进程和线程信息:pid、tid、进程名、线程名。
- 信号信息:信号类型、错误码、出错地址。
- 寄存器值:所有通用寄存器和特殊寄存器的值。
- 调用栈:完整的backtrace,包括函数名和偏移量。
- 内存映射:进程的整个地址空间布局,哪些库加载在什么位置。
- 堆栈内容:出错点附近的栈内存dump。
解读tombstone,我一般按这个顺序来:
- 先看信号和原因:确定是什么类型的错误。
- 再看backtrace:找到出错的函数。如果tombstone里没有符号化,你需要用
addr2line或者ndk-stack工具来解析。 - 看寄存器:特别是x0(通常是第一个参数)、LR(返回地址)、PC(当前指令地址)。
- 看内存映射:确认出错地址属于哪个库的哪个段。
5.5 知识体系总览
下面这张图,是我自己总结的Crash分析知识体系。你可以把它当作一个检查清单,遇到Crash时,按图索骥。
这张图把Crash分析分成了三大块:Java Crash、Native Crash和Tombstone解读。每一块下面又有具体的分析要点。你调试的时候,可以对照着这个图,一步步来,不容易漏掉关键信息。
adb shell cat /data/tombstones/tombstone_XX查看。别等到设备重启了才想起来,那时候可能已经被清掉了。
好了,Crash日志分析这块,核心就是这些。Java Crash相对简单,Native Crash需要多花点心思在信号和寄存器上。Tombstone是终极武器,信息最全,但也最复杂。多练几次,你就能从那些看似杂乱无章的十六进制数字里,读出系统想告诉你的“遗言”了。