若是有這麼一個需求,要監聽全部OC方法的耗時時間,咱們要如何實現?本文將描述如何利用 fishhook 去攔截底層的 objc_msgSend 實現,從而達到監控全部 OC 方法的目的。html
Ps: 本文不涉及 objc_msgSend 源碼解析,只專一於 Hook及耗時統計。對其底層實現有興趣的能夠查看 arm64下Objc_msgSend的實現註釋 ,已詳細註釋了每句彙編代碼。git
在開始以前,須要先了解些基礎知識,有便於後續的閱讀。github
fishhook是facebook開源的老牌Hook框架,相比於利用消息轉發機制實現 Method Swizzle,其在 dylib 連接 Mach-O 時,更改動態綁定地址,能實現系統C/C++函數的Hook,具體可查看筆者以前寫的 Hook利器之fishhook。objective-c
因爲 Objc_msgSend 使用匯編實現,因此咱們須要學習些會用到的彙編指令,才能清晰的知道每一步在作什麼。編程
arm64 有64位處理器,能同時處理64位數據,其每條指令固定長度爲32bit,即4個字節。api
x0-x30數據結構
一般 x0 – x7 分別會存放方法的前 8 個參數,若是參數個數超過了8個,多餘的參數會存在棧上,新方法會經過棧來讀取。框架
當方法有返回值時,執行結束會將結果放在 x0 上,若是方法返回值是一個較大的數據結構時,結果則會存在 x8 執行的地址上。編程語言
x29 又稱爲FP,用於保存棧底地址。函數
x30 寄存器又稱爲LR,用於保存要執行的下一條指令,後面咱們 Hook 時須要屢次存取該寄存器。
w0-w30 表示訪問其低32位寄存器
SP(x30)
棧寄存器,在任意時刻會保存咱們棧頂的地址,後面咱們會經過偏移它來暫存參數。
PC(x31)
存放當前執行的指令的地址,不可被修改。
SPRs
SPRs是狀態寄存器,用於存放程序運行中一些狀態標識。不一樣於編程語言裏面的if else.在彙編中就須要根據狀態寄存器中的一些狀態來控制分支的執行。狀態寄存器又分爲 The Current Program Status Register (CPSR) 和 The Saved Program Status Registers (SPSRs)。 通常都是使用 CPSR, 當發生異常時, CPSR 會存入 SPSR 。當異常恢復,再拷貝 CPSR。瞭解便可。
指令 | 做用 |
---|---|
mov | 用寄存器之間的賦值 |
str、stp | 存儲指令,用於將一個/一對寄存器的數據寫入內存中 |
ldr、ldp | 加載指令,用於從內存讀取一個/一對數據到寄存器 |
b、bl、blr | 跳轉指令,分別是不帶返回、帶返回、帶返回並指定pc |
ret | 子程序返回指令,返回地址默認保存在LR(X30) |
另外還有ADD、SUB、AND、CBZ等指令,在objc_msgSend源碼裏會用到,這裏不涉及就不具體闡述了,有興趣的可搜下arm64指令集。
在Xcode編寫彙編須要用到 GCC內嵌彙編,有興趣能夠看下 ARM GCC內嵌, 僅作了解。
設計思路: 當咱們要統計函數的耗時,最直接的方式就是記錄執行函數前和執行函數後的時間,差值就是所消耗時間。
因爲存在多層級 objc_msgSend 調用,因此須要涉及一個調用棧來保存調用層級和起始時間。調用before_objc_msgSend時,都將最新的調用指令進行入棧操做,記錄當前時間和調用層級,調用after_objc_msgSend時取出棧頂元素,便可獲得方法及對應的耗時。
//用於記錄方法
typedef struct {
Class cls;
SEL sel;
uint64_t time;
uintptr_t lr; // lr寄存器內的地址值
} MethodRecord;
//主線程方法調用棧
typedef struct {
MethodRecord *stack;
int allocCount; //棧內最大可放元素個數
int index; //當前方法的層級
bool isMainThread;
} ThreadMethodStack;
static id (*orgin_objc_msgSend)(id, SEL, ...);
static pthread_key_t threadStackKey;
static YEThreadCallRecord *threadCallRecords = NULL;
static int recordsCurrentCount;
static int recordsAllocCount;
static int maxDepth = 3; // 設置記錄的最大層級
static uint64_t minConsumeTime = 1000; // 設置記錄的最小耗時
複製代碼
定義兩個結構體來構成咱們的調用棧。
void startMonitor(void) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
pthread_key_create(&threadStackKey, cleanThreadStack);
struct rebinding rebindObjc_msgSend;
rebindObjc_msgSend.name = "objc_msgSend";
rebindObjc_msgSend.replacement = hook_objc_msgSend;
rebindObjc_msgSend.replaced = (void *)&orgin_objc_msgSend;
struct rebinding rebs[1] = {rebindObjc_msgSend};
rebind_symbols(rebs, 1);
});
}
// 線程私有數據的清理函數
void cleanThreadStack(void *ptr) {
if (ptr != NULL) {
ThreadMethodStack *threadStack = (ThreadMethodStack *)ptr;
if (threadStack->stack) {
free(threadStack->stack);
}
free(threadStack);
}
}
// 獲取當前線程的調用棧
ThreadMethodStack* getThreadMethodStack() {
ThreadMethodStack *tms = (ThreadMethodStack *)pthread_getspecific(threadStackKey);
if (tms == NULL) {
tms = (ThreadMethodStack *)malloc(sizeof(ThreadMethodStack));
tms->stack = (MethodRecord *)calloc(128, sizeof(MethodRecord));
tms->allocLength = 64;
tms->index = -1;
tms->isMainThread = pthread_main_np();
pthread_setspecific(threadStackKey, tms);
}
return tms;
}
複製代碼
因爲 objc_msgSend 會在多個線程被調用,因此須要讓保證當前線程的調用棧不被其餘線程修改,這時就用到了 pthread_key_create
線程私有數據概念,關於這部份內容可查看筆者以前的文章 什麼是線程私有數據 。
利用 fishhook 提供的 api 將 objc_msgSend 替換成咱們的 hook_objc_msgSend, 並建立一個key爲 _thread_key 線程私有數據方便後續存放調用棧。
void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
ThreadMethodStack *tms = getThreadMethodStack();
if (tms) {
int nextIndex = (++tms->index);
if (nextIndex >= tms->allocCount) {
tms->allocCount += 64;
tms->stack = (MethodRecord *)realloc(tms->stack, tms->allocCount * sizeof(MethodRecord));
}
MethodRecord *newRecord = &tms->stack[nextIndex];
newRecord->cls = object_getClass(self);
newRecord->sel = _cmd;
newRecord->lr = lr;
if (tms->isMainThread) {
struct timeval now;
gettimeofday(&now, NULL);
newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
}
}
}
uintptr_t after_objc_msgSend() {
ThreadMethodStack *tms = getThreadMethodStack();
int curIndex = tms->index;
int nextIndex = tms->index--;
MethodRecord *record = &tms->stack[nextIndex];
if (tms->isMainThread) {
struct timeval now;
gettimeofday(&now, NULL);
uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
if (time < record->time) {
time += 100 * 1000000;
}
uint64_t cost = time - record->time;
if (cost > minConsumeTime && tms->index < maxDepth) {
// 爲記錄棧分配內存
if (!threadCallRecords) {
recordsAllocCount = 1024;
threadCallRecords = malloc(sizeof(threadCallRecords) * recordsAllocCount);
}
recordsCurrentCount++;
if (recordsCurrentCount >= recordsAllocCount) {
recordsAllocCount += 1024;
threadCallRecords = realloc(threadCallRecords, sizeof(threadCallRecords) * recordsAllocCount);
}
// 添加記錄元素
YEThreadCallRecord *yeRecord = &threadCallRecords[recordsCurrentCount - 1];
yeRecord->cls = record->cls;
yeRecord->depth = curIndex;
yeRecord->sel = record->sel;
yeRecord->time = cost;
}
}
// 恢復下條指令
return record->lr;
}
//arm64標準:sp % 16 必須等於0
#define saveParameters() \ __asm volatile ( \ "str x8, [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");
#define loadParameters() \ __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" \ "ldr x8, [sp], #16\n" );
// 前置方法的返回值會儲存在x8上,因此調用咱們本身的方法前先保存下x8,接着將方法加載到x12中去執行指令
#define call(b, value) \ __asm volatile ("str x8, [sp, #-16]!\n"); \ __asm volatile ("mov x12, %0\n" :: "r"(value)); \ __asm volatile ("ldr x8, [sp], #16\n"); \ __asm volatile (#b " x12\n");
// 替換的objc_msgSend
__attribute__((__naked__))
static void hook_objc_msgSend() {
// 存原調用參數
saveParameters()
// 將lr存放的指令放在x2,做爲before_objc_msgSend參數
__asm volatile ("mov x2, lr\n");
// Call our before_objc_msgSend.
call(blr, &before_objc_msgSend)
// 恢復參數
loadParameters()
// 調用objc_msgSend
call(blr, orgin_objc_msgSend)
saveParameters()
// Call our after_objc_msgSend.
call(blr, &after_objc_msgSend)
// x0存的是after_objc_msgSend返回的下條指令,返回給lr指針
__asm volatile ("mov lr, x0\n");
// Load original objc_msgSend return value.
loadParameters()
// 返回
__asm volatile ( "ret");
}
複製代碼
實現比較簡單,將寄存器x0至x8的內容保存到棧內存中,調用 before_objc_msgSend 建立當前方法對應結構體 MethodRecord ,記錄當前時間、方法信息並保存 lr 指針後,恢復x0至x8的內容調用原 objc_msgSend;調用結束後再次重複存取寄存器內容,將 MethodRecord 取出計算總體耗時。
須要注意的是,當咱們在 objc_msgSend 的過程當中調用自定義的函數時,會改變 lr 寄存器中的值,致使最後的 ret函數 找不到下一條指令,因此須要在 before_objc_msgSend 記錄 lr值,並在 after_objc_msgSend 恢復。
在寫本文時,初期嘗試過用 Aspects 框架實現耗時監控,這裏列出當時的思考。
今年計劃完成10個優秀第三方源碼解讀,會陸續提交到 iOS-Framework-Analysis ,歡迎 star 項目陪伴筆者一塊兒提升進步,如有什麼不足之處,敬請告知 🏆。