native异常详解

/ 0评 / 1

前言

native的异常不同于Java异常,往往堆栈是一堆的地址,如下所示

file

本文主要介绍如何通过一系列的工具去还原native异常堆栈。

获取崩溃日志

分析Native Crash最直接的方式是查看logcat日志,一般情况下,只要APP没有自己实现信号捕获机制(比如使用了Bugly插件或者google breakpad),就不会影响到Runtime正常打印调用栈。我们通常只需要执行

$ adb logcat|grep DEBUG

如果crash已经发生了,如果没有清空logcat的缓存,也可以如下挽救一下,执行如下命令,最新的崩溃在最后面

$ adb logcat --buffer=crash

当然,如果你的手机有root权限,可以pull出tombstone文件,目录在/data/tombstones/。tombstone文件是在出现Native Crash时的崩溃转储文件,一般最多保存10个文件,如果有新的Crash则会覆盖掉旧的文件。

file

如果没有root权限,可以使用bugreport命令导出日志文件,里面也可能有墓碑文件

$ adb bugreport E:\Reports\MyBugReports

更多用法可以查看官方文档

file

native异常格式

要想准确的定位异常发生原因,那么首先得看懂crash的日志

signal 5 (SIGTRAP), code 1 (TRAP_BRKPT), fault addr 0x79ce10f8e8

要想看懂异常堆栈,其实只需要关注第二点以及第四点

异常堆栈解析

#00 pc 000000000000e8e8  /....../libnesample.so (Java_com_example_nesample_MainActivity_stringFromJNI+28) (BuildId: 55b26db218ce4b27f098e20358fe013adec533c1)

1、#00:栈帧号
2、000000000000e8e8:pc地址,16进制
3、/....../libnesample.so:异常出现的so
4、Java_com_example_nesample_MainActivity_stringFromJNI:方法名(如果为_Z开头的乱码,可以使用C++fit工具还原)
5、+28:函数内部偏移地址符号偏移量(以字节为单位):28
6、BuildId:so文件的BuildId

我们可以通过函数起始的pc地址加上函数内部偏移地址符号偏移量,也可以算出异常堆栈的地址,具体查看末尾的nm工具

signal信号量

与 Java 平台不同,C/C++ 没有一个通用的异常处理接口,在 C 层,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式,linux 把这些中断处理,统一为信号量,每一种异常都有一个对应的信号,可以注册回调函数进行处理需要关注的信号量。
所有的信号量都定义在\文件中:

#define SIGHUP 1    // 终端连接结束时发出(不管正常或非正常)
#define SIGINT 2    // 程序终止(例如Ctrl-C)
#define SIGQUIT 3   // 程序退出(Ctrl-\)
#define SIGILL 4    // 执行了非法指令,或者试图执行数据段,堆栈溢出
#define SIGTRAP 5   // 断点时产生,由debugger使用
#define SIGABRT 6   // 调用abort函数生成的信号,表示程序异常
#define SIGIOT 6    // 同上,更全,IO异常也会发出
#define SIGBUS 7    // 非法地址,包括内存地址对齐出错,比如访问一个4字节的整数, 但其地址不是4的倍数
#define SIGFPE 8    // 计算错误,比如除0、溢出
#define SIGKILL 9   // 强制结束程序,具有最高优先级,本信号不能被阻塞、处理和忽略
#define SIGUSR1 10  // 未使用,保留
#define SIGSEGV 11  // 非法内存操作,与SIGBUS不同,他是对合法地址的非法访问,比如访问没有读权限的内存,向没有写权限的地址写数据
#define SIGUSR2 12  // 未使用,保留
#define SIGPIPE 13  // 管道破裂,通常在进程间通信产生
#define SIGALRM 14  // 定时信号,
#define SIGTERM 15  // 结束程序,类似温和的SIGKILL,可被阻塞和处理。通常程序如果终止不了,才会尝试SIGKILL
#define SIGSTKFLT 16  // 协处理器堆栈错误
#define SIGCHLD 17  // 子进程结束时, 父进程会收到这个信号。
#define SIGCONT 18  // 让一个停止的进程继续执行
#define SIGSTOP 19  // 停止进程,本信号不能被阻塞,处理或忽略
#define SIGTSTP 20  // 停止进程,但该信号可以被处理和忽略
#define SIGTTIN 21  // 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号
#define SIGTTOU 22  // 类似于SIGTTIN, 但在写终端时收到
#define SIGURG 23   // 有紧急数据或out-of-band数据到达socket时产生
#define SIGXCPU 24  // 超过CPU时间资源限制时发出
#define SIGXFSZ 25  // 当进程企图扩大文件以至于超过文件大小资源限制
#define SIGVTALRM 26    // 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
#define SIGPROF 27  // 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间
#define SIGWINCH 28 // 窗口大小改变时发出
#define SIGIO 29    // 文件描述符准备就绪, 可以开始进行输入/输出操作
#define SIGPOLL SIGIO   // 同上,别称
#define SIGPWR 30   // 电源异常
#define SIGSYS 31   // 非法的系统调用

so文件解析

一个完整的 so 由C代码加一些 debug 信息组成,这些debug信息会记录 so 中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表,这种 so 也叫做未 strip 的,通常体积会比较大。

通常release的 so 都是需要经过一个strip操作的,这样strip之后的 so 中的debug信息会被剥离,整个 so 的体积也会缩小。

如下可以看到strip之前和之后的大小对比。

可以简单将这个debug信息理解为Java代码混淆中的mapping文件,只有拥有这个mapping文件才能进行堆栈分析。

如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。

所以,这些debug信息尤为重要,是我们分析NE问题的关键信息,那么我们在编译 so 时候务必保留一份未被strip的so 或者剥离后的符号表信息,以供后面问题分析,并且每次编译的so 都需要保存,一旦产生代码修改重新编译,那么修改前后的符号表信息会无法对应,也无法进行分析。

