Android native so
NativeCrash 即 Native Exception, 是 C/C++ 运行过程中产生的错误,Native Exception 不同于普通的 Java Exception,普通的 logcat 无法直接还原成可阅读的堆栈,一般没有源码也无法调试。
普通的 logcat 只能看到类似这样的信息:
A/libc: Fatal signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0x72a275a838 in tid 19185 (xcrash.sample), pid 19185 (xcrash.sample)
很难从这些信息分析具体原因,这时就需要从 so 相关信息来看。
分析 so 组成
一个完整的 so 由 C/c++ 代码加一些 debug 信息组成,这些 debug 信息会记录 so 中所有方法的对照表,就是 方法名 和其 偏移地址 的对应表,也叫做符号表,这种 so 也叫做未 strip (没有去掉符号表的),通常体积会比较大。
而 release 的 so 都会经过 strip 操作,strip 之后的 so 中的 debug 信息会被剥离,整个 so 的体积也会缩小。

如下可以看到 strip 之前和之后的大小对比:2.9MB (strip) vs 23.6MB (未 strip)
$ ls -l
-rw-r--r-- 1 weiwei staff 2867944 10 8 10:30 libtest-s.so
-rw-r--r-- 1 weiwei staff 23614624 10 8 10:30 libtest.so
可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析,没有堆栈信息就很难解决发生的异常。
所以这些 debug 信息很重要,是我们分析 Native Exception 问题的关键信息,那么我们在编译 so 时候务必保留一份未被 strip 的 so 或者剥离后的符号表信息,以供后面问题分析,并且每次编译的 so 都需要保存,一旦产生代码修改重新编译,那么修改前后的符号表信息会无法对应,也无法进行分析。
查看 so 信息
在 macOS/Linux 下可以使用 file 命令查看 so 文件信息:stripped 代表是没有 debug 信息的 so, with debug_info, not stripped 代表包含 debug 信息的 so。
$ file libtest.so
libtest.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9a28e26533f985cf3ae8733ccd0c2cae8cc49fb3, with debug_info, not stripped
$ file libtest-s.so
libtest-s.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=9a28e26533f985cf3ae8733ccd0c2cae8cc49fb3, stripped
下面看下如何获取两种状态下的 so。
获取 so 文件
无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,下面文件树列表是 Cmake 编译 so 产生的两种对应的 so。
我们执行 release 任务,例如:
$ ./gradlew libTest:assembleRelease
执行完成后可以在 libTest/build/intermediates 目录下找到两种对应的 so: (下面输出结果对应的是 AGP 7.0+)
# weiwei in ~/NativeDemo/libTest/build/intermediates
$ tree -L 2
├── cmake
│ └── release
│ └── obj
│ ├── arm64-v8a
│ │ ├── libtest.so
│ └── armeabi-v7a
│ ├── libtest.so
├── merged_native_libs
│ └── release
│ └── out
│ └── lib
│ ├── arm64-v8a
│ │ ├── libtest.so
│ └── armeabi-v7a
│ ├── libtest.so
├── stripped_native_libs
│ └── release
│ └── out
│ └── lib
│ ├── arm64-v8a
│ │ ├── libtest.so
│ └── armeabi-v7a
│ ├── libtest.so
cmake和merged_native_libs目录下的 so 文件都是未做 strip 处理的,我们需要保存好这个文件。stripped_native_libs目录下则是经过 strip 处理的 so 文件,会被打到最终的 apk 当中。
保存未做 strip 的 so 文件,最好是在 ci 构建新的 Library 或者 Application 时自动将其保存的特定位置以便于后续分析使用。
另外也可以通过 Android sdk 提供的工具 aarch64-linux-android-strip 手动进行 strip,工具位于 $HOME/Library/Android/sdk/ndk/21.4.7075529/toolchains 目录下 (自行选择可用的 NDK 版本)
在 toolchains 目录下通过 fzf 搜索 -android-strip 可以得到如下

