目录
背景
我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有一个黑科技,就是我们的PIL Hook,目前行业上比较出名的就是xhook,和bhook了。
native 线程创建
了解PLT Hook之前,我们先了解一下native层常用的创建线程的手段,没错,就是pthread
int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
- __pthread_ptr:pthread_t类型的参数,成功时tidp指向的内容被设置为新创建线程的pthread_t
- __attr 线程的属性
- __start_routine 执行函数,新创建线程从此函数开始运行
- __start_routine中 需要运行的入参,如果__start_routine不需要入参,则该值为null
接下里我们用这个例子去说明,我们在MainActivity中设定了一个名叫threadCreate的jni调用,开启一个新线程,在新线程里面打印一些传递的数据。
libtest.so中的代码 /* 声明结构体 */ struct member { int num; char *name; }; /* 定义线程pthread */ static void *pthread(void *arg) { struct member *temp; /* 线程pthread开始运行 */ printf("pthread start!\n"); /* 打印传入参数 */ temp = (struct member *) arg; printf("member->num:%d\n", temp->num); printf("member->name:%s\n", temp->name); return NULL; } extern "C" JNIEXPORT void JNICALL Java_com_example_signal_MainActivity_threadCreate(JNIEnv *env, jobject thiz) { pthread_t tidp; struct member *b; /* 为结构体变量b赋值 */ b = (struct member *) malloc(sizeof(struct member)); b->num = 10086; b->name = "pika"; /* 创建线程pthread */ if ((pthread_create(&tidp, NULL, pthread, (void *) b)) == -1) { printf("create error!\n"); } }
通过jni方式调用的pthread,我们就没办法用常规手段去监控了。所以我们才需要plt hook的方式
PLT
介绍plt hook之前,我们还是有必要了解一些前置的知识。在linux中,会存在很多地址无关的代码。在我们的编写模块中,其实会遇到很多共享对象地址冲突的问题,如果相互依赖的对象是以绝对地址的方式存在的话,那么运行的时候就会发生地址冲突,比如进程A里面两个方法都被定位到了同一个地址,所以才有了地址无关的代码。
地址无关的代码大多数采用运行时基地址+编译时定向偏移,其中基地址可以在运行时确定,但是某个符号的运行时地址相对于基地址来说,就可以是一个确定的偏移数值。通过这种方式,函数可以在被需要的时候再进行绑定地址即可,在编译时只需要记录偏移就可以保证后期的运行寻址的正常。这个保存偏移地址的东西,就叫做GOT表(全局偏移表),当代码需要引用到这个符号的时候,就可以通过GOT表间接定位到真正的地址,动态链接器(linker)执行重定位(relocate)操作时,这里会被填入真实的外部调用的绝对地址。
通过这一种方式,linux已经能在符号地址绑定这块得到了较好的性能,但是GOT表的生成也是链接过程的一个消耗,所以linux又提供了一种叫延迟绑定的手段,只有在函数真正用到的时候,才进行函数的地址定位。我们来了解一下步骤:
当我们进行链接的时候,链接器不进行函数符号的寻址,而是通过一条push指令作为替代品(消耗非常小),push指令的入参可以是rel.plt等重定位表相关的下标,在运行时才进行真正的函数地址寻址。
但是!!在我们Android体系中,目前只有 MIPS 架构支持 lazy binding,所以目前在android,对plt表的内容定位就不在运行时进行,而是直接在链接时确定,未来会不会更多支持延迟绑定呢,还不确定,所以这个我们作为了解即可。
PLT Hook
我们从上面调用可以看到,plt表的调用原理,所以我们的hook点也很明确,如果我们想要fun1-> fun2 变成 fun1 -> fun 3的话(fun2 跟 fun3 必须是外部函数,如果不是外部函数就不会生成plt表进行跳转,因为是本模块就不需要借助plt表,直接生成地址无关代码偏移即可)
以上面的例子出发,我们需要对libtest中的pthread_create进行hook,从而采集pthread_create的数据,因为我们实现plthook需要以下几步。
定位出pthread_create的相对偏移(上面说过函数的真实地址是基地址+相对偏移),那么这个偏移在哪呢?我们从上面流程图可以看到,偏移就在.rel.plt中(并不是所有偏移都在这里,重定位信息可以分布在.rel.plt
, .rela.plt
, .rel.dyn
, .rela.dyn
, .rel.android
, .rela.android
等多个表中,但是一般的外部调用不需要经过全局函数跳转都在.rel.plt表中),我们可以通过readif -r libtest.so去查看
就这样我们找到了偏移地址 0x23f8
2.找到基地址,从前面我们可以知道,基地址是运行时决定的,我们可以在运行时检索/proc/self/maps文件,在里面找到libtest.so的匹配项即可
格式如下
so的范围地址 权限 基地址(重点关注) dev inode so名称
3.通过基地址+偏移,我们得到了跳转目标函数的地址,这个时候只需要把这个地址指向的函数更改为我们自定义函数即可,地址的概念,p->自定义函数
4.虽然我们实现了函数替换,但是这个被替换的函数地址可能会缺少相关的读写权限,导致出现读取该地址的时候发生读写异常,我们可以通过
int mprotect(void* __addr, size_t __size, int __prot);
进行读写权限的添加,addr就是当前的地址,size就是大小,我们以当前页大小执行即可(被修改权限的地址[addr, addr+len-1]),prot当前权限枚举
5.由于存在缓存指令的影响,我们需要消除这部分可能已经被缓存的指令,可以通过已提供的
void __builtin___clear_cache (char *begin, char *end);
去清除指令缓存,以页为单位。一个地址所处的页与结束时的页可以通过以下代码换算
#define PAGE_START(addr) ((addr) & PAGE_MASK) #define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)
其中PAGE_SIZE 由宏定义,这里为 #define PAGE_SIZE 4096
通过以上步骤,我们就能够实现了我们对pthread的hook,这里给出完整的实现
bool isHook = true; int my_pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void* p1) { if(isHook){ isHook = false; __android_log_print(ANDROID_LOG_INFO, "hello", "%s","pthread hook power by pika"); return pthread_create(__pthread_ptr,__attr,__start_routine,p1); } else{ return 0; } } #define PAGE_START(addr) ((addr) & PAGE_MASK) #define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE) void hook() { char line[512]; FILE *fp; uintptr_t base_addr = 0; uintptr_t addr; //寻找基地址 if(NULL == (fp = fopen("/proc/self/maps", "r"))) return; while(fgets(line, sizeof(line), fp)) { if(NULL != strstr(line, "libtest.so") && sscanf(line, "%" PRIxPTR"-%*lx %*4s 00000000", &base_addr) == 1) break; } fclose(fp); __android_log_print(ANDROID_LOG_INFO, "hello", "%u", base_addr); if(0 == base_addr) return; //得到真实的函数地址 可由readif -r 看到 addr = base_addr + 0x23f8; // 添加读写权限 mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE); // 替换为函数地址 *(void **)addr = (unsigned*)&my_pthread_create; // 清除缓存 __builtin___clear_cache(static_cast<char *>((void *) PAGE_START(addr)), static_cast<char *>((void *) PAGE_END(addr))); }
调用hook()后,libtest中pthread_create 就会被转化为my_pthread_create的调用,这样我们就实现了一次plt hook!
xhook bhook
上面我们hook的偏移都是基于通过readif看到的偏移地址,但是实际上这个地址都用readif可能会非常不方便,而且我们也只是检索了rel.plt表,实际上会存在多个复杂的跳转现象时,就需要检索所有的重定位表。但是没关系,这些xhook bhook都帮我们做了,只需要调用封装好的方法即可,我们这里就不结束api了,感兴趣读者可自行观看readme
plt hook总结
最后我们来总结一下plt hook相关优缺点
优点 | 缺点 |
---|---|
可操作性强,原理简单易用 | 局限性 plt hook 只能作用在外部函数,即调用生成重定位表的方法中 |
适配成本低,只需要hook 相关重定位表即可,由elf文件保证其规范 |
当前,为了解决plt hook的局限性问题,同时也有对inline hook 的开源框架,但是inline hook存在适配成本较高稳定性较差的问题,一直没有得到非常大的推广,一般只在特殊场景下的使用,这里普及一下并不详细展开说明!看完这里读者朋友们应该能够理解plt hook在pthread_create的应用,由于里面涉及了一些elf文件的内容,我们先粗略了解,必要的时候需要进一步学习查询即可,我们在以后会推出elf文件相关的介绍文章,欢迎继续关注!到这里,android性能优化线程相关的优化就到此结束,更多关于Android plt hook native线程监控的资料请关注其它相关文章!