so文件与crash日志对应

当native异常发生的是时候查看异常堆栈,里面有一个BuildID,如下所示

通过这个就能知道崩溃对应的so。

通过Linux的flie命令即可知道so的BuildID,如下所示

带debug信息的so

$ file libnesample.so
libnesample.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=55b26db218ce4b27f098e20358fe013adec533c1, with debug_info, not stripped

strip后的so

$ file libnesample.so
libnesample.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[sha1]=55b26db218ce4b27f098e20358fe013adec533c1, stripped

如果本地已经无法找到Crash对应的符号表文件或者Debug SO文件了,但还能找回Crash对应的APP版本的Native工程代码,可以重新用NDK编译出Debug SO。然后检查BuildId是否一致,一份代码多次打包,build是一致的

符号表位置

不同的gradle版本或Android Studio版本位置不同,不过一般都在app/build/intermediates/下面的某些文件夹里面

strip的so文件路径

没有strip的so文件路径

在打包so提供给别人使用的时候,需要检查一下是否去掉了调试信息,不然可能带来风险

strip so

常用的strip 命令

不同架构的strip

在Android 手机中 cpu 架构是各不相同的,比如aarch64-linux-android-strip.exe、arm-linux-androideabi-strip.exe因此不同架构下要使用各自的strip裁减对应的so库。

ndk-stack

具体的用法如下

要使用 ndk-stack,首先要有一个包含未剥离版应用共享库的目录。如果您使用 ndk-build,则可在 $PROJECT_PATH/obj/local/<abi> 中找到这些未剥离版共享库,其中 <abi> 是崩溃设备的 ABI,通过查看崩溃日志中ABI信息可以获取到。(so路径只需要到<abi>这一级,不是so的路径,不然会解析不出来)

使用此工具的方式有两种。您可以将 logcat 文本作为直接输入馈送到程序。例如:

adb logcat | $NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a

您也可以使用 -dump 选项将 logcat 指定为输入文件。例如:

$ adb logcat > /tmp/foo.txt
$NDK/ndk-stack -sym $PROJECT_PATH/obj/local/armeabi-v7a -dump foo.txt

该工具会在开始解析 logcat 输出时查找第一行星号。例如:

*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***

具体信息可以查看官方文档

上面的异常堆栈命令如下

$ ndk-stack -sym \app\build\intermediates\cmake\release\obj\arm64-v8a\ -dump crash_log.txt

解析结果如下

addr2line工具

按照名字可以知道,此工具是将异常堆栈的地址转换成行号的工具,用法如下,后面的地址见异常堆栈,如下所示,libnesample.so为我们自己的so,所以地址就是000000000000e8e8,也可以传递多个地址,空格隔开

$ aarch64-linux-android-addr2line -f -C -e libnesample.so 000000000000e8e8

解析结果如下

$ aarch64-linux-android-addr2line.exe -f -C -e  libnesample.so 000000000000e8e8
//如下为输出
Java_com_example_nesample_MainActivity_stringFromJNI
C:/Users/R22345/Desktop/NESample/app/src/main/cpp/native-lib.cpp:16

如果解析失败,会显示问号,一般解析失败的原因就是so与堆栈不匹配

注意事项

当我们使用addr2line工具的时候会发现,会有很多不同版本的工具比如arm-linux-androideabi-addr2line、aarch64-linux-android-addr2line、x86_64-linux-android-addr2line,所以我们需要使用对应版本的工具,不然会报File format not recognized错误

32位CPU:arm-linux-androideabi-addr2line
64CPU:aarch64-linux-android-addr2line
x86CPU:i686-linux-android-addr2line
x86_64CPU:x86_64-linux-android-addr2line

一般手机都用aarch64-linux-android-addr2line即可,也可以通过命令去查看,如下所示,处理器类型为aarch64

$ adb shell cat /proc/cpuinfo

其他工具

aarch64-linux-android-c++filt(c++filt工具)

windwos版本有很多不同的版本,如果提示File format not recognized,换一个版本

因为C++ name demangling的存在,我们查看堆栈的时候,可能函数名字变成一个_Z开头的的字符串,可以使用此工具还原

$ arm-linux-androideabi-c++filt.exe _ZN8gameplay14PituCameraGame10initializeEv
//结果
gameplay::PituCameraGame::initialize()

aarch64-linux-android-nm(nm工具)

windwos版本有很多不同的版本,如果提示File format not recognized,换一个版本

nm命令是linux下自带的特定文件分析工具,一般用来检查分析二进制文件、库文件、可执行文件中的符号表,返回二进制文件中各段的信息。

$ aarch64-linux-android-nm.exe -D libnesample.so > auttt.log

举个例子

#00 pc 000000000000e8e8  /....../libnesample.so (Java_com_example_nesample_MainActivity_stringFromJNI+28) (BuildId: 55b26db218ce4b27f098e20358fe013adec533c1)

首先使用nm命令导出符号表,然后查找对应的函数名,上面的例子是Java_com_example_nesample_MainActivity_stringFromJNI

结果如下

000000000000e8cc T Java_com_example_nesample_MainActivity_stringFromJNI

e8cc转换为10进制为59596,然后加上偏移量28为59624,59624转换为16进制为e8e8,与堆栈的pc地址相符

参考链接

https://zhuanlan.zhihu.com/p/352651095

https://blog.csdn.net/hexingen/article/details/124860298

https://zhuanlan.zhihu.com/p/42833833

https://bugly.qq.com/docs/user-guide/symbol-configuration-android/?v=20220805151734

https://cloud.tencent.com/developer/article/1192001

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注