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
...
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)
...
从捕获到的日志可以看到非常完整的信息: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 符号表的提取