起源
??公司項目里帅腌,開發(fā)中長期在使用DiDi的DoraemonKit庫里的一些調(diào)試小工具, 剛好最近需要對一些代碼耗時做分析懒豹,想到了,其中包含的DoraemonTimeProfiler類可以通過動態(tài)方式,無侵入的對函數(shù)的耗時進行統(tǒng)計竹椒,一時好奇苹熏,想看看具體是如何做到的勒葱。
基本邏輯
??打開源碼菇夸,其實這部分代碼并不多共苛,從邏輯上非常易于理解判没。我們知道OC方法的執(zhí)行,本質(zhì)上就是通過調(diào)用objc_msgSend函數(shù)來發(fā)送消息隅茎,所以基本思路是對objc_msgSend方法進行hook澄峰,通過對objc_msgSend方法執(zhí)行開始前和結(jié)束后分別記錄時間,再進行計算就可以得出方法的調(diào)用時長辟犀。
??但由于為objc_msgSend是基于匯編實現(xiàn)的俏竞,在編譯期間就決定了函數(shù)地址,所以無法通過動態(tài)的method_swizzle來進行替換堂竟。這里采用了fishhook庫動態(tài)的對objc_msgSend進行了hook魂毁,
fishhook簡單來講,就是利用動態(tài)庫的共享緩存庫在運行時進行綁定的原理出嘹,把我們的hook代碼注入進去席楚,具體的原理,不是這篇文章的重點疚漆,就不做分析了酣胀。
fishhook對objc_msgSend進行hook的代碼如下:
void dtp_hook_begin(void) {
_call_record_enabled = true;
pthread_key_create(&_thread_key, &release_thread_call_stack);
doraemon_rebind_symbols((struct doraemon_rebinding[1]){"objc_msgSend", (void *)hook_objc_msgSend, (void **)&orig_objc_msgSend},1);
}
void dtp_hook_end(void) {
_call_record_enabled =false;
doraemon_rebind_symbols((struct doraemon_rebinding[1]){"objc_msgSend", (void*)orig_objc_msgSend,NULL},1);
}
自定義objc_msgSend如何實現(xiàn)
??上面分析了如何對objc_msgSend進行hook,接下來就到了本文的重點娶聘,這個hook_objc_msgSend方法闻镶,應該如何編寫來進行時間統(tǒng)計呢?
DoraemonTimeProfiler給出的答案是這樣的:
__attribute__ ((__naked__))
static void hook_objc_msgSend() {
//before之前保存objc_msgSend的參數(shù)
save()
//將objc_msgSend執(zhí)行的下一個函數(shù)地址傳遞給before_objc_msgSend的第二個參數(shù)x0 self, x1 _cmd, x2: lr address
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");
// 執(zhí)行before_objc_msgSend blr 除了從指定寄存器讀取新的 PC 值外效果和 bl 一...
call(blr, &before_objc_msgSend)
// 恢復objc_msgSend參數(shù)丸升,并執(zhí)行
load()
call(blr, orig_objc_msgSend)
//after之前保存objc_msgSend執(zhí)行完成的參數(shù)
save()
//調(diào)用 after_objc_msgSend
call(blr, &after_objc_msgSend)
//將after_objc_msgSend返回的參數(shù)放入lr,恢復調(diào)用before_objc_msgSend前的lr地址
// x0 是整數(shù)/指針args的第一個arg傳遞寄存器 x0 是整數(shù)/指針值的(第一個)返回值寄存器
__asm volatile ("mov lr, x0\n");
//恢復objc_msgSend執(zhí)行完成的參數(shù)
load()
//方法結(jié)束,繼續(xù)執(zhí)行l(wèi)r
ret()
}
代碼不長铆农,我們來一步一步進行分析。
首先這里為什么實現(xiàn)都是C和匯編狡耻,是因為objc_msgSend本身是基于匯編進行實現(xiàn)墩剖,所以hook的方法對于objc_msgSend相關(guān)的部分都必須是基于匯編來實現(xiàn)。
__attribute__ ((__naked__))
這里聲明是告訴編譯器在函數(shù)調(diào)用的時候不使用棧保存參數(shù)信息夷狰,由于objc_msgSend本身使用了該修飾符岭皂,所以這里我們也必須同樣的進行修飾。
這里簡單介紹下:
在arm64匯編中沼头,在小于9個參數(shù)時爷绘,通過x0-x8寄存器對參數(shù)進行保存书劝,當超過時,會通過椡林粒空間進行存儲购对,objc_msgSend使用此聲明猜測可能是基于性能考慮。
save()
保存objc_msgSend本身的方法棧信息陶因。因為在objc_msgSend方法執(zhí)行前骡苞,我們會執(zhí)行方法before_objc_msgSend,從而對寄存器造成污染楷扬,為了確保能夠正確的執(zhí)行objc_msgSend方法解幽,需要對當前的寄存器狀態(tài)進行保存,等我們的方法執(zhí)行完畢后毅否,再對寄存器進行恢復亚铁,從而保證OC方法的正確執(zhí)行。
call(blr, &before_objc_msgSend)
通過blr匯編語句螟加,執(zhí)行before_objc_msgSend方法徘溢,從而在OC方法執(zhí)行前,記錄方法開始的時間捆探。
before_objc_msgSend的實現(xiàn)簡單描述如下:
before_objc_msgSend {
獲取當前堆棧信息
更新記錄的開始執(zhí)行時間到堆棧信息中
}
具體實現(xiàn)代碼參考源碼:
static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr)
load()
恢復我們通過save()保存的objc_msgSend方法信息然爆。
call(blr, orig_objc_msgSend)
調(diào)用原始的objc_msgSend,開始OC方法的執(zhí)行黍图。
save()
(此時方法執(zhí)行完畢)
和之前的save()原因一樣曾雕,這里是因為objc_msgSend方法執(zhí)行完畢后,我們需要記錄結(jié)束時間助被,會對寄存器造成污染剖张,所以需要在after_objc_msgSend方法執(zhí)行前,對寄存器狀態(tài)進行保存揩环。
call(blr, &after_objc_msgSend)
通過匯編語句搔弄,調(diào)用after_objc_msgSend方法,進行方法的耗時的計算丰滑,并將函數(shù)信息和耗時存儲到dtp_records中顾犹。
after_objc_msgSend的實現(xiàn)簡單描述如下:
after_objc_msgSend {
找到緩存中當前堆棧信息
讀取之前緩存的執(zhí)行時間
計算出方法的耗時
生成一條記錄記錄方法信息和耗時
更新記錄到dtp_records中
}
具體實現(xiàn)代碼參考源碼:
static inline uintptr_t pop_call_record()
__asm volatile ("mov lr, x0\n");
恢復寄存器x0的內(nèi)容到lr中,還原h(huán)ook_objc_msgSend的lr寄存器褒墨。
這里需要講一下炫刷,為什么x0的值是lr寄存器的內(nèi)容?
首先我們要理解x0在函數(shù)結(jié)束時郁妈,會作為返回值寄存器浑玛,存儲函數(shù)的返回內(nèi)容, 而after_objc_msgSend的返回值為pRecord->lr,也就是hook_objc_msgSend原始的lr的內(nèi)容噩咪。
pRecord->lr為什么是原始的lr寄存器的內(nèi)容呢顾彰?
這里我們結(jié)合before_objc_msgSend和after_objc_msgSend的源碼來仔細分析:
void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
push_call_record(self, object_getClass(self), _cmd, lr);
}
static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) {
thread_call_stack *cs = get_thread_call_stack();
if (cs) {
...
thread_call_record *newRecord = &cs->stack[nextIndex];
...
newRecord->lr = lr; // 此時存儲了lr到thread_call_record中
...
}
}
uintptr_t after_objc_msgSend() {
return pop_call_record();
}
static inline uintptr_t pop_call_record() {
thread_call_record *pRecord = &cs->stack[nextIndex];
...
return pRecord->lr; // 返回before_objc_msgSend中存儲的lr
}
在before_objc_msgSend時, "lr參數(shù)"被存入了thread_call_record對象中失晴。
那么這個"lr參數(shù)"是什么呢?
我們發(fā)現(xiàn)在before_objc_msgSend執(zhí)行前拘央,有一行匯編語句:__asm volatile ("mov x2, lr\n");
,
它將lr寄存器的內(nèi)容移到了x2寄存器中书在,而x2寄存器接下來做了什么呢灰伟?在arm64架構(gòu)下,x2寄存器對應的是before_objc_msgSend方法的第三個入?yún)ⅲ╱intptr_t lr)儒旬。
因此寄存器lr的內(nèi)容被存入了thread_call_record對象中栏账。而在after_objc_msgSend時,pRecord->lr對應的即是thread_call_record所保存的lr寄存器的原始內(nèi)容栈源。
load()
寄存器還原到objc_msgSend剛執(zhí)行結(jié)束后的狀態(tài)挡爵。
ret()
源碼:#define ret() __asm volatile ("ret\n");
匯編里的return語句,代表當前方法內(nèi)容執(zhí)行完畢甚垦,跳出方法塊茶鹃,繼續(xù)執(zhí)行。
這時會將lr寄存器讀取到pc寄存器中艰亮,跳出hook_objc_msgSend函數(shù)繼續(xù)執(zhí)行闭翩,這也是為什么執(zhí)行ret前要還原lr寄存器內(nèi)容的原因。
整理的流程如圖所示:
重要的方法
執(zhí)行過程中迄埃,有幾個重要的方法疗韵,這里著重分析下。
save()
源碼:
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");
save主要用戶緩存當前方法的寄存器狀態(tài)侄非,所以將x0-x8的寄存器緩存到了棧上蕉汪。
之所以用到了x9寄存器,是因為arm棧是按照16字節(jié)對齊逞怨,而由于寄存器大小固定為8字節(jié)者疤,所以需要x9來進行字節(jié)補齊。
這里簡單介紹幾個知識:
- sp:棧頂寄存器骇钦,寄存器移動到棧上都需要依賴sp寄存器宛渐。
-
stp x8, x9, [sp, #-16]!
匯編語句屬于精簡的語句,省去了棧拉伸的代碼眯搭。實際上等同于以下指令:
sub sp,sp,#0x16
stp x8,x9,[sp]
load()
源碼:
#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n");
load和save是對應關(guān)系窥翩,用于恢復save存儲的寄存器信息。
call()
源碼:
#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");
call方法的參數(shù)b,在當前環(huán)境下鳞仙,都是用了blr跳轉(zhuǎn)指令進行方法執(zhí)行寇蚊。
第一行和第三行是對x8寄存器進行暫存和恢復,之所以這么做棍好,是因為__asm volatile ("mov x12, %0\n" :: "r"(value));
執(zhí)行過程中仗岸,會通過x8來保存函數(shù)地址允耿,再進行跳轉(zhuǎn)。
總結(jié)
想不到簡簡單的函數(shù)耗時統(tǒng)計扒怖,卻包含了好幾個知識點:
- obj_msgSend()如何hook
- fishhook原理
- 匯編基本指令
雖然大部分都有相關(guān)的文章可以輔助理解较锡,但當自己去做的時候,還是會發(fā)現(xiàn)很多新的理解盗痒,一點點的積累一點點的進步蚂蕴。