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

时间: 2022-09-15

Android&nbsp;线程优化知识点学习

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

捕获与解析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原生实现多线程断点续传功能

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

Android&nbsp;WebView开发之WebView与Native交互

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

Android程序开发之WebView使用总结

前言: 今天修改项目中一个有关WebView使用的bug,激起了我总结WebView的动机,今天抽空做个总结. 使用场景: 1.)添加权限 <uses-permission android:name="android.permission.INTERNET" /> 2.)布局文件 <WebView android:id="@+id/webView" android:layout_width="match_parent" andr

android应用开发之spinner控件的简单使用

Android的控件有很多种,其中就有一个Spinner的控件,这个控件其实就是一个下拉显示列表.Spinner是位于 android.widget包下的,每次只显示用户选中的元素,当用户再次点击时,会弹出选择列表供用户选择,而选择列表中的元素同样来自适配器.Spinner是View类的一个子类. 先看spinner的效果图: 代码: MainActivity package com.mecury.spinnertest; import java.util.ArrayList; import a

Android编程开发之TextView控件用法(2种方法)

本文实例讲述了Android编程开发之TextView控件用法.分享给大家供大家参考,具体如下: 这里我们会讲讲常用控件的使用. 在今后的大多数章节里面也是一样的,我们会具体的说说某些控件的用法.因为只要把这些控件组合在一起它们就是一个应用了. 好吧我们直接看看这个控件怎么用. 细心的同学会发现,其实这个控件的内容是定义在values文件夹里面的strings.xml中的. 那么我们只需要给它加一段代码: 复制代码 代码如下: <string name="test">Wel

Android编程开发之seekBar采用handler消息处理操作的方法

本文实例讲述了Android编程开发之seekBar采用handler消息处理操作的方法.分享给大家供大家参考,具体如下: 该案例简单实现进度条可走,可拖拽的功能,下面请看源码: 布局文件: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout

Android编程开发之TextView单击链接弹出Activity的方法

本文实例讲述了Android编程开发之TextView单击链接弹出Activity的方法.分享给大家供大家参考,具体如下: 话不多说直接上码: 核心源码: package com.example.textview4; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.text.SpannableString; import android.tex

Android编程开发之EditText实现输入QQ表情图像的方法

本文实例讲述了Android编程开发之EditText实现输入QQ表情图像的方法.分享给大家供大家参考,具体如下: 实现效果如下: 将QQ表情图像放到res下的drawable-hdpi文件夹下: 布局文件: <EditText android:id="@+id/edittext" android:layout_width="fill_parent" android:layout_height="wrap_content" android:

Android编程开发之在Canvas中利用Path绘制基本图形(圆形,矩形,椭圆,三角形等)

本文实例讲述了Android编程开发之在Canvas中利用Path绘制基本图形的方法.分享给大家供大家参考,具体如下: 在Android中绘制基本的集合图形,本程序就是自定义一个View组件,程序重写该View组件的onDraw(Canvase)方法,然后在该Canvas上绘制大量的基本的集合图形. 直接上代码: 1.自定义的View组件代码: package com.infy.configuration; import android.content.Context; import andro

Android编程开发之EditText中不输入特定字符会显示相关提示信息的方法

本文实例讲述了Android编程开发之EditText中不输入特定字符会显示相关提示信息的方法.分享给大家供大家参考,具体如下: 先看效果图: 源码如下: 布局文件: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="

Android编程开发之RadioGroup用法实例

本文实例讲述了Android编程开发之RadioGroup用法.分享给大家供大家参考,具体如下: RadioGroup 有时候比较有用.主要特征是给用户提供多选一机制. MainActivity.java package com.example.lesson16_radio; import android.app.Activity; import android.os.Bundle; import android.widget.RadioButton; import android.widget

Android程序开发之ListView 与PopupWindow实现从左向右滑动删除功能

文章实现的功能是:在ListView的Item上从右向左滑时,出现删除按钮,点击删除按钮把Item删除. 看过文章后,感觉没有必要把dispatchTouchEvent()和onTouchEvent()两个方法都重写,只要重写onTouchEvent就好了.于是对代码作了一些调整: public class MyListView extends ListView { private static final String TAG = "MyListView"; private int