通过_Unwind_Backtrace函数获取堆栈信息,实际上只能获取到堆栈的十六进制地址信息,如图2-25所示。根据这些地址是无法查看到有效信息的,所以我们还需要将地址还原成对应的函数详细信息。
图2-25 获取的堆栈信息
将十六进制的地址堆栈还原成附带有效信息的堆栈有线上和线下两种方式。
1.线上堆栈信息还原
我们首先要知道地址对应的是哪个so库,确认so库名有多种方法,比如通过解析maps文件,然后对比地址范围,就能确认是哪个so文件了。不过在实际业务中,我们会调用Linux系统提供的dladdr函数,该函数专门用于获取指定地址所在的共享库的信息,函数的原型如下。函数中入参addr表示查询地址,info是一个Dl_info结构的指针,用于存储查询结果。
我们在示例程序中调用dladdr函数并打印信息,可以看到dladdr不仅能获取地址对应的so库名称,还能获取地址对应的符号名称,因此我们可以将地址、so库名称、符号名称一起打印出来,以此来让堆栈的信息更加完整。
运行程序后,我们就可以看到更加完整的堆栈信息,如图2-26所示。通过堆栈中函数的符号名称,我们基本就能定位到是哪些函数出现了异常。堆栈日志中#0、#1行的信息实际上是liboptimize.so库中我们的Hook函数和堆栈捕获函数,所以#2行的信息便是内存分配异常的地方,对应的so库名称为libexample.so,异常函数为Java_com_example_performance_1optimize_memory_NativeLeakActivity_mallocLeak。
图2-26 确认so库名称和符号名称后的堆栈信息
通过日志我们还可以发现,对于liboptimize.so、libexample.so等有符号表的so库,dli_sname能显示正确的函数符号名称,但对于libart.so等符号表已经被移除的so库,则显示为null。对于线上的正式包,出于安全和包体积的考虑,我们一般也会移除so库的符号表。符号表移除后,libexample.so库便不能正常获取符号了,也就无法定位出问题的函数,此时我们可以根据堆栈的地址来进行线下的堆栈信息还原。
2.线下堆栈信息还原
在线下环境中,我们可以使用addr2line工具来进行堆栈信息的还原,该工具会根据函数的偏移地址,获取该偏移地址对应的函数名、行号等信息。什么是函数的偏移地址呢?堆栈中的十六进制数是函数的绝对地址,它是在整个虚拟内存空间中的地址,而偏移地址则是so库中的内部地址,所以只需要用绝对地址减去so库的相对地址就能得到偏移地址。在前面的线上堆栈信息还原的代码中,实际上已经计算出了每个地址的偏移地址,代码如下所示。
补充了偏移地址,我们通过日志就能知道异常函数的偏移地址是0x1edea。接下来就可以通过addr2line工具来进行函数名和对应行的信息还原了,Android的NDK中提供了该工具,执行如下命令:
命令中的-C表示将低级别的符号名称解码为用户级别的名称,-f表示在显示文件名、行号信息的同时显示函数名,-e用于指定需要转换地址的可执行文件名。执行结果如图2-27所示,可以看到,该偏移地址位于mock_native_leak.cpp文件的第10行,我们根据这个信息就可以准确定位有问题的地方。
图2-27 使用addr2line进行堆栈还原
需要注意的是,通过addr2line工具来解析的so库是需要带有符号表的,否则也无法正确地进行堆栈还原。在编译产物(如图2-28所示)的merged_native_libs(如图2-29所示)中即能找到带有符号表的so库,编译产物文件中还有一个名为stripped_native_libs的文件,里面的so库都是移除了符号表的,线上的正式包中都会使用这里的so库。
图2-28 编译产物
图2-29 带符号表的so库
到这里我们就完成了对示例程序中异常分配内存的函数的检测和定位。虽然示例中讲解的是一个很明显的内存申请异常,而实际项目中往往出现的都是较隐秘的内存泄漏,但是排查和定位异常的原理及流程与这里讲解的都是一致的,我们只需要再补充拦截free内存释放函数,然后按照固定频率(比如10min/次)将malloc函数总共申请的内存和free函数总共释放的内存相减计算差值,如果差值不断变大并超过我们设置的阈值,比如512MB,则认为该so库发生了内存泄漏。分析和治理so库中内存异常的流程较长,而且知识点也比较多,这里建议读者能够自己操作一遍,以更深刻地理解整个流程以及其中涉及的知识点。
对于第三方sdk,因为都是已经移除符号表的,所以无法通过addr2line查看对应的函数和对应的行数,即使第三方sdk的so库没有移除符号表,我们在没有源码的情况下也无法修改。因此,对于第三方sdk的so库,我们只需要通过Native Hook来确认其是否存在内存泄漏或异常问题即可,如果有的话则将该so库替换成稳定的版本。