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)

解读这种日志,我个人的习惯是三步走:

  1. 看异常类型:NullPointerException,说明是空指针。
  2. 看异常描述:它告诉你是在调用一个null对象的length()方法时挂的。
  3. 看调用栈:从最上面一行开始,找到我们自己代码的位置——MainActivity.java:25。这就是问题所在。
我的小技巧:有时候调用栈很长,别慌。重点关注“at com.example.”或者你自己包名的行。系统框架的调用栈一般不会出问题,除非是系统Bug。

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,我一般按这个顺序来:

  1. 先看信号和原因:确定是什么类型的错误。
  2. 再看backtrace:找到出错的函数。如果tombstone里没有符号化,你需要用addr2line或者ndk-stack工具来解析。
  3. 看寄存器:特别是x0(通常是第一个参数)、LR(返回地址)、PC(当前指令地址)。
  4. 看内存映射:确认出错地址属于哪个库的哪个段。
避坑指南:我曾经遇到过一个tombstone,backtrace指向了libc.so里的memcpy函数。一开始以为是内存拷贝越界,查了半天没结果。后来仔细看了寄存器,发现x0(目标地址)指向了一个已经释放的堆内存。原来是Use-After-Free问题,不是简单的越界。所以,别只看backtrace,寄存器信息往往藏着关键线索。

5.5 知识体系总览

下面这张图,是我自己总结的Crash分析知识体系。你可以把它当作一个检查清单,遇到Crash时,按图索骥。

Crash日志分析知识体系 Crash日志 Java Crash 异常类型/描述 调用栈定位 Native Crash 信号(Signal)分析 寄存器/内存映射 Tombstone 解读 Backtrace 符号化 寄存器值分析 内存布局检查 常用工具:addr2line / ndk-stack / gdb

这张图把Crash分析分成了三大块:Java Crash、Native Crash和Tombstone解读。每一块下面又有具体的分析要点。你调试的时候,可以对照着这个图,一步步来,不容易漏掉关键信息。

注意:Tombstone文件是系统自动生成的,但有时候会被系统清理掉。如果你遇到频繁的Native Crash,建议第一时间把tombstone文件pull出来,或者用adb shell cat /data/tombstones/tombstone_XX查看。别等到设备重启了才想起来,那时候可能已经被清掉了。

好了,Crash日志分析这块,核心就是这些。Java Crash相对简单,Native Crash需要多花点心思在信号和寄存器上。Tombstone是终极武器,信息最全,但也最复杂。多练几次,你就能从那些看似杂乱无章的十六进制数字里,读出系统想告诉你的“遗言”了。


专注资料整理