10、堆栈分析工具:addr2line、objdump、ndk-stack、Java堆栈解析
堆栈分析,说白了就是看程序死在哪里、怎么死的。
我刚开始做Android系统调试那会儿,遇到Native崩溃就头大。满屏的十六进制地址,根本不知道对应哪行代码。后来学会了这几个工具,才算是真正开了窍。
10.1 addr2line:把地址翻译成代码行
这个工具我几乎每天都会用到。它的作用很简单:给你一个内存地址,它告诉你这个地址对应哪个源文件的哪一行。
核心用法:
# 基本用法
addr2line -e libxxx.so 0x1234
# 带偏移量
addr2line -e libxxx.so 0x1234+0x100
# 显示函数名
addr2line -e libxxx.so -f 0x1234
我个人习惯把addr2line和dmesg配合使用。系统崩溃时,dmesg里会打印出一串调用栈,里面全是地址。复制出来,一行一行翻译,很快就能定位到问题。
小技巧:如果地址翻译出来是"??:0",说明你的so文件没有带符号表。编译时加上-g选项,或者用带symbols的版本。
10.2 objdump:反汇编利器
addr2line能告诉你代码在哪一行,但有时候你还需要知道那一行到底编译成了什么指令。这时候就得用objdump。
我在项目中遇到过一个问题:某个函数偶尔会崩溃,但addr2line指向的代码行看起来完全正常。反汇编一看,发现是编译器优化导致指令重排,把空指针检查给优化掉了。
# 反汇编整个so
objdump -d libxxx.so
# 只看某个函数
objdump -d libxxx.so | grep -A 50 "function_name"
# 显示源码和汇编对照
objdump -d -S libxxx.so
注意:objdump的输出非常详细,动辄几万行。我建议先用addr2line定位到具体函数,再用objdump只看那个函数的反汇编。不然你会被信息淹没。
10.3 ndk-stack:自动解析Native崩溃
这个工具是Google专门为Android NDK开发的。它能把logcat里的Native崩溃栈,自动翻译成源码行号。
嗯,这里要注意:ndk-stack需要你提供带符号表的so文件。如果你用的是strip过的release版本,它也没辙。
# 基本用法
adb logcat | ndk-stack -sym /path/to/symbols
# 从文件读取
ndk-stack -sym /path/to/symbols -dump crash.txt
我曾经在一个项目里,用ndk-stack配合脚本,实现了崩溃栈的自动解析和邮件通知。每次CI构建后,如果测试出现Native崩溃,系统会自动解析并发送报告。省了不少人工排查的时间。
10.4 Java堆栈解析
Java层的堆栈相对友好,因为Android系统已经帮我们做了大部分翻译工作。但有些场景还是需要手动处理。
10.4.1 常见Java堆栈格式
java.lang.NullPointerException
at com.example.MyClass.myMethod(MyClass.java:42)
at com.example.MyClass.onClick(MyClass.java:100)
at android.view.View.performClick(View.java:7441)
这种格式一看就懂:异常类型、发生位置、调用链。但有时候你会看到这样的堆栈:
android.os.DeadObjectException
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:540)
看到"Native Method"就要注意了,这说明底层Binder通信出了问题。这时候光看Java堆栈是不够的,得结合Native层的日志一起分析。
10.4.2 混淆堆栈的反混淆
如果你用的是ProGuard或R8混淆过的APK,堆栈里的类名和方法名都是a、b、c这样的字母。这时候需要用到retrace工具:
# 使用retrace
retrace.sh mapping.txt crash.txt
# 或者用ProGuard自带的工具
java -jar proguard.jar retrace mapping.txt crash.txt
我的习惯:每次发布release版本时,我都会把mapping.txt文件归档保存,按版本号命名。这样以后出问题,随时可以反混淆。别等到出事了才找mapping文件,那时候可能已经找不到了。
10.5 工具对比与选择
| 工具 | 适用场景 | 输入 | 输出 |
|---|---|---|---|
| addr2line | 单个地址翻译 | 地址、so文件 | 文件名、行号 |
| objdump | 反汇编、指令级分析 | so文件 | 汇编代码 |
| ndk-stack | 批量解析Native崩溃 | logcat输出 | 源码行号堆栈 |
| retrace | 反混淆Java堆栈 | mapping文件 | 原始类名/方法名 |
10.6 实战流程
我一般按这个流程来处理崩溃问题:
- 看日志:先判断是Java崩溃还是Native崩溃
- Java崩溃:直接看堆栈,必要时用retrace反混淆
- Native崩溃:用ndk-stack批量解析,或者手动用addr2line翻译关键地址
- 深挖原因:如果行号不够,用objdump看反汇编,确认是不是编译器优化或内存踩踏
说白了,这些工具就是你的眼睛。没有它们,你面对的就是一堆十六进制数字,啥也看不出来。有了它们,你才能看到代码的真实执行路径。
避坑指南:我曾经遇到过一个情况,addr2line翻译出来的行号,跟实际代码对不上。后来发现是so文件版本不对。记住:一定要用崩溃时那个版本的so文件来解析,否则地址偏移量对不上,翻译结果就是错的。
这些工具各有侧重,但目标一致:把晦涩的二进制地址,变成你能读懂的代码位置。我个人建议,先把addr2line和ndk-stack用熟,这两个是日常排查Native崩溃最常用的。Java堆栈解析相对简单,但反混淆那步容易忽略,记得保存好mapping文件。