然后可以进入 aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/ 目录,使用如下命令可以直接将 debug 的 so 进行 strip 处理
aarch64-linux-android-strip --strip-all libtest.so
使用 Cmake 进行编译的时候,可以增加如下命令,可以直接编译出 strip 的 so
#set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -s")
#set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -s")
使用 mk 文件进行编译的时候,可以增加如下命令,也可以直接编译出 strip 的 so
-fvisibility=hidden
异常捕获解析
可以参考 Android NativeCrash 捕获与解析 使用 logcat, DropBox, Breakpad 都可以获取到 Native exception 日志,但都存在不少问题。
- logcat 的缺点是只能测试调试过程中使用,正式环境无法使用。
- DropBox 的缺点是只适用于系统应用,普通应用使用困难。
- Breakpad 是 Google 开源的,非常成熟,但使用比较麻烦,而且代码量很大。
iqiyi/xCrash 是爱奇艺开源的捕获 Java exception, Native exception, ANR 的库,不需要任何 root 权限或系统权限,很适合用来捕获 Native exception,具体使用和原理可以参考项目中的文档。
当异常发生时, xCrash 捕获异常并保存到初始化设置的目录,Native exception 示例如下(完成可以参考 native crash (arm64-v8a))
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Tombstone maker: 'xCrash 2.4.6'
Crash type: 'native'
Start time: '2019-10-12T03:18:02.523+0800'
Crash time: '2019-10-12T03:18:21.127+0800'
App ID: 'xcrash.sample'
App version: '1.2.3-beta456-patch789'
Rooted: 'No'
API level: '29'
OS version: '10'
Kernel version: 'Linux version 3.18.137-g382d7256ce44 #1 SMP PREEMPT Fri Jul 12 06:00:07 UTC 2019 (aarch64)'
ABI list: 'arm64-v8a,armeabi-v7a,armeabi'
Manufacturer: 'Google'
Brand: 'google'
Model: 'Pixel'
Build fingerprint: 'google/sailfish/sailfish:10/QP1A.190711.020/5800535:user/release-keys'
ABI: 'arm64'
pid: 20501, tid: 20501, name: xcrash.sample >>> xcrash.sample <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
x0 0000000000000003 x1 0000000000000000 x2 000000751128fd60 x3 0000007511290020
x4 000000751128fd60 x5 00000075a26c1708 x6 000000751128fd50 x7 00000075200a59dc
x8 0000000000000000 x9 79fc7e30c0ff4d9e x10 00000000000003e8 x11 0000000000000000
x12 0000000000004100 x13 0000000000000001 x14 0000000000080100 x15 0000000000000000
x16 00000074b9be4d20 x17 00000074b9bcc86c x18 00000075a57fa000 x19 00000075a4f52000
x20 0000000000000000 x21 00000075a4f52000 x22 0000007fe0ef23a0 x23 00000074bb1b62fe
x24 0000000000000004 x25 00000075a5107020 x26 00000075a4f520b0 x27 0000000000000001
x28 0000007fe0ef2130 x29 0000007fe0ef2090
sp 0000007fe0ef2070 lr 00000074b9bcc8cc pc 00000074b9bcc884
backtrace:
#00 pc 000000000000b884 /data/app/xcrash.sample-WeCpVYjROKKgYtuzbHflHg==/lib/arm64/libxcrash.so (xc_test_call_4+24)
#01 pc 000000000000b8c8 /data/app/xcrash.sample-WeCpVYjROKKgYtuzbHflHg==/lib/arm64/libxcrash.so (xc_test_call_3+24)
#02 pc 000000000000b8f8 /data/app/xcrash.sample-WeCpVYjROKKgYtuzbHflHg==/lib/arm64/libxcrash.so (xc_test_call_2+24)
#03 pc 000000000000b920 /data/app/xcrash.sample-WeCpVYjROKKgYtuzbHflHg==/lib/arm64/libxcrash.so (xc_test_call_1+16)
#04 pc 000000000000b9b4 /data/app/xcrash.sample-WeCpVYjROKKgYtuzbHflHg==/lib/arm64/libxcrash.so (xc_test_crash+124)
#05 pc 000000000013f350 /apex/com.android.runtime/lib64/libart.so (art_quick_generic_jni_trampoline+144)
#06 pc 00000000001365b8 /apex/com.android.runtime/lib64/libart.so (art_quick_invoke_static_stub+568)
...
从捕获到的日志可以看到非常完整的信息:Crash 类型,启动时间,崩溃时间,各种版本信息和设备信息,崩溃的进程和线程,
从 signal 可以看到崩溃的原因是 fault addr 0x0
具体崩溃的堆栈则是在 backtrace 中,我们可以通过 addr2line 工具配合编译生成的带符号表的 so 文件,找到代码中崩溃的位置
addr2line 工具位于 $HOME/Library/Android/sdk/ndk/selectVersion/toolchains , 我们在这个目录通过 fzf 搜索 -addr2line :
~/Library/Android/sdk/ndk/21.4.7075529/toolchains 搜索结果

aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
x86_64-4.9/prebuilt/darwin-x86_64/bin/x86_64-linux-android-addr2line
x86-4.9/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line
llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
llvm/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line
llvm/prebuilt/darwin-x86_64/bin/x86_64-linux-android-addr2line
llvm/prebuilt/darwin-x86_64/bin/i686-linux-android-addr2line
llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line
~/Library/Android/sdk/ndk/23.1.7779620/toolchains 搜索结果

llvm/prebuilt/darwin-x86_64/bin/llvm-addr2line
可以看出不能 ndk 版本还是不一样,高版本只有一个 llvm-addr2line, 低版本则存在多个 *-linux-android-addr2line
具体使用方法如下:
通过日志找到崩溃的 so 文件 (未 strip), 然后执行下面指令,传入 so 文件路径和 backtrace 的堆栈信息
addr2line -C -f -e 未strip的so路径 backtrace堆栈信息
- -e 表示打印错误地址的对应路径及行数
- -C -f 表示打印错误行数所在的函数名称
- backtrace 堆栈信息可以写多个,使用空格隔开
- addr2line 程序可以使用完整路径或者切换到所在路径使用
测试 release 的 arm64 so 示例:
# weiwei in ~/Library/Android/sdk/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin
aarch64-linux-android-addr2line -e xcrash_lib/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a/libxcrash.so 000000000000b884
xCrash/xcrash_lib/src/main/cpp/xcrash/xc_test.c:65
# 实测 ndk/21.4.7075529: aarch64-linux-android-addr2line, x86_64-linux-android-addr2line, i686-linux-android-addr2line 都是可用的,arm-linux-androideabi-addr2line, llvm-addr2line 则不行
# weiwei in ~/Library/Android/sdk/ndk/23.1.7779620/toolchains/llvm/prebuilt/darwin-x86_64/bin
llvm-addr2line -e xcrash_lib/build/intermediates/merged_native_libs/release/out/lib/arm64-v8a/libxcrash.so 000000000000b884
xCrash/xcrash_lib/src/main/cpp/xcrash/xc_test.c:65
找到异常发生的位置之后就可以从源代码去分析具体异常原因了,如果不方便保存带 debug 信息的 so 文件,可以单独提取出符号表,符号表提取和使用参考:Android NativeCrash 捕获与解析# 三、so 符号表的提取