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

作者:Pika 时间:2021-09-10 22:06:01 

背景

我们在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)操作时,这里会被填入真实的外部调用的绝对地址。

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需要以下几步。

定位出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的匹配项即可

格式如下

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线程监控的资料请关注脚本之家其它相关文章!

来源:https://juejin.cn/post/7143945091366223903

标签:Android,plt,hook,native,线程监控
0
投稿

猜你喜欢

  • c# 基于任务的异步编程模式(TAP)的异常处理

    2023-08-03 15:44:06
  • Unity中C#和Java的相互调用实例代码

    2022-02-28 13:40:53
  • 详谈java中File类getPath()、getAbsolutePath()、getCanonical的区别

    2022-08-18 19:21:44
  • c#基于Win32Api实现返回Windows桌面功能

    2022-11-21 15:29:51
  • C# 向Word中设置/更改文本方向的方法(两种)

    2023-01-12 21:37:33
  • Unity中的RegisterPlugins实用案例深入解析

    2022-04-02 10:14:41
  • Java表单重复提交的避免方法

    2022-03-03 10:06:06
  • C#简单实现显示中文格式星期几的方法

    2021-09-08 12:27:05
  • 解决Android软键盘弹出覆盖h5页面输入框问题

    2023-06-19 11:33:24
  • Java详细分析LCN框架分布式事务

    2022-10-17 15:49:08
  • c#爬虫爬取京东的商品信息

    2022-12-03 14:38:11
  • Java实现Flappy Bird游戏源码

    2022-11-02 16:55:29
  • SpringBoot如何在普通类加载Spring容器

    2023-10-06 03:56:27
  • SpringBoot集成Swagger2的方法

    2023-11-26 13:15:42
  • jvm调优的几种场景(小结)

    2023-04-11 18:37:04
  • C#图像处理之图像均值方差计算的方法

    2023-12-10 17:35:33
  • 利用Spring Boot和JPA创建GraphQL API

    2023-04-01 07:11:41
  • 详解Java利用同步块synchronized()保证并发安全

    2021-09-12 12:15:07
  • springboot项目启动,但是访问报404错误的问题

    2022-09-21 10:30:10
  • WPF实现动画效果(六)之路径动画

    2022-02-05 01:42:15
  • asp之家 软件编程 m.aspxhome.com