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

目录
  • 背景
  • native 线程创建
  • PLT
  • PLT Hook
  • xhook bhook
  • plt hook总结

背景

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

(0)

相关推荐

  • 捕获与解析Android NativeCrash

    目录 一.NE 简介 1.1.so 组成 1.2.查看 so 状态 1.3.获取 strip 和未被 strip 的 so 二.NE 捕获与解析 2.1.logcat捕获 2.2.通过DropBox日志解析--适用于系统应用 2.3.通过BreakPad捕获解析--适用于所有应用 2.3.1.BreakPad的实现功能 2.3.2.BreakPad的捕获原理 2.3.3.解析dump文件 2.3.4.获取崩溃堆栈 三.so符号表的提取 3.1.提取 so 的符号表 3.2.符号表分析 3.2.1

  • Android nativePollOnce函数解析

    nativePollOnce的实现函数是android_os_MessageQueue_nativePollOnce,代码如下: android_os_MessageQueue.cpp static void android_os_MessageQueue_nativePollOnce(JNIEnv*env, jobject obj, jintptr, jint timeoutMillis) NativeMessageQueue*nativeMessageQueue = reinterpret_

  • Android WebView开发之WebView与Native交互

    目录 前言 一.JS调用Native的三种方式 完整源码 二.Native调用WebView的两种方案 完整源码 前言 附GitHub源码:WebViewExplore 一.JS调用Native的三种方式 1.通过WebView的addJavascriptInterface进行对象映射 需要注意的是这种调用方式,如果你的 minSdkVersion <=16那么需要考虑到4.2之前的漏洞问题. mWebView.addJavascriptInterface(new JsCallAndroidIn

  • android原生实现多线程断点续传功能

    本文实例为大家分享了android实现多线程断点续传功能的具体代码,供大家参考,具体内容如下 需求描述: 输入一个下载地址,和要启动的线程数量,点击下载 利用多线程将文件下载到手机端,支持 断点续传. 在前两章的java 多线程的从基础上进行 效果展示 示例代码: 布局 activity_main.xml <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.Const

  • Android 线程优化知识点学习

    目录 前言 一.线程调度原理解析 线程调度的原理 线程调度模型 Android 的线程调度 线程调度小结 二.Android 异步方式汇总 Thread HandlerThread IntentService AsyncTask 线程池 RxJava 三.Android线程优化实战 线程使用准则 线程池优化实战 四.定位线程创建者 如何确定线程创建者 Epic实战 五.优雅实现线程收敛 线程收敛常规方案 基础库如何使用线程 基础库优雅使用线程 前言 在实际项目开发中会频繁的用到线程,线程使用起来

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

    目录 背景 native 线程创建 PLT PLT Hook xhook bhook plt hook总结 背景 我们在android超级优化-线程监控与线程统一可以知道,我们能够通过asm插桩的方式,进行了线程的监控与线程的统一,通过一系列的黑科技,我们能够将项目中的线程控制在一个非常可观的水平,但是这个只局限在java层线程的控制,如果我们项目中存在着native库,或者存在着很多其他so库,那么native层的线程我们就没办法通过ASM或者其他字节码手段去监控了,但是并不是就没有办法,还有

  • Android性能优化之RecyclerView分页加载组件功能详解

    目录 引言 1 分页加载组件 1.1 功能定制 1.2 手写分页列表 1.3 生命周期管理 2 github 引言 在Android应用中,列表有着举足轻重的地位,几乎所有的应用都有列表的身影,但是对于列表的交互体验一直是一个大问题.在性能比较好的设备上,列表滑动几乎看不出任何卡顿,但是放在低端机上,卡顿会比较明显,而且列表中经常会伴随图片的加载,卡顿会更加严重,因此本章从手写分页加载组件入手,并对列表卡顿做出对应的优化 1 分页加载组件 为什么要分页加载,通常列表数据存储在服务端会超过100条

  • Android性能优化之利用Rxlifecycle解决RxJava内存泄漏详解

    前言: 其实RxJava引起的内存泄漏是我无意中发现了,本来是想了解Retrofit与RxJava相结合中是如何通过适配器模式解决的,结果却发现了RxJava是会引起内存泄漏的,所有想着查找一下资料学习一下如何解决RxJava引起的内存泄漏,就查到了利用Rxlifecycle开源框架可以解决,今天周末就来学习一下如何使用Rxlifecycle. 引用泄漏的背景: RxJava作为一种响应式编程框架,是目前编程界网红,可谓是家喻户晓,其简洁的编码风格.易用易读的链式方法调用.强大的异步支持等使得R

  • Android性能优化之JVMTI与内存分配

    目录 前言 JVMTI JVMTI 简介: native层开启jvmti 前置准备 复写Agent 开启jvmtiCapabilities 设置jvmtiEventCallbacks 开启监听 java层开启agent 验证分配数据 总结 前言 内存治理一直是每个开发者最关心的问题,我们在日常开发中会遇到各种各样的内存问题,比如OOM,内存泄露,内存抖动等等,这些问题都有以下共性: 难发现,内存问题一般很难发现,业务开发中关系系数更少 治理困难,内存问题治理困难,比如oom,往往堆栈只是压死骆驼

  • 浅谈Android性能优化之内存优化

    1.Android内存管理机制 1.1 Java内存分配模型 先上一张JVM将内存划分区域的图 程序计数器:存储当前线程执行目标方法执行到第几行. 栈内存:Java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法.栈帧包括局部标量表, 操作数栈. 本地方法栈:本地方法栈主要是为执行本地方法服务的.而Java栈是为执行Java方法服务的. 方法区:该区域被线程共享.主要存储每个类的信息(类名,方法信息,字段信息等).静态变量,常量,以及编译器编译后的代码等. 堆:Java中的堆是被线程共享的,

  • Android 性能优化系列之bitmap图片优化

    背景 Android开发中,加载图片过多.过大很容易引起OutOfMemoryError异常,即我们常见的内存溢出.因为Android对单个应用施加内存限制,默认分配的内存只有几M(具体视不同系统而定).而载入的图片如果是JPG之类的压缩格式(JPG支持最高级别的压缩,不过该压缩是有损的),在内存中展开会占用大量的内存空间,也就容易形成内存溢出.那么高效的加载Bitmap是很重要的事情.Bitmap在Android中指的是一张图片,图片的格式有.jpg .jpg .webp 等常见的格式. 如何

  • Android性能优化之图片大小,尺寸压缩综合解决方案

    目录 前言 常见的图片压缩方法 质量压缩 尺寸压缩 libjpeg 图片压缩流程 总结 前言 在Android中我们经常会遇到图片压缩的场景,比如给服务端上传图片,包括个人信息的用户头像,有时候人脸识别也需要捕获图片等等.这种情况下,我们都需要对图片做一定的处理,比如大小,尺寸等的压缩. 常见的图片压缩方法 质量压缩 尺寸压缩 libjpeg 质量压缩 首先我们要介绍一个api--Bitmap.compress() @WorkerThread public boolean compress(Co

  • Android性能优化全局异常处理详情

    目录 前言 1 UncaughtExceptionHandler 1.1 替代Android异常机制 1.2 可选择的异常处理 2 日志上传 2.1 日志收集 2.2 日志存储 3 策略设计模式实现上传功能 前言 异常崩溃,是Android项目中一项比较棘手的问题,即便做了很多的try - catch处理,也不能保证上线不会崩,而且一旦出现崩溃,就会出现下图的弹窗,xx应用停止运行了,这种体验对用户来说是非常差的,因此已经很明显地提示,我们做的app崩溃了. 像现在企业应用,有的在发生崩溃的时候

  • Android 性能优化实现全量编译提速的黑科技

    目录 一.背景描述 二.效果展示 2.1.测试项目介绍 三.思路问题分析与模块搭建: 3.1.思路问题分析 3.2.模块搭建 四.问题解决与实 编译流程启动,需要找到哪一个 module做了修改 module 依赖关系获取 module 依赖关系 project 替换成 aar 技术方案 hook 编译流程 五.一天一个小惊喜( bug 较多) 5.1 output 没有打包出 aar 5.2 发现运行起来后存在多个 jar 包重复问题 5.3 发现 aar/jar 存在多种依赖方式 5.4 发

  • Android性能优化之弱网优化详解

    目录 弱网优化 1.Serializable原理 1.1 分析过程 1.2 Serializable接口 1.3 ObjectOutputStream 1.4 序列化后二进制文件的一点解读 1.5 常见的集合类的序列化问题 1.5.1 HashMap 1.5.2 ArrayList 2.Parcelable 2.1 Parcel的简介 2.2 Parcelable的三大过程介绍(序列化.反序列化.描述) 2.2.1 描述 2.2.2 序列化 2.2.3 反序列化 2.3 Parcelable的实

随机推荐