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 实战流程

我一般按这个流程来处理崩溃问题:

  1. 看日志:先判断是Java崩溃还是Native崩溃
  2. Java崩溃:直接看堆栈,必要时用retrace反混淆
  3. Native崩溃:用ndk-stack批量解析,或者手动用addr2line翻译关键地址
  4. 深挖原因:如果行号不够,用objdump看反汇编,确认是不是编译器优化或内存踩踏

说白了,这些工具就是你的眼睛。没有它们,你面对的就是一堆十六进制数字,啥也看不出来。有了它们,你才能看到代码的真实执行路径。

避坑指南:我曾经遇到过一个情况,addr2line翻译出来的行号,跟实际代码对不上。后来发现是so文件版本不对。记住:一定要用崩溃时那个版本的so文件来解析,否则地址偏移量对不上,翻译结果就是错的。

堆栈分析工具知识体系 应用崩溃 Java崩溃 Native崩溃 retrace / proguard addr2line objdump ndk-stack 源码行号 / 函数名 / 汇编指令 定位崩溃根因

这些工具各有侧重,但目标一致:把晦涩的二进制地址,变成你能读懂的代码位置。我个人建议,先把addr2line和ndk-stack用熟,这两个是日常排查Native崩溃最常用的。Java堆栈解析相对简单,但反混淆那步容易忽略,记得保存好mapping文件。

专注资料整理