dl_iterate_phdr 的 ANR 问题
最近一直在修改一个库,这个库是基于字节开源的 memory-leak-detector 修改的
由于近期增加了一系列的 hook,hook 方法众多,且几乎全量 hook 了所有 so,导致启动时会有 ANR 发生
ANR 原因说明
发生 ANR 的核心原因比较神奇。
背景
memory-leak-detector 使用的 xhook
作为 native hook(可能当时还没有 bhook)
xhook 一个比较不智能的地方,就是无法自动 hook 后续加载的 so
于是,memory-leak-detector 使用了 xdl(hook dlopen) + xhook 组合的方式,实现了这个功能。但是,问题很大。
原因
dl_iterate_phdr
是一个底层方法,他可以获取到so基址
xdl 包装了一层,使用 xdl_iterate_phdr,同样允许传入 callback,在 callback 循环中获取到了 so 基址,生成 elf 结构(其实也就是获得了所有地址),进行函数替换(hook过程),同时需要修改这片内存为可写
问题就发生在这里
通过读取 /proc/self/maps 来获取当前内存地址的状态并修改为可写,但是 memory-leak-detector 的逻辑是:
xdl_iterate_phdr(dl_iterate_callback, NULL, XDL_FULL_PATHNAME);
// 循环每个内存中的 so 依次回调
int dl_iterate_callback(dl_phdr_info *info, size_t size, void *data) {
if (info == NULL) {
return 0;
}
return common_callback(info->dlpi_name, get_so_base(info), data);
}
// 没什么细节
int common_callback(const char *name, uintptr_t base, void *data) {
int ret = 0;
// 省略
ret = default_callback(name, base);
// 省略
return ret;
}
int default_callback(const char *name, uintptr_t base) {
// 省略
tryHookAllFunc(elf);
// 省略
return 0;
}
static void tryHookAllFunc(xh_elf_t elf) {
for (int i = 0; i < sizeof(sPltGot) / sizeof(sPltGot[0]); i++) {
// 问题就在这里,xh_elf_hook 每次调用均会触发一次 proc/self/maps 的 IO 操作,这部分属于 xhook 的源码
xh_elf_hook(&elf, (const char *) sPltGot[i][0], (void *) sPltGot[i][1], NULL);
}
}
于是,整个过程,读取 proc/self/maps 的次数 = o 个数 * hook 方法个数
,机器越低端越漫长,最终 ANR。
既然耗时,移动到子线程是否可以呢
?不可以
dl_iterate_phdr 带一个 linker 的全局锁
,影响范围很大,activity 也会无法跳转。
如何解决
添加缓存层
我们其实只是关注 so 相关的内存状态,proc/self/maps 在没有新的 so 被加载的时候,已经被加载进内存的 so,地址没有任何变化,那我们就可以进行一次缓存。
在启动时,延迟一段时间初始化 nativedump,这个时候,so 的加载分为两种情况
- 此时 so 已经被加载
这种情况,就很简单,共用同一份当前时刻的 maps 的缓存,只要进行一次真实读取 - 此时 so 还未被加载
这种情况,需要刷新缓存。但是,我们已经延迟了初始化,所以,未被加载的 so,应该是零星几个,分别在不同时间触发几次真实 IO,是可以接受的
这样,就解决了问题,实际测试,会有轻微的掉帧(因为无论如何 dl_iterate_phdr 都是带锁的,在读取 maps 时都会阻塞)
实现
一开始在尝试,自己进行解决,后来发现 matrix 同样发现了这个问题,且对于 xhook 做了很多修修补补:
可以去查阅 matrix 的 xhook 改进版
Introduce maps and use it for replacing parsing maps multiple times.
8877ece7 tomystang tomystang@tencent.com