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工程师,有两种方法打开这个功能
- 通过setprop如下
## 首先,需要关掉Android的系统服务,这样可以追踪app中调用系统服务的分配
adb shell stop
## 然后,设置libc.debug.hooks.enable为1,打开hook功能
adb shell setprop libc.debug.hooks.enable 1
## 最后重新启动Android系统中的系统服务
adb shell start
- 只在当前的shell环境中,启用这个功能如下
## 进入shell环境
adb shell
## 使能shell变量LIBC_HOOKS_ENABLE为1,打开hook功能
export LIBC_HOOKS_ENABLE=1
## 运行一个本地程序,ls,这样就会跟踪ls的内存分配情况
ls
对于APP工程师,依然有两种方式打开这个功能,这两种方式都是通过设定LIBC_HOOKS_ENABLE=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系统中。
- 通过在app中增加wrap.sh脚本达到.
这个方法在后续的调试中,还会大量登场,因此,下面通过一个小节详细介绍这部分内容。
如何在app中使用wrap.sh
通过wrap.app来增加简单的shell命令,是非常可行的。但当命令复杂起来,用shell脚本就更加合适了。
- 新建一个shell脚本,命名为wrap.sh
- 将wrap.sh放入src/main/resources/lib/*中
当打包之后,wrap.sh将会出现在apk的lib/arm64-v8a/目录下
- 修改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
}
}
- 测试,为了查看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的回调,以及DDMS图形工具进行查看