HeapSnap工具原理及其应用

转载自我的CSDN Blog

简介

HeapSnap工具,其名称源于Heap Snapshot,意即堆内存快照。其实现方式是:在不同的时间点上保存堆内存的快照,然后对比这些不同时间点的快照,找出导致内存增长的泄露点。

HeapSnap工具专门用于处理Android系统中native进程的heap内存泄露问题。它的实现是基于Android已有的libc debug机制,对目标进程的影响小,易于使用,且解决问题的概率高。

源码下载: https://github.com/albuer/heapsnap

HeapSnap工具演示

演示环境: Android5.1

  • 使能malloc leak调试开关

    1
    setprop libc.debug.malloc 1
  • 启动测试程序,该程序每3秒钟泄露4KB大小的内存,测试代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <android/log.h>
    4
    5 void* foo(void)
    6 {
    7 void* p = malloc(4096);
    8 memset(p, 0x5A, 4096);
    9 return p;
    10 }
    11
    12 int main(void)
    13 {
    14 int count=0;
    15 void * p = NULL;
    16 while(1) {
    17 p = foo();
    18 ++count;
    19 printf("%d: %p\n", count, p);
    20 sleep(3);
    21 }
    22 return 0;
    23 }
  • 向目标进程注入代码,从而让目标进程拥有保存heap快照的功能

    1
    heapsnap -p <进程ID> -l libheapsnap.so
  • 保存heap快照

    1
    kill -21 <进程ID>

    从logcat信息中可以看到heap快照被保存在/data/local/tmp/heap_snap/proc_7104_0000.heap,其内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Heap Snapshot v0.2

    Total memory: 103424
    Allocation records: 2

    size 4096, dup 25, 0xb6ec576e, 0xb6f3d282, 0xb6fa74ba, 0xb6fa73ac, 0xb6f3d39c, 0xb6fa7428
    #00 pc 0000676e /system/lib/libc_malloc_debug_leak.so (leak_malloc+101)
    #01 pc 00012282 /system/lib/libc.so (malloc+9)
    #02 pc 000004ba /system/bin/leak_test
    #03 pc 000003ac /system/bin/leak_test
    #04 pc 0001239c /system/lib/libc.so (__libc_init+43)
    #05 pc 00000428 /system/bin/leak_test
    size 1024, dup 1, 0xb6ec576e, 0xb6f3d282, 0xb6f60efa, 0xb6f65eb2, 0xb6f61bac, 0xb6f63172, 0xb6f61128, 0xb6fa73b8, 0xb6f3d39c, 0xb6fa7428
    #00 pc 0000676e /system/lib/libc_malloc_debug_leak.so (leak_malloc+101)
    #01 pc 00012282 /system/lib/libc.so (malloc+9)
    #02 pc 00035efa /system/lib/libc.so (__smakebuf+21)
    #03 pc 0003aeb2 /system/lib/libc.so (__swsetup+105)
    #04 pc 00036bac /system/lib/libc.so
    #05 pc 00038172 /system/lib/libc.so (vfprintf+17)
    #06 pc 00036128 /system/lib/libc.so (printf+23)
    #07 pc 000003b8 /system/bin/leak_test
    #08 pc 0001239c /system/lib/libc.so (__libc_init+43)
    #09 pc 00000428 /system/bin/leak_test

    ******** MAPS ********

    其中:

  • Total memory: 103424,指的是进程申请的堆内存大小,此处是103424 Bytes

  • Allocation records: 2,总共有2条内存分配路径

  • size 4096, dup 25, 0xb6ec576e, … ,size指的是每块内存的大小,此处为4096Bytes,dup指重复的次数,此处为25,表示这个内存分配路径被调用了25次,后面的地址就是backtrace,以及对backtrace解析后的信息。

这个demo程序中只有两条heap分配记录,第一条的size为4096,dup为25,表明这个heap分配路径被重复执行了25次,且分配的内存都没有释放掉,因些我们能够很明显看出这个内存分配路径就是泄露点了。

从backtrace我们可以看到调用malloc函数的位置是

#02 pc 000004ba /system/bin/leak_test

我们调用addr2line查询地址”0x000004ba”所指向的代码行:

arm-linux-gnueabi-addr2line -ife out/target/product/rk3288/symbols/system/bin/leak_test 0x4ba
test
/home/cmy/WORK/android-src/android5.1-vr/external/heapsnap/leak_test.c:7

通过addr2line命令反查到”0x4ba”这个地址指向leak_test.c文件的第7行,foo()函数。

在实际的程序中,heap快照中的分配记录通常都是非常多的,不容易找出哪个内存分配路径是泄露点,此时可以通过对比不同时间点上,相同内存分配路径下的dup次数来做出判断,如果某个分配路径的dup次数处于持续增长的状态,那么它就很可能是一个泄露点了。

HeapSnap工具的实现原理

  • 获取进程的heap信息
    我们首先来看下在Android中,进程的heap分配的backtrace信息是如何被保存以及如何读取的。

    方法很简单,只需要先打开malloc调试开关之后,再执行目标进程,那么在这个新运行的进程内发生的所有heap分配的backtrace信息就会被记录下来,之后在该进程内调用get_malloc_leak_info函数即可获取到所有前面保存的heap’s backtrace记录。

    1. malloc调试开关

      • 调用’setprop libc.debug.malloc 1’设置好属性后,malloc开关就使能了且被设置为leak模式(注:在Android7以及之后的版本,设置属性”setprop libc.debug.malloc.options backtrace”)。

      • 接着,我们开始执行目标进程,进行启动过程上中会初始化一个加载器,在Android中是/system/bin/linker

      • 通过加载器linker,把所有需要用到的so文件都加载进来。

      • 在linker加载so文件过程中,会自动执行so文件内的.init/.init_array section代码

      • 在libc.so中,定义了一个函数”attribute((constructor)) static void libc_preinit()”,修饰符“attribute__((constructor))”告知编译器把libc_preinit函数指针放到.init_array section,于是函数libc_preinit在so被linker加载时被自动执行了

      • 在__libc_preinit()函数中,会去读取libc.debug.malloc属性值,并根据所获得的值设置malloc/free…等函数指针指向不同的函数实现,此处libc.debug.malloc为1,则函数指针指向leak_malloc/leak_free….等leak_xxxx形式的函数实现。

        于是,属性libc.debug.malloc决定了所使用到的mallc/free的实现函数。

      1. heap’s backtrace信息的保存与读取
      • 当malloc调试开关设置为leak模式后,在进程内执行malloc/calloc等函数时候,实际调用的是leak_malloc/leak_calloc等函数,这些函数会在分配的内存前面附加一个头部信息,在该头部信息里面保存了此次内存分配的backtrace信息。
      • libc提供了一个函数get_malloc_leak_info,通过该函数我们就能获得当前进程所有未释放的heap的backtrace信息了。最终我们获取到的backtrace信息如前面所示。
  • 进程注入
    前面说到调用get_malloc_leak_info可以获得当前进程的heap信息,但要怎么才能让目标进程去调用get_malloc_leak_info函数,一种是修改代码目标代码,加入获取进程heap快照的相关代码后重新编译;还有另外一种方式就是使用进程注入,它能把一段代码注入到目标进程中并执行。

    进程注入流程图如下所示:

    process inject

    大致流程描述如下:

  1. 先调用ptrace(PTRACE_ATTACH, …)把当前程序附着在目标进程之上,即成为目标进程的父进程。

  2. 在目标进程中开辟一块内存空间用于传递参数信息到目标进程

  3. 把要注入的程序库文件名信息保存在前面开辟出来的内存空间,并调用dlopen把该程序库加载到目标进程中。

  4. 在程序库中定义有一个“extern “C” void attribute((constructor)) prepare()”函数,这个函数会在so被加载到目标进程后被自动执行,它会为信号SIGTTIN注册一个处理函数,当该信号被触发后,其注册的处理函数会调用get_malloc_leak_info获得当前进程的heap快照,并保存成文件。

  5. 至此,注入程序已经把指定代码注入到目标进程中,并且在目标进程中注册了一个函数用于处理信号SIGTTIN,于是我们可以用kill命令向目标进程发送SIGTTIN信号,然后目标进程的当前heap快照就会被保存下来了。

  6. 最后,我们需要把先前在目标进程中开辟的那块内存空间释放掉,还原寄存器,调用ptrace(PTRACE_DETACH, pid, …)结束对该进程的追踪,目标进程就能沿着原来的流程继续执行下去了,只是在它内部多了我们注入的代码。