Android性能優(yōu)化之plt?hook與native線程監(jiān)控詳解
背景
我們?cè)赼ndroid超級(jí)優(yōu)化-線程監(jiān)控與線程統(tǒng)一可以知道,我們能夠通過(guò)asm插樁的方式,進(jìn)行了線程的監(jiān)控與線程的統(tǒng)一,通過(guò)一系列的黑科技,我們能夠?qū)㈨?xiàng)目中的線程控制在一個(gè)非常可觀的水平,但是這個(gè)只局限在java層線程的控制,如果我們項(xiàng)目中存在著native庫(kù),或者存在著很多其他so庫(kù),那么native層的線程我們就沒(méi)辦法通過(guò)ASM或者其他字節(jié)碼手段去監(jiān)控了,但是并不是就沒(méi)有辦法,還有一個(gè)黑科技,就是我們的PIL Hook,目前行業(yè)上比較出名的就是xhook,和bhook了。
native 線程創(chuàng)建
了解PLT Hook之前,我們先了解一下native層常用的創(chuàng)建線程的手段,沒(méi)錯(cuò),就是pthread
int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
- __pthread_ptr:pthread_t類型的參數(shù),成功時(shí)tidp指向的內(nèi)容被設(shè)置為新創(chuàng)建線程的pthread_t
- __attr 線程的屬性
- __start_routine 執(zhí)行函數(shù),新創(chuàng)建線程從此函數(shù)開(kāi)始運(yùn)行
- __start_routine中 需要運(yùn)行的入?yún)?,如果__start_routine不需要入?yún)?,則該值為null
接下里我們用這個(gè)例子去說(shuō)明,我們?cè)贛ainActivity中設(shè)定了一個(gè)名叫threadCreate的jni調(diào)用,開(kāi)啟一個(gè)新線程,在新線程里面打印一些傳遞的數(shù)據(jù)。
libtest.so中的代碼
/* 聲明結(jié)構(gòu)體 */
struct member {
int num;
char *name;
};
/* 定義線程pthread */
static void *pthread(void *arg) {
struct member *temp;
/* 線程pthread開(kāi)始運(yùn)行 */
printf("pthread start!\n");
/* 打印傳入?yún)?shù) */
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;
/* 為結(jié)構(gòu)體變量b賦值 */
b = (struct member *) malloc(sizeof(struct member));
b->num = 10086;
b->name = "pika";
/* 創(chuàng)建線程pthread */
if ((pthread_create(&tidp, NULL, pthread, (void *) b)) == -1) {
printf("create error!\n");
}
}
通過(guò)jni方式調(diào)用的pthread,我們就沒(méi)辦法用常規(guī)手段去監(jiān)控了。所以我們才需要plt hook的方式
PLT
介紹plt hook之前,我們還是有必要了解一些前置的知識(shí)。在linux中,會(huì)存在很多地址無(wú)關(guān)的代碼。在我們的編寫(xiě)模塊中,其實(shí)會(huì)遇到很多共享對(duì)象地址沖突的問(wèn)題,如果相互依賴的對(duì)象是以絕對(duì)地址的方式存在的話,那么運(yùn)行的時(shí)候就會(huì)發(fā)生地址沖突,比如進(jìn)程A里面兩個(gè)方法都被定位到了同一個(gè)地址,所以才有了地址無(wú)關(guān)的代碼。
地址無(wú)關(guān)的代碼大多數(shù)采用運(yùn)行時(shí)基地址+編譯時(shí)定向偏移,其中基地址可以在運(yùn)行時(shí)確定,但是某個(gè)符號(hào)的運(yùn)行時(shí)地址相對(duì)于基地址來(lái)說(shuō),就可以是一個(gè)確定的偏移數(shù)值。通過(guò)這種方式,函數(shù)可以在被需要的時(shí)候再進(jìn)行綁定地址即可,在編譯時(shí)只需要記錄偏移就可以保證后期的運(yùn)行尋址的正常。這個(gè)保存偏移地址的東西,就叫做GOT表(全局偏移表),當(dāng)代碼需要引用到這個(gè)符號(hào)的時(shí)候,就可以通過(guò)GOT表間接定位到真正的地址,動(dòng)態(tài)鏈接器(linker)執(zhí)行重定位(relocate)操作時(shí),這里會(huì)被填入真實(shí)的外部調(diào)用的絕對(duì)地址。

