android 如何分析应用的内存(七)——malloc hook

android 如何分析应用的内存(七)

接上文,介绍六大板块中的第二个————malloc hook

上一篇的自定义分配函数,常常只能解决当前库中的分配,而不能跟踪整个app中的分配。

为此,android的libc库,从Android 9.0开始引入了malloc hook技术

这个技术定义了四个全局变量,这个变量指向对应的函数。这四个全局变量是:

void* (*volatile __malloc_hook)(size_t, const void*);
void* (*volatile __realloc_hook)(void*, size_t, const void*);
void (*volatile __free_hook)(void*, const void*);
void* (*volatile __memalign_hook)(size_t, size_t, const void*);

他们的对应关系如下:

hook function
__malloc_hook malloc
__malloc_hook calloc
__realloc_hook realloc
__free_hook free
__memalign_hook memalign
__memalign_hook posix_memalign
__memalign_hook aligned_alloc

注意:在32位系统中,有两个已经不在推荐使用的函数,
pvalloc和valloc对应的hook为__memalign_hook。

再次注意:malloc_usable_size这个函数,目前还没有与之对应的hook可以使用

这些变量可以在任何时刻被修改,但是他们不是线程安全的,因此,一定要在适当的时候,修改这些变量。否则,可能会导致程序崩溃

启动Bionic中的hook功能

对于Framework工程师,有两种方法打开这个功能

  1. 通过setprop如下
## 首先,需要关掉Android的系统服务,这样可以追踪app中调用系统服务的分配
adb shell stop
## 然后,设置libc.debug.hooks.enable为1,打开hook功能
adb shell setprop libc.debug.hooks.enable 1
## 最后重新启动Android系统中的系统服务
adb shell start
  1. 只在当前的shell环境中,启用这个功能如下
## 进入shell环境
adb shell
## 使能shell变量LIBC_HOOKS_ENABLE为1,打开hook功能
export LIBC_HOOKS_ENABLE=1
## 运行一个本地程序,ls,这样就会跟踪ls的内存分配情况
ls

对于APP工程师,依然有两种方式打开这个功能,这两种方式都是通过设定LIBC_HOOKS_ENABLE=1环境变量来实现

  1. 通过命令行中设置wrap.app属性传递
adb shell setprop wrap.<APP> '"LIBC_HOOKS_ENABLE=1"'

举例如下:

## 要设置的app的包名为com.google.android.googlequicksearchbox 
adb shell setprop wrap.com.google.android.googlequicksearchbox '"LIBC_HOOKS_ENABLE=1 logwrapper"'

原理简述:系统在启动app的时候,会查看是否有wrap.app这个属性,如果有且不会空,则会将这个属性的值,作为一个shell命令,这个shell命令的后面,还会跟上应用的包名。比如上例会在一个shell中运行如下命令

LIBC_HOOKS_ENABLE=1 logwrapper com.google.android.googlequicksearchbox

然后在同一个shell环境中,启动应用。在上面的例子中,我们设置了一个环境变量LIBC_HOOKS_ENABLE=1,然后再将com.google.android.googlequicksearchbox中未输出到log系统中的log重定向到了log系统中。

  1. 通过在app中增加wrap.sh脚本达到.

这个方法在后续的调试中,还会大量登场,因此,下面通过一个小节详细介绍这部分内容。

如何在app中使用wrap.sh

通过wrap.app来增加简单的shell命令,是非常可行的。但当命令复杂起来,用shell脚本就更加合适了。

  1. 新建一个shell脚本,命名为wrap.sh
  2. 将wrap.sh放入src/main/resources/lib/*中

在这里插入图片描述

当打包之后,wrap.sh将会出现在apk的lib/arm64-v8a/目录下

  1. 修改apk的编译选项。
    在Android的manifest文件中的application标签,打开调试
android:debuggable="true"

同时还需要在build.gradle中将 useLegacyPackaging设置为true。这句话的意思是:是否要使用传统的对so库进行压缩的打包方式。如果没有设置,当minSdk>=23时,将不会压缩,且会保持页对齐方式进行打包。

如下:

packagingOptions {
    pickFirst 'lib/arm64-v8a/libc++_shared.so'
    exclude 'META-INF/*'
    doNotStrip "*/arm64-v8a/*.so"
    jniLibs {
        useLegacyPackaging true
    }
}
  1. 测试,为了查看wrap.sh是否生效,我们在wrap.sh中增加了一个环境变量如下:
#!/system/bin/sh
export wanbiaowrapsh=1
exec "$@"

在这个脚本中,我们定义了一个环境变量wanbiaowrapsh并将其值置为1并使用exec命令,执行程序

除此之外在app的适当,读取环境变量是否成功如下:

val env = System.getenv()
for (envName in env.keys) {
    Timber.w("Midimanager %s=%s%n", envName, env[envName])
}

运行app,查看log,可得如下输出。
在这里插入图片描述
可见,环境变量已经能够被正确的读取了

注意:在wrap.sh的脚本中,换行符,一定要是LF,而不能是CRLF。如果是后者将会导致应用被卡主,而无法正常运行

注意:wrap.sh的使用条件,最低为Android 8.1

实现bionic中的hook

有了前面打开bionic的功能。接下来我们将实现上面介绍的四个hook。

在这一部分中,我们依然会使用上一小节中,定义的AllPtr和Debug对象,记录所有的分配调用栈和时间。

接下来是每个函数的实现。

//因为hook函数不是线程安全的,因此在修改他们的时候,加了如下的锁
static std::recursive_mutex mutexMalloc;
static std::recursive_mutex mutexFree;
static std::recursive_mutex mutexRealloc;
static std::recursive_mutex mutexMemalign;

//在上一小节中定义的两个函数,分别保存和删除ptr
extern void addToAllptr(void *ptr,std::size_t sz);
extern void popFromAllptr(void *ptr);

//分别保存,原始的分配函数和释放函数
const auto origin_malloc = __malloc_hook;
const auto origin_free = __free_hook;
const auto origin_realloc = __realloc_hook;
const auto origin_Memalign = __memalign_hook;

//新的malloc分配函数
void *new_malloc(size_t size, const void * caller){
    
    
    std::unique_lock<std::recursive_mutex> _l(mutexMalloc);
    //使用原始分配函数,进行分配
    auto ptr = origin_malloc(size,caller);
    //为了让addToAllptr函数能够正常使用malloc,恢复原位
    __malloc_hook = origin_malloc;
    addToAllptr(ptr,size);
    __malloc_hook = new_malloc;
    return ptr;//注意要返回,当没有返回值时,有些编译器可能不会提示错误
}

//新的free释放函数
void new_free(void* ptr, const void* caller){
    
    
    std::unique_lock<std::recursive_mutex> _l(mutexFree);
    //使用原始的释放函数,进行释放
    origin_free(ptr,caller);
    //为了让popFromAllptr能够正常使用free,恢复原位
    __free_hook = origin_free;
    popFromAllptr(ptr);
    __free_hook = new_free;
}

//剩下两个没有做任何修改,仅仅是演示使用
void* new_realloc(void* ptr, size_t size, const void* caller){
    
    
    //nothing to do
    return origin_realloc(ptr,size,caller);
}

void* new_memalign(size_t size, size_t size1, const void* caller){
    
    
    //nothing to do
    return origin_Memalign(size,size1,caller);
}

除了在上面进行修改以外,还需要在启动的时候,将对应的hook函数变量改成新的值,因此在加载so库的时候,做此操作。如下:

jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
    
    
    //分别将对应的hook函数,修改成新的值
    {
    
    
        std::unique_lock<std::recursive_mutex> _l(mutexMalloc);
        __malloc_hook = new_malloc;
    }
    {
    
    
        std::unique_lock<std::recursive_mutex> _l(mutexFree);
        __free_hook = new_free;
    }
    {
    
    
        std::unique_lock<std::recursive_mutex> _l(mutexRealloc);
        __realloc_hook = new_realloc;
    }
    {
    
    
        std::unique_lock<std::recursive_mutex> _l(mutexMemalign);
        __memalign_hook = new_memalign;
    }

    return JNI_VERSION_1_6;
}

这样当so库加载完成之后,就能够正确的处理我们自定义的hook函数了。

在进行测试之前,还需要进行一点额外的操作。

上一小节中,定义了一个void printStackTrace(char *buffer, int size);函数。现在再定义一个函数,void printStackTrace();将对应的堆栈信息直接输出到log系统中。如下

void Debug::printStackTrace() {
    
    
        const auto maxStackDeep = 50;
        intptr_t stackBuf[maxStackDeep];
        char outBuf[1024*maxStackDeep];
        memset(outBuf, 0, sizeof(outBuf));
        dumpBacktraceIndex(outBuf, stackBuf, captureBacktrace(stackBuf, maxStackDeep));
        ALOGD("-----start-----");
        for(int i=0;i<maxStackDeep;i++){
    
    
            auto startLine = outBuf+i*1024;
            if(strlen(startLine) > 0){
    
    
                ALOGD("%s", outBuf+i*1024);
            }
        }
        ALOGD("-----end-----");
    }

上面函数,将堆栈层数,调整到了50层。

开始测试

因为在后续的文章中,还会使用到wrap.sh。所以本次测试直接使用wrap.sh。内容如下,可直接复制使用

#!/system/bin/sh
export LIBC_HOOKS_ENABLE=1
exec "$@"

打包并运行,可在log系统中观测到类似如下的log。

-----start-----
2023-06-14 13:20:02.045 19720-19720 Find_Debug              pid-19720                            D  #0: 0x792227647c  0x2747c  _ZN4Find5Debug16captureBacktraceEPlm      /data/app/~~KRrGK7sLlqLxlvtobHnUfg==/com.example.test_malloc-Ot3c1Kcp1YMgiDUYl4TFjA==/lib/arm64/libtest_malloc.so

//省略若干                                                                         
                                                                                                    #38: 0x799d80a258  0x20a258              /apex/com.android.art/lib64/libart.so
2023-06-14 13:20:02.045 19720-19720 Find_Debug              pid-19720                            D  -----end-----

截图如下:
在这里插入图片描述

从log可以看到,几乎所有的分配都被捕获到了,而不仅仅是当前代码库中的分配。这对于framework工程师来讲,有很高的参考价值。

同样的,也将void printStackTrace(char *buffer, int size);函数的堆栈增加到50层。此处不在贴图,原理和上面一样。

接下来就是按照上一小节中的方法,使用shell脚本,直接按照时间过滤。得到久未释放的指针。然后查看其调用栈,获取具体的分配情况,以鉴别是否内存泄漏

至此。malloc hook介绍完毕。这个方法,要求在Android 9.0以上的版本才可以使用。为了能够在Android 9.0以下的版本中使用,在下一小节,我们将介绍malloc的另外的功能————malloc调试和libc的回调

猜你喜欢

转载自blog.csdn.net/xiaowanbiao123/article/details/131206881