最近出現了幾篇關於二進制重排啓動優化的文章。全部方案中都須要事先統計全部的函數調用狀況,並根據函數調用的頻次來進行代碼的重排。python
這些函數調用中,OC對象的方法調用最多。統計OC對象的方法調用能夠在運行時經過第三方庫好比fishhook來Hook全部objc_msgSend調用來實現,也能夠在編譯後連接前經過靜態插樁的方式來實現Hook攔截。ruby
對於靜態插樁的實現通常有以下兩個方案:bash
藉助於LLVM語法樹分析來實現代碼插樁。函數
將源碼編譯爲靜態庫,並經過修改靜態庫中.o目標文件的代碼段來實現代碼插樁。工具
上述的兩個方法實現起來比較複雜,要麼就要了解LLVM,要麼就要熟悉目標文件中間字節碼以及符號表相關的底層知識。post
本文所介紹的是第三種靜態Hook方案,也是依賴於靜態庫這個前提來實現對objc_msgSend函數進行Hook,從而實如今編譯前連接後的OC對象方法調用插樁。優化
這個方案實現的原理很簡單。由於靜態庫其實只是一個編譯階段的中間產物,靜態庫目標文件中的全部引用的外部符號會保存到一張字符串表中,全部函數調用都只是記錄了函數名稱在字符串表的索引位置,在連接時會纔會根據符號名稱來替換爲真實的函數調用指令。所以咱們能夠將全部靜態庫字符串表中的objc_msgSend統一替換爲另一個長度相同的字符串:hook_msgSend(名字任意只要長度一致並惟一)便可。而後在主工程源代碼中實現一個名字爲hook_msgSend的函數便可。這個函數必需要和objc_msgSend的函數簽名保持一致,這樣在連接時全部靜態庫中的objc_msgSend調用都會統一轉化爲hook_msgSend調用。ui
下面的是具體的實現步驟:spa
1. 在主工程中編寫hook_msgSend的實現。code
hook_msgSend的函數簽名要和objc_msgSend保持一致,而且要在主工程代碼中實現,並且必需要用匯編代碼實現。具體實現的邏輯和目前不少文章中介紹的對objc_msgSend函數的Hook實現保持一致便可。
不少對objc_msgSend進行Hook的實現實際上是不完整的,所以若是想徹底掌握函數調用ABI規則的話請參考:《深刻iOS系統底層之函數調用》
2. 將全部其餘代碼都統一編譯爲一個或多個靜態庫。
將源代碼按功能編譯爲一個或多個靜態庫,而且主工程連接這些靜態庫。這種程序代碼的組織方式已經很成熟了,最經常使用的方法是咱們能夠藉助代碼依賴集成工具cocoapods來實現,這裏就再也不贅述了。
3. 在主工程的Build Phases 中添加Run Script腳本。
咱們須要保證這個腳本必定要運行在連接全部靜態庫以前執行。所以能夠放到Compile Sources 下面。
4. 實現靜態庫符號替換的Run Script腳本。
這是最爲關鍵的一步,咱們能夠實現一個符號替換的程序,而後在Run Script腳本中 執行這個符號替換程序。符號替換程序的輸入參數就是主工程中所連接的全部靜態庫的路徑。至於這個符號替換程序如何編寫則沒有限制,你能夠用ruby編寫也能夠用python也能夠用C語言編寫。 不管用何種方法實現,你都須要首先了解一下靜態庫.a的文件結構。你能夠從:《深刻iOS系統底層之靜態庫》一文中掌握到一個靜態庫文件的組成結構。瞭解了靜態庫文件的組成結構後,你的符號替換程序要作的事情就能夠按以下步驟實現:
一)、 打開靜態庫.a文件。
二)、找到.a文件中的字符串表部分。字符串表的描述以下:
struct stringtab
{
int size; //字符串表的尺寸
char strings[0]; //字符串表的內容,每一個字符串以\0分隔。
};
複製代碼
字符串表中的strings的內容就是一個個以\0分隔的字符串,這些字符串的內容其實就是這個目標文件所引用的全部外部和內部的符號名稱。
三)、將字符串表中的objc_msgSend字符串替換爲hook_msgSend字符串。
四)、保存並關閉靜態庫.a文件。
5. 編譯、連接並運行你的主工程程序。
採用本文中所介紹的靜態Hook方法的好處是咱們沒必要Hook全部的OC方法調用,而是能夠有選擇的進行特定對象和類的方法調用攔截。所以這種技術不只能夠應用代碼重排統計上,還能夠應用在其餘的監控和統計應用中。由於這種機制能夠避免程序在運行時進行objc_msgSend替換而產生的函數調用風暴問題。另外的一個點就是這個方法不侷限於對objc_msgSend進行Hook,還能夠對任意的其餘函數進行Hook處理。所以這種技術也能夠應用在其餘方面。