通過(guò)這一種方式,linux已經(jīng)能在符號(hào)地址綁定這塊得到了較好的性能,但是GOT表的生成也是鏈接過(guò)程的一個(gè)消耗,所以linux又提供了一種叫延遲綁定的手段,只有在函數(shù)真正用到的時(shí)候,才進(jìn)行函數(shù)的地址定位。我們來(lái)了解一下步驟:

當(dāng)我們進(jìn)行鏈接的時(shí)候,鏈接器不進(jìn)行函數(shù)符號(hào)的尋址,而是通過(guò)一條push指令作為替代品(消耗非常?。?,push指令的入?yún)⒖梢允莚el.plt等重定位表相關(guān)的下標(biāo),在運(yùn)行時(shí)才進(jìn)行真正的函數(shù)地址尋址。

但是?。≡谖覀傾ndroid體系中,目前只有 MIPS 架構(gòu)支持 lazy binding,所以目前在android,對(duì)plt表的內(nèi)容定位就不在運(yùn)行時(shí)進(jìn)行,而是直接在鏈接時(shí)確定,未來(lái)會(huì)不會(huì)更多支持延遲綁定呢,還不確定,所以這個(gè)我們作為了解即可。
PLT Hook
我們從上面調(diào)用可以看到,plt表的調(diào)用原理,所以我們的hook點(diǎn)也很明確,如果我們想要fun1-> fun2 變成 fun1 -> fun 3的話(fun2 跟 fun3 必須是外部函數(shù),如果不是外部函數(shù)就不會(huì)生成plt表進(jìn)行跳轉(zhuǎn),因?yàn)槭潜灸K就不需要借助plt表,直接生成地址無(wú)關(guān)代碼偏移即可)

以上面的例子出發(fā),我們需要對(duì)libtest中的pthread_create進(jìn)行hook,從而采集pthread_create的數(shù)據(jù),因?yàn)槲覀儗?shí)現(xiàn)plthook需要以下幾步。
定位出pthread_create的相對(duì)偏移(上面說(shuō)過(guò)函數(shù)的真實(shí)地址是基地址+相對(duì)偏移),那么這個(gè)偏移在哪呢?我們從上面流程圖可以看到,偏移就在.rel.plt中(并不是所有偏移都在這里,重定位信息可以分布在.rel.plt, .rela.plt, .rel.dyn, .rela.dyn, .rel.android, .rela.android等多個(gè)表中,但是一般的外部調(diào)用不需要經(jīng)過(guò)全局函數(shù)跳轉(zhuǎn)都在.rel.plt表中),我們可以通過(guò)readif -r libtest.so去查看

就這樣我們找到了偏移地址 0x23f8
2.找到基地址,從前面我們可以知道,基地址是運(yùn)行時(shí)決定的,我們可以在運(yùn)行時(shí)檢索/proc/self/maps文件,在里面找到libtest.so的匹配項(xiàng)即可
格式如下
so的范圍地址 權(quán)限 基地址(重點(diǎn)關(guān)注) dev inode so名稱
3.通過(guò)基地址+偏移,我們得到了跳轉(zhuǎn)目標(biāo)函數(shù)的地址,這個(gè)時(shí)候只需要把這個(gè)地址指向的函數(shù)更改為我們自定義函數(shù)即可,地址的概念,p->自定義函數(shù)
4.雖然我們實(shí)現(xiàn)了函數(shù)替換,但是這個(gè)被替換的函數(shù)地址可能會(huì)缺少相關(guān)的讀寫(xiě)權(quán)限,導(dǎo)致出現(xiàn)讀取該地址的時(shí)候發(fā)生讀寫(xiě)異常,我們可以通過(guò)
int mprotect(void* __addr, size_t __size, int __prot);
進(jìn)行讀寫(xiě)權(quán)限的添加,addr就是當(dāng)前的地址,size就是大小,我們以當(dāng)前頁(yè)大小執(zhí)行即可(被修改權(quán)限的地址[addr, addr+len-1]),prot當(dāng)前權(quán)限枚舉
5.由于存在緩存指令的影響,我們需要消除這部分可能已經(jīng)被緩存的指令,可以通過(guò)已提供的
void __builtin___clear_cache (char *begin, char *end);
去清除指令緩存,以頁(yè)為單位。一個(gè)地址所處的頁(yè)與結(jié)束時(shí)的頁(yè)可以通過(guò)以下代碼換算
#define PAGE_START(addr) ((addr) & PAGE_MASK) #define PAGE_END(addr) (PAGE_START(addr) + PAGE_SIZE)
其中PAGE_SIZE 由宏定義,這里為 #define PAGE_SIZE 4096
通過(guò)以上步驟,我們就能夠?qū)崿F(xiàn)了我們對(duì)pthread的hook,這里給出完整的實(shí)現(xiàn)
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;
//得到真實(shí)的函數(shù)地址 可由readif -r 看到
addr = base_addr + 0x23f8;
// 添加讀寫(xiě)權(quán)限
mprotect((void *)PAGE_START(addr), PAGE_SIZE, PROT_READ | PROT_WRITE);
// 替換為函數(shù)地址
*(void **)addr = (unsigned*)&my_pthread_create;
// 清除緩存
__builtin___clear_cache(static_cast<char *>((void *) PAGE_START(addr)),
static_cast<char *>((void *) PAGE_END(addr)));
}
調(diào)用hook()后,libtest中pthread_create 就會(huì)被轉(zhuǎn)化為my_pthread_create的調(diào)用,這樣我們就實(shí)現(xiàn)了一次plt hook!
xhook bhook
上面我們hook的偏移都是基于通過(guò)readif看到的偏移地址,但是實(shí)際上這個(gè)地址都用readif可能會(huì)非常不方便,而且我們也只是檢索了rel.plt表,實(shí)際上會(huì)存在多個(gè)復(fù)雜的跳轉(zhuǎn)現(xiàn)象時(shí),就需要檢索所有的重定位表。但是沒(méi)關(guān)系,這些xhook bhook都幫我們做了,只需要調(diào)用封裝好的方法即可,我們這里就不結(jié)束api了,感興趣讀者可自行觀看readme
plt hook總結(jié)
最后我們來(lái)總結(jié)一下plt hook相關(guān)優(yōu)缺點(diǎn)
| 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|
| 可操作性強(qiáng),原理簡(jiǎn)單易用 | 局限性 plt hook 只能作用在外部函數(shù),即調(diào)用生成重定位表的方法中 |
| 適配成本低,只需要hook 相關(guān)重定位表即可,由elf文件保證其規(guī)范 |
當(dāng)前,為了解決plt hook的局限性問(wèn)題,同時(shí)也有對(duì)inline hook 的開(kāi)源框架,但是inline hook存在適配成本較高穩(wěn)定性較差的問(wèn)題,一直沒(méi)有得到非常大的推廣,一般只在特殊場(chǎng)景下的使用,這里普及一下并不詳細(xì)展開(kāi)說(shuō)明!看完這里讀者朋友們應(yīng)該能夠理解plt hook在pthread_create的應(yīng)用,由于里面涉及了一些elf文件的內(nèi)容,我們先粗略了解,必要的時(shí)候需要進(jìn)一步學(xué)習(xí)查詢即可,我們?cè)谝院髸?huì)推出elf文件相關(guān)的介紹文章,歡迎繼續(xù)關(guān)注!到這里,android性能優(yōu)化線程相關(guān)的優(yōu)化就到此結(jié)束,更多關(guān)于Android plt hook native線程監(jiān)控的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 權(quán)限(permission)整理
本文主要介紹Android 權(quán)限的整理,在開(kāi)發(fā)Android應(yīng)用的時(shí)候,根據(jù)需求的不同,會(huì)用到不同的權(quán)限,這里整理了很多,有需要的同學(xué)可以參考下2016-07-07
詳解Android XML中引用自定義內(nèi)部類view的四個(gè)why
本篇文章主要介紹了詳解Android XML中引用自定義內(nèi)部類view,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。2016-12-12
Android工具欄頂出轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的實(shí)現(xiàn)方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Android工具欄頂出轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)各位Android開(kāi)發(fā)者們具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09
完美解決Android客戶端RSA解密部分亂碼的問(wèn)題
下面小編就為大家分享一篇完美解決Android客戶端RSA解密部分亂碼的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03
android 版本檢測(cè) Android程序的版本檢測(cè)與更新實(shí)現(xiàn)介紹
做個(gè)網(wǎng)站的安卓客戶端,用戶安裝到自己手機(jī)上,如果我出了新版本怎么辦呢?要有版本更新功能,感興趣的朋友可以了解下2013-01-01
Android使用TabLayout+Fragment實(shí)現(xiàn)頂部選項(xiàng)卡
本文通過(guò)實(shí)例代碼給大家介紹了Android使用TabLayout+Fragment實(shí)現(xiàn)頂部選項(xiàng)卡功能,包括TabLyout的使用,感興趣的朋友參考下本文吧2017-05-05

