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

4,580 阅读8分钟

本文为稀土掘金技术社区首发签约文章,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)操作时,这里会被填入真实的外部调用的绝对地址。

image.png

通过这一种方式,linux已经能在符号地址绑定这块得到了较好的性能,但是GOT表的生成也是链接过程的一个消耗,所以linux又提供了一种叫延迟绑定的手段,只有在函数真正用到的时候,才进行函数的地址定位。我们来了解一下步骤:

image.png

当我们进行链接的时候,链接器不进行函数符号的寻址,而是通过一条push指令作为替代品(消耗非常小),push指令的入参可以是rel.plt等重定位表相关的下标,在运行时才进行真正的函数地址寻址。

image.png

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

PLT Hook

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

image.png

以上面的例子出发,我们需要对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去查看 image.png 就这样我们找到了偏移地址 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性能优化线程相关的优化就到此结束,恭喜踏入下一篇!