靜態插樁的方式來實現Hook Method

經過fishhook攔截方法的侷限性

我以前寫了一個開源庫TimeProfiler監控全部的OC方法耗時。能夠在開發App階段,很方便的看到主線程全部OC方法的耗時。可是因爲TimeProfiler是經過fishhook基於運行時hook,因此從原理上,它就有侷限性:不能選擇hook部分類的OC方法。這形成2個很難解決的問題:python

  1. 不能選擇hook一部分類的OC方法,所有hook會有性能問題,因此也不能線上使用。
  2. 個別同窗反映,TimeProfiler hook某個類的方法,會crash。可是因爲代碼安全性,不能把代碼給我看,由於這個類跟項目強相關,也不能造一個crash的demo給我排查問題。因此我只能盲猜哪裏出問題,效率極低。而他們也會由於hook這個類crash,致使不能用到這麼好的工具,多惋惜~ 而KKMagicHook能夠選擇不hook這個類,不妨礙使用這個工具。

KKMagicHook經過靜態插樁的方式來實現Hook,能夠選擇本身須要hook的模塊。git

既然你們有這樣的痛點,我就來想辦法解決。網上有facebook方案:經過 llvm 插樁;手淘提到的彙編插樁。好吧,對於只能工做以外時間作這個事情,我暫時沒有時間去作這個(但確實挺感興趣的,後面時間容許,我研究完,也會分享出來)。而後看到這篇文章:靜態攔截iOS對象方法調用的簡易實現,大佬只是大體說了原理,可是網上並無找到任何關於它的實現。我只好本身動手,在作的過程當中,感受仍是挺複雜的(至少你要很是熟悉靜態庫和目標文件的結構。大佬說的簡易,應該是相對於llvm 插樁跟彙編插樁來講吧),有許多坑~ 因此也寫這篇文章分享一下。程序員

實現過程遇到的坑跟核心邏輯

我就不一行一行解讀具體實現代碼了,我挑遇到的坑跟核心邏輯說一下,而後你們結合代碼KKMagicHook,就很容易理解了。github

靜態庫是fat file

腳本只處理arm64架構的靜態庫,若是靜態庫是fat file,包含多種架構。我是先從fat file中提取出arm64架構的靜態庫,交給腳本處理;處理完以後,在replace fat file中的arm64架構。安全

def deal_fat_file():
    global staticLibPath, fatFilePath
    fatFilePath = staticLibPath
    (fatFileDir, fatFileName) = os.path.split(fatFilePath)
    fatFileName = 'tmp-arm64-'+fatFileName
    staticLibPath = os.path.join(fatFileDir, fatFileName)
    # 提取出arm64架構的靜態庫
    os.system('lipo ' + fatFilePath + ' -thin ' + 'arm64 -output '+ staticLibPath)

def replace_fat_file():
    # replace fat file中的arm64架構
    os.system('lipo '+fatFilePath+' -replace arm64 '+staticLibPath+' -output '+fatFilePath)
    os.remove(staticLibPath)
複製代碼

特別說明,處理後,只有arm64裏的objc_msgSend方法被替換成了hook_msgSend,因此在arm64平臺的設備上運行時候,都是調用hook_msgSend;而在其它架構平臺,依然是調用objc_msgSend方法,對其它架構平臺沒有任何影響。bash

目標文件頭size的意義

//靜態庫自己的符號表頭跟目標文件頭數據結構同樣的
struct object_header {  
    char        name[16];       /* 名稱 */
    char        timestamp[12];  /* 生成的時間戳 */
    char        userid[6];        /* 用戶id */
    char        groupid[6];  /* 組id */
    uint64_t    mode;            /* 文件訪問模式 */
    uint64_t    size;            /* 目標文件的字節大小 */
    uint32_t    endheader;        /* 頭結束標誌 */
    char        longname[0];   /* 目標文件名(不定長) */
};
複製代碼

網上全部文章都說size是目標文件的字節大小,可是我在解析過程當中,發現咋算都對不上。最後看MachOView源碼才知道,size表示目標文件的大小 + longname的大小。因此說只有longname長度爲0時候,size才表示目標文件的大小。longname長度能夠從name中獲取,若是name是以"#1/"開頭,那"#1/xx",xx就表示longname的長度。不然longname長度爲0。數據結構

過濾須要處理的類

其實咱們過濾的是須要處理的目標文件,可是目標文件名就是類名(類名是ClassA,目標文件名就是ClassA.o),而且一個類在一個文件中。因此說咱們過濾須要處理的目標文件,就是過濾須要處理的類。架構

腳本中默認是替換靜態庫中全部類的objc_msgSend方法,當選擇處理模式爲:need_process_objFile,就只替換need_process_objFile集合裏的類的objc_msgSend方法;當選擇處理模式爲:needless_process_objFile,表示除了needless_process_objFile集合裏的類不替換,靜態庫中其他的類的objc_msgSend方法都替換。less

need_process_objFile = set() # set('xx1', 'xx2') 表示靜態庫中,僅xx1跟xx2須要處理
needless_process_objFile = set() # set('xx1', 'xx2') 表示靜態庫中,xx1跟xx2不須要處理,剩下的都須要處理

def process_object_file(name, location, size):
# 根據須要,下面三行中,只需打開一行,另外兩行須要註釋掉
process_mode = 'default' # 默認處理該靜態庫中的全部目標文件(類)
#process_mode = 'need_process_objFile' # 只處理need_process_objFile集合(上面的集合,須要賦值)中的類
#process_mode = 'needless_process_objFile' # 除了needless_process_objFile集合(上面的集合,須要賦值)中的類不處理,剩下的都須要處理

# 這裏能夠過濾不須要處理的目標文件,或者只選擇須要處理的目標文件
# 默認處理該靜態庫中的全部目標文件
if process_mode == 'need_process_objFile':
    if name in need_process_objFile:
        find_symtab(location, size)
elif process_mode == 'needless_process_objFile':
    if not name in need_process_objFile:
        find_symtab(location, size)
else:
    find_symtab(location, size)
複製代碼

尋找字符串表的location跟size

遍歷目標文件的Load Commands,找到符號表,根據stroff算出location。函數

struct symtab_command {
	uint32_t	cmd;		/* LC_SYMTAB */
	uint32_t	cmdsize;	/* sizeof(struct symtab_command) */
	uint32_t	symoff;		/* symbol table offset */
	uint32_t	nsyms;		/* number of symbol table entries */
	uint32_t	stroff;		/* string table offset */
	uint32_t	strsize;	/* string table size in bytes */
};
複製代碼

這塊須要知道理論知識:

  1. iOS程序員的自我修養-MachO文件結構分析(二)
  2. iOS程序員的自我修養-MachO文件靜態連接(三)

替換字符串表中的objc_msgSend

直接看我開源出來的代碼,這塊邏輯很好懂。可是我作這塊時候,踩好多坑(反思了一下,主要是我不懂python),好比我不知道python不能在原文件中修改指定位置內容(確實查到能夠經過os.system調用sed,而後回寫等方式),可是靜態庫只能以二進制方式打開,而那些都是處理文本。
我本來是找到字符串表,而後decode成字符串,而後替換完成,再encode成二進制,可是這樣會形成失真。緣由decode過程,\x00會被丟棄。最後發現二進制也能夠替換😂。

def replace_Objc_MsgSend(fileLen):
pos = 0
bytes = b''
(loc, size) = symtabList_loc_size[0]
listIndex = 1
with open(staticLibPath, 'rb') as fileobj:
    while pos < fileLen:
        if pos == loc:
            content = fileobj.read(size)
            content = content.replace(b'\x00_objc_msgSend\x00', b'\x00_hook_msgSend\x00')
            pos = pos + size
            if listIndex < len(symtabList_loc_size):
                (loc, size) = symtabList_loc_size[listIndex]
                listIndex = 1 + listIndex
        else:
            step = 4
            if loc > pos:
                step = loc - pos
            else:
                step = fileLen - pos
            content = fileobj.read(step)
            pos = pos + step
        bytes = bytes + content
with open(staticLibPath, 'wb+') as fileobj:
    fileobj.write(bytes)

複製代碼

_hook_msgSend的實現

.macro CALL_HOOK_BEFORE
    BACKUP_REGISTERS
    mov x2, lr
    bl _hook_objc_msgSend_before
    RESTORE_REGISTERS
.endmacro

.macro CALL_HOOK_AFTER
    BACKUP_REGISTERS
    bl _hook_objc_msgSend_after
    mov lr, x0
    RESTORE_REGISTERS
.endmacro

# hookObjcMsgSend.py裏定義了函數名爲hook_msgSend,若是修改腳本里的函數名,這裏的函數名,也需跟腳本保持一致
ENTRY _hook_msgSend

CALL_HOOK_BEFORE
bl _objc_msgSend
CALL_HOOK_AFTER
ret

END_ENTRY _hook_msgSend
複製代碼

這個彙編代碼詳細解說,請見我以前博客監控全部的OC方法耗時。惟獨須要注意的是,彙編裏的函數名,要跟hookObjcMsgSend.py裏定義的函數名一致。

KKMagicHook的適用場景

我以爲KKMagicHook算是TimeProfiler的進階版本,雖然能夠實現TimeProfiler所有的功能,可是認爲若是你要hook全部的OC方法,那爲啥不用TimeProfiler,使用更簡單。因此能用TimeProfiler就用TimeProfiler吧。

KKMagicHook應該更適用於,你想監控某個模塊的OC方法耗時,你把這個模塊編譯成靜態庫,而後用KKMagicHook中的腳本處理一下,就能夠了。例如項目中使用了TalkingData這個第三方庫,咱們想監控/評估一下這個第三方庫的性能問題,這個時候就不想監控項目中其它類了,以避免干擾分析。如圖,很清晰顯示TalkingData這個庫全部OC方法的耗時:

KKMagicHook的意義

這個庫自己跟TimeProfiler同樣,是可視化OC方法的耗時。可是毫不止於此,KKMagicHook的核心邏輯是靜態插樁的方式來實現Hook Method,能夠服務更廣的場景。這個TimeProfiler和fishhook關係同樣,TimeProfiler只能用來可視化方法耗時,可是fishhook能夠服務更廣的場景。

因此你們可使用KKMagicHook的核心邏輯,來服務本身項目許多方面。

源碼

KKMagicHook

參考

  1. juejin.im/post/5e1280…
  2. juejin.im/post/5d5275…
  3. juejin.im/post/5d5278…
相關文章
相關標籤/搜索