第4章:Android符号化工具链:addr2line、ndk-stack、llvm-symbolizer 实战

说到Android崩溃日志的符号化,很多新手第一反应就是「好麻烦」。其实工具就那么几个,用熟了就跟吃饭一样自然。今天我就把这三大工具——addr2line、ndk-stack、llvm-symbolizer,掰开揉碎了讲清楚。

4.1 为什么需要符号化?

你想想看,Native崩溃日志里全是这样的地址:

#00 pc 0000000000012345 /system/lib64/libc.so
#01 pc 0000000000056789 /data/app/com.example/lib/arm64/libnative.so

这谁看得懂?符号化就是把「0000000000012345」变成「Java_com_example_MainActivity_nativeCrash+0x123」。说白了,就是把机器地址翻译成人类能读的函数名和行号。

我个人习惯,拿到崩溃日志的第一件事就是符号化。不符号化,你连问题在哪都不知道,更别提修了。

4.2 addr2line:最基础的符号化工具

addr2line是GNU binutils家族的一员。它简单、直接,适合处理单个地址的查询。

4.2.1 基本用法

# 基本命令格式
arm-linux-androideabi-addr2line -f -C -e libnative.so 0x12345

# 参数说明
-f  # 显示函数名
-C  # 对C++函数名进行demangle
-e  # 指定so文件路径

我在项目中遇到过一个问题:用addr2line查地址,结果返回「??:0」。后来发现是so文件没带符号表。嗯,这里要注意——release包通常会被strip掉符号,你得用带符号的so文件。

4.2.2 批量处理地址

如果崩溃日志里有多个地址要查,可以这样:

# 把地址写进文件,一行一个
echo "0x12345" > addresses.txt
echo "0x67890" >> addresses.txt

# 批量查询
arm-linux-androideabi-addr2line -f -C -e libnative.so < addresses.txt

不过说实话,addr2line处理大量地址时效率不高。我一般只用来查一两个关键地址。

注意:addr2line需要和so文件架构匹配。arm64的so要用aarch64-linux-android-addr2line,32位的用arm-linux-androideabi-addr2line。搞错了会报错或者出假结果。

4.3 ndk-stack:Google官方的符号化利器

ndk-stack是Android NDK自带的工具。它最大的优势是能自动解析整个崩溃日志,不需要你手动一个个查地址。

4.3.1 基本用法

# 从文件读取崩溃日志
ndk-stack -sym /path/to/symbols -dump crash.log

# 从管道输入
adb logcat | ndk-stack -sym /path/to/symbols

参数说明:

  • -sym:指定符号文件目录(就是带符号的so文件所在目录)
  • -dump:从文件读取崩溃日志

4.3.2 实战案例

我记得有一次线上崩溃,日志里全是libflutter.so的地址。用addr2line一个个查太慢了。我直接用ndk-stack一把梭:

# 假设符号文件在 ./symbols/arm64-v8a/ 目录下
ndk-stack -sym ./symbols/arm64-v8a/ -dump crash.log

输出结果:

********** Crash dump: **********
Build fingerprint: 'google/...'
pid: 12345, tid: 12346, name: Thread-1
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
    r0 00000000  r1 00000001  r2 00000002  r3 00000003
    ...
    #00 pc 0000000000123456  /data/app/.../libflutter.so
    (Dart_IsSameError+0x12)
    #01 pc 0000000000127890  /data/app/.../libflutter.so
    (Dart_HandleError+0x24)

你看,函数名和偏移量都出来了。Dart_IsSameError,一看就知道是Flutter引擎里的问题。

小技巧:ndk-stack会自动递归查找符号目录下的所有so文件。你只需要把带符号的so文件按架构放在对应目录下就行。

4.4 llvm-symbolizer:LLVM家族的符号化工具

llvm-symbolizer是LLVM项目的一部分。它比addr2line更强大,支持更多格式,而且速度更快。

4.4.1 基本用法

# 基本命令
llvm-symbolizer -obj=libnative.so -e=libnative.so 0x12345

# 常用参数
-obj   # 指定so文件
-e     # 指定可执行文件
-f     # 显示函数名
-C     # demangle C++函数名
-inl   # 显示内联函数信息

4.4.2 为什么推荐llvm-symbolizer?

我个人更偏爱llvm-symbolizer,原因有三:

  1. 支持更多格式:除了ELF,还支持Mach-O、COFF等
  2. 内联函数处理:能显示内联函数的调用链
  3. 速度更快:特别是处理大so文件时,比addr2line快不少

我曾经用addr2line查一个100MB的so文件,等了快10秒。换成llvm-symbolizer,2秒就出结果了。

4.4.3 实战:处理内联函数

# 显示内联函数信息
llvm-symbolizer -obj=libnative.so -e=libnative.so -f -C -inl 0x12345

# 输出示例
inline_1
/Users/me/project/src/native.cpp:123:0
inline_2
/Users/me/project/src/native.cpp:120:0
main_function
/Users/me/project/src/native.cpp:100:0

你看,它把内联函数的调用链都列出来了。这在调试性能优化时特别有用。

4.5 三大工具对比

特性 addr2line ndk-stack llvm-symbolizer
批量处理 支持(需脚本) 自动解析整个日志 支持(管道输入)
内联函数 不支持 不支持 支持
速度 中等
依赖 GNU binutils Android NDK LLVM
适用场景 查单个地址 解析完整崩溃日志 需要内联信息

4.6 实战:完整符号化流程

好了,理论讲完了。咱们来走一遍完整的流程。假设你拿到了一个崩溃日志:

# 崩溃日志片段
#00 pc 0000000000012345  /data/app/.../libnative.so
#01 pc 0000000000056789  /data/app/.../libnative.so
#02 pc 000000000009abcd  /data/app/.../libnative.so

第一步,确认so文件架构:

file libnative.so
# 输出: ELF 64-bit LSB shared object, ARM aarch64

第二步,选择工具。我一般这样选:

  • 只有一两个地址 → addr2line
  • 完整崩溃日志 → ndk-stack
  • 需要内联信息 → llvm-symbolizer

第三步,执行符号化:

# 用ndk-stack解析整个日志
ndk-stack -sym ./symbols/arm64-v8a/ -dump crash.log

# 或者用llvm-symbolizer查单个地址
llvm-symbolizer -obj=libnative.so -e=libnative.so -f -C 0x12345

第四步,分析结果。看到函数名后,去源码里定位问题。

核心要点:符号化的前提是必须有带符号的so文件。release包通常会被strip,所以一定要在构建时保留符号文件。我建议把符号文件归档到专门的服务器上,方便后续排查。

4.7 避坑指南

做符号化这么多年,我踩过不少坑。分享几个常见的:

  • so文件版本不匹配:我曾经用旧版本的so去符号化新版本的崩溃日志,结果全是乱码。一定要确保so文件和崩溃日志来自同一个构建版本。
  • 架构搞混:arm64的so用arm的addr2line,会报错。用file命令确认架构再选工具。
  • 符号被strip:release包默认会strip符号。构建时加上LOCAL_STRIP_MODULE := false或者用obj/local/目录下的未strip版本。
  • 地址偏移:有些崩溃日志里的地址是相对地址,有些是绝对地址。addr2line和llvm-symbolizer默认处理相对地址,ndk-stack会自动处理。

嗯,差不多就这些了。工具是死的,人是活的。多练几次,你就能找到最适合自己的那一套流程。