当前位置:首页 > 谈天说地

Android性能优化之plt hook与native线程监控详解

34资源网2022-09-19504

背景

我们在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线程监控的资料请关注萬仟网其它相关文章!

看完文章,还可以扫描下面的二维码下载快手极速版领4元红包

快手极速版二维码

快手极速版新人见面礼

除了扫码领红包之外,大家还可以在快手极速版做签到,看视频,做任务,参与抽奖,邀请好友赚钱)。

邀请两个好友奖最高196元,如下图所示:

快手极速版邀请好友奖励

扫描二维码推送至手机访问。

版权声明:本文由34楼发布,如需转载请注明出处。

本文链接:https://www.34l.com/post/22279.html

分享给朋友:

相关文章

阳台盆栽种什么比较好养?这九种植物非常适合阳台种植

阳台盆栽种什么比较好养?这九种植物非常适合阳台种植

阳台盆栽种什么比较好养?在阳台养花种菜的小伙伴,肯定知道下面这9种植物,今天我就选出来9种有花,也有水果,个人认为好养,而且下面还有三种水果还比较好吃,家里到时候不用买水果了。…

情浓时,海誓山盟;情淡时,形同陌路

情浓时,海誓山盟;情淡时,形同陌路

1、如果一个人影响到了你的情绪,你的焦点应该放在控制自己的情绪上,而不是影响你情绪的人身上。只有这样,才能真正自信起来。…

最傻的六种员工离职理由,希望你不是其中一员

最傻的六种员工离职理由,希望你不是其中一员

春节后,今年的中国员工离职率应该是最低的。但是职场上,还是会有很多员工提离职。离职原因各种各样, 不开心,不舒服,工资少,学不到东西等等。那么,最傻的六种员工离职是哪些呢?…

lenovo手机网上哪里买(联想旗舰店官网商城)

lenovo手机网上哪里买(联想旗舰店官网商城)

昨晚联想拯救者电竞手机 2 Pro 正式发布,搭载骁龙 888 旗舰芯片、八指操控体系,配备 6.92 英寸 AMOLED 144Hz 三星定制电竞无孔屏幕,5500mAh 容量电池,堪称 “堆料狂魔”,这款手机于今日 10:00 正式开售…

怎么用电脑开wifi热点(笔记本电脑连接wifi教程)

很多的时候我们手里不会随时准备着路由器,所以经常会导致手机等移动设备缺乏wifi使用。其实,一根网线、一台Win10笔记本就能轻松设置Wifi共享,并且借助Win10自带的移动Wifi热点就能轻松实现Wifi共享,提供给其他笔记本或手机、平…

已覆盖70%前十大快递/快运客户,商用车后市场玩家「大车队长」眼中的轮胎“生命力”

已覆盖70%前十大快递/快运客户,商用车后市场玩家「大车队长」眼中的轮胎“生命力”

2020年,商用车后市场头部创业公司「大车队长」正式完成了数千万元人民币A轮融资,由经纬中国领投。融资后的一年里,大车队长成长迅速,还发布了全新的“5113”战略,即5年服务100万台车、1000万个轮位、完成300亿元营收。 截至目前,大…