我以前寫了一個開源庫TimeProfiler,監控全部的OC方法耗時。能夠在開發App階段,很方便的看到主線程全部OC方法的耗時。可是因爲TimeProfiler是經過fishhook基於運行時hook,因此從原理上,它就有侷限性:不能選擇hook部分類的OC方法。這形成2個很難解決的問題:python
KKMagicHook經過靜態插樁的方式來實現Hook,能夠選擇本身須要hook的模塊。git
既然你們有這樣的痛點,我就來想辦法解決。網上有facebook方案:經過 llvm 插樁;手淘提到的彙編插樁。好吧,對於只能工做以外時間作這個事情,我暫時沒有時間去作這個(但確實挺感興趣的,後面時間容許,我研究完,也會分享出來)。而後看到這篇文章:靜態攔截iOS對象方法調用的簡易實現,大佬只是大體說了原理,可是網上並無找到任何關於它的實現。我只好本身動手,在作的過程當中,感受仍是挺複雜的(至少你要很是熟悉靜態庫和目標文件的結構。大佬說的簡易,應該是相對於llvm 插樁跟彙編插樁來講吧),有許多坑~ 因此也寫這篇文章分享一下。程序員
我就不一行一行解讀具體實現代碼了,我挑遇到的坑跟核心邏輯說一下,而後你們結合代碼KKMagicHook,就很容易理解了。github
腳本只處理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
//靜態庫自己的符號表頭跟目標文件頭數據結構同樣的
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)
複製代碼
遍歷目標文件的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 */
};
複製代碼
這塊須要知道理論知識:
直接看我開源出來的代碼,這塊邏輯很好懂。可是我作這塊時候,踩好多坑(反思了一下,主要是我不懂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)
複製代碼
.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算是TimeProfiler的進階版本,雖然能夠實現TimeProfiler所有的功能,可是認爲若是你要hook全部的OC方法,那爲啥不用TimeProfiler,使用更簡單。因此能用TimeProfiler就用TimeProfiler吧。
KKMagicHook應該更適用於,你想監控某個模塊的OC方法耗時,你把這個模塊編譯成靜態庫,而後用KKMagicHook中的腳本處理一下,就能夠了。例如項目中使用了TalkingData這個第三方庫,咱們想監控/評估一下這個第三方庫的性能問題,這個時候就不想監控項目中其它類了,以避免干擾分析。如圖,很清晰顯示TalkingData這個庫全部OC方法的耗時:
這個庫自己跟TimeProfiler同樣,是可視化OC方法的耗時。可是毫不止於此,KKMagicHook的核心邏輯是靜態插樁的方式來實現Hook Method,能夠服務更廣的場景。這個TimeProfiler和fishhook關係同樣,TimeProfiler只能用來可視化方法耗時,可是fishhook能夠服務更廣的場景。
因此你們可使用KKMagicHook的核心邏輯,來服務本身項目許多方面。