Android Native 异常捕获与解析

1,249 阅读6分钟

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 的体积也会缩小。

5MG6LA

如下可以看到 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
  • cmakemerged_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 可以得到如下

ylwbKV

然后可以进入 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 搜索结果

8xDTeQ

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 搜索结果

mO3apY

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 符号表的提取

References