精准定位Android Native崩溃
背景
一直以来线上的Android Native崩溃都是通过第三方SDK收集到的信息作为主要的分析方向,但是最近一段时间频繁出现了大量异常的崩溃信息,表现在出现崩溃的用户设备的内存可用占比都是低于10%。

起初以为是某个版本做了改动出现了内存泄露,导致线上OOM将其归结为内存优化处理。随着更多的线上崩溃出现,从其中一部分日志调用栈里发现崩溃的地方大部分并非内存分配的调用。既然不是OOM导致的崩溃,就需要分析背后的原因。
现有手段
对于Android而言,native层Crash相比于Java层更难捕获与定位,线上发布的so更是去除了调试信息。目前已知的分析工具有以下几种:
- ndk-stack
- 使用方式: 通过输入带有调试信息的so路径与崩溃日志获得源文件行号
- 带有调试信息的so: 必须
- 使用门槛: 低
- addr2line
- 使用方式: 通过崩溃时的程序计数器(Program Counter)与带有调试信息的so得到源文件行号
- 带有调试信息的so: 必须
- 使用门槛: 低
- objdump
- 输出so库中的汇编与源码信息,如果so库没有调试信息则只有汇编代码。
- 带有调试信息的so: 可有可无
- 使用门槛: 高,需要一定的汇编知识
通常一次编译会先生成一个有含有调试信息的 so, 路径通常是在 obj/local/ 各 abi 目录下,其中还有一些中间文件(比如.o文件)。再通过对这些含有调试信息的 so 进行一次 strip , 产生对应的无调试信息so。
我们的项目中发布流程全部是由CD服务器来打包输出最终产物,但是每一次打包都会清理编译目录,导致编译结果已经没办法找回了。
所以只有objdump是目前能够使用的工具,使用命令
./aarch64-linux-android-objdump -S ~/Desktop/lib/arm64-v8a/libXPlayer.so > ~/Desktop/1.txt
这里解释一下这个命令:
- aarch64-linux-android-objdump是NDK自带的反汇编工具,有32位跟64位的区别。这里我要分析的是64位的so,所以使用的是aarch64。
- -S 参数表示将反汇编代码与源代码交叉输出,我们的so没有调试信息所以只有汇编代码。
- 最后一个参数> 表示将结果输出到~/Desktop/1.txt文件
执行命令无误我们将得到一个包含汇编代码的文本文件。接下来我们需要从崩溃日志中提取崩溃点的PC(Program Counter)地址定位到对应的汇编代码。

也就是蓝色箭头所指的地方0x268e28,接着打开我们刚才导出的汇编代码文件

查找268e28就可以定位到具体崩溃的汇编语句,这句的汇编的伪代码:
// LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。
// ldr w19, [x0,#4]
float w19 = *(float *)(x0 + 4); // 将寄存器x0的值 + 4之后作为地址,读取数据写入寄存器w19。
结合崩溃日志中的信号信息 signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0000000000000004 访问了0x4这个地址,可以很容易的得出结论x0寄存器保存的值是0,最终导致访问了错误的地址。解决方案,网上追溯x0寄存器在哪里赋值了。
是否有更好的方法?
虽然最终通过汇编的方式解决了问题,但是如果在崩溃的点是栈上的地址就要往上回溯,中间还得处理各种跳转指令一不小心就会在汇编指令中迷失方向。
那么有没有更好的方法? 既然都已经到了汇编这一层,基于之前的逆向经验我想是否可以使用IDA来分析?
说干就干,将so拖入ida64。一路下一步之后等待它分析完成。分析完成后快捷键g跳转到我们崩溃地址268e28

你可能会说这不是跟我们导出的汇编代码文件一样吗?只是多了代码高亮。
当然如果只是这样没必要使用ida,但是它还有另外一种流程视图可以把所有的分支跳转帮我们理清代码逻辑,快速定位到这块代码的具体逻辑分支。
按下快捷键空格

最后我们只需要关注分析这块代码逻辑,并且可以顺着调用链往上追溯需要跟踪的信息。
总结
虽然最终通过ida的方式解决了问题,但是比起直接通过符号表的方式去排查问题,效率会低了一点。不过汇编的方式能够比较精确的定位到所在问题,毕竟通过行号定位有可能出现逻辑太多不能确定是哪个变量出问题。
ida是个十分强大的工具,不仅在逆向中可以发挥作用。在正向开发中也能帮助我们分析应用中的问题,比如手写的算法翻译成汇编不一定高效,可以通过逆向的方式查看最终生成汇编代码去做优化。