本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

背景

咱们在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*);
  1. __pthread_ptr:pthread_t类型的参数,成功时tidp指向的内容被设置为新创立线程的pthread_t
  2. __attr 线程的属性
  3. __start_routine 履行函数,新创立线程从此函数开端运转
  4. __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)操作时,这儿会被填入实在的外部调用的肯定地址。

Android性能优化 - plt hook 与native线程监控

经过这一种方法,linux现已能在符号地址绑定这块得到了较好的功能,可是GOT表的生成也是链接进程的一个耗费,所以linux又供给了一种叫推迟绑定的手法,只有在函数实在用到的时分,才进行函数的地址定位。咱们来了解一下过程:

Android性能优化 - plt hook 与native线程监控

当咱们进行链接的时分,链接器不进行函数符号的寻址,而是经过一条push指令作为替代品(耗费十分小),push指令的入参能够是rel.plt等重定位表相关的下标,在运转时才进行实在的函数地址寻址。

Android性能优化 - plt hook 与native线程监控

可是!!在咱们Android系统中,现在只有 MIPS 架构支撑 lazy binding,所以现在在android,对plt表的内容定位就不在运转时进行,而是直接在链接时确认,未来会不会更多支撑推迟绑定呢,还不确认,所以这个咱们作为了解即可。

PLT Hook

咱们从上面调用能够看到,plt表的调用原理,所以咱们的hook点也很清晰,假如咱们想要fun1-> fun2 变成 fun1 -> fun 3的话(fun2 跟 fun3 必须是外部函数,假如不是外部函数就不会生成plt表进行跳转,由于是本模块就不需求凭借plt表,直接生成地址无关代码偏移即可)

Android性能优化 - plt hook 与native线程监控

以上面的比如动身,咱们需求对libtest中的pthread_create进行hook,从而收集pthread_create的数据,由于咱们完成plthook需求以下几步。

  1. 定位出pthread_create的相对偏移(上面说过函数的实在地址是基地址+相对偏移),那么这个偏移在哪呢?咱们从上面流程图能够看到,偏移就在.rel.plt中(并不是所有偏移都在这儿,重定位信息能够分布在.rel.plt,.rela.plt,.rel.dyn,.rela.dyn,.rel.android,.rela.android等多个表中,可是一般的外部调用不需求经过大局函数跳转都在.rel.plt表中),咱们能够经过readif -r libtest.so去查看
    Android性能优化 - plt hook 与native线程监控
    就这样咱们找到了偏移地址 0x23f8

2.找到基地址,从前面咱们能够知道,基地址是运转时决议的,咱们能够在运转时检索/proc/self/maps文件,在里边找到libtest.so的匹配项即可(xhook 做法,为了兼容android4 及以上,个人更喜爱用dl_phdr_info 5.0 以上直接获取)

格局如下

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功能优化线程相关的优化就到此完毕,恭喜踏入下一篇!