01
認識mach-o的必要性
了解mach-o的結構可以幫助認識系統加載二進制文件的動態鏈接和靜態鏈接。應用層面,使用initialize的c 函數計算啟動時間耗時也需要以mach-o的結構知識為鋪墊。還可以用在使用clang自註冊啟動任務上。後續會一一展開說明。
02
mach-o的定義
mach-o是mach object的縮寫,是存儲程序或庫的標準格式。app的mach-o又稱為可執行文件,靜態庫的.a文件也為mach-o文件,還有諸如此類的一些文件。
- .o目標文件:MH_OBJECT
- 靜態庫文件.a : MH_OBJECT
- 可執行文件:MH_EXECUTE
- 動態庫:MH_DYLIB
- dyld:MH_DYLINKER
- 符號信息:MH_DSYM。可在loader.h的源碼中看到全部的mach-o文件。
想要深入了解mach-o,可以自己創建一個,在xocde的build setting --> mach-o type 下選擇類型,依據xcode的提示步驟可以創建出。如果已經有了文件,想知道是否為mach-o,也可以使用xcode打開文件,在build setting --> mach-o type下查看屬於哪種類型。xcode
的查看示意見下圖:
![](https://news.xinpengboligang.com/upload/keji/b0b6d0a5552a1e9bd16f2dcb908f0d47.jpeg)
![](https://news.xinpengboligang.com/upload/keji/43ec7848ca1293b2e52ff5f50d10175d.jpeg)
03
mach-o的結構
查看mach-o的內部結構需要借助於工具mach-o View下載地址。目前下載下來之後需要在mac上運行使用。舉例,使用mach-o View查看app的可執行文件,首先需要編譯項目,這樣會生成.app文件,然後項目中搜索.app:
![](https://news.xinpengboligang.com/upload/keji/9352225b8478134e5f5700b9b29b3982.jpeg)
右鍵show in finder,就會找到app包。如下圖:
![](https://news.xinpengboligang.com/upload/keji/5d9133f73a9fae49c835a4e67d3e0e37.jpeg)
查找可執行文件,文件以項目名稱命名的。下圖中第一個就是:
![](https://news.xinpengboligang.com/upload/keji/6c2157b1ca21a9cecc52c6f5da1216c4.jpeg)
通過打開mach-o View可以看到,mach-o分為三大部分,無論是什麼類型的mach-o文件,都分為3大部分。
- mach-o header 描述了Mach-o的cpu框架以及加載命令等信息;
- load Commands 記錄虛擬內存中的佈局例如有哪些段,段從哪開始,段占用多大空間;
- data 記錄段的具體數據。如下圖,三大部分在mach-o中分佈:
![](https://news.xinpengboligang.com/upload/keji/72a69ba3db56bf3a3632700ea3b8dd10.jpeg)
1、第一部分:mach-o Header 詳解
mach-o header 的結構如下圖中紅框展示:
![](https://news.xinpengboligang.com/upload/keji/a26b9c74aa4e1260f03293b12e140447.jpeg)
- magic number :系統加載器通過該字段判斷文件適用於32位還是64位;
- cpu type:cpu類型,該字段確保系統可以將合適的二進制文件在當下架構下進行,為x86,arm64等;
- file type :說明文件類型(可執行文件、庫文件、核心轉儲文件、內核擴展文件、DYSM文件、動態庫等)mach-o為MH-EXECUTE.;
- number of load command 說明加載命令的條數;
- size of load commands 表示加載命令的大小;
如上所述,header介紹了文件的基礎信息。
2、第二部分:mach-o內容
mach-o的內容部分分為load commands和 data。
- load commands 如圖所示:
每一個命令的含義下表:
命令名稱命令含義LC_SEGMENT_64將文件中的段映射到進程地址空間LC_DYLD_INFO_ONLYdyld相關信息LC_SYMTAB加載全局符號表信息LC_DYSYMTAB動態鏈接符號表信息LC_DYLD_INFO_ONLYdyld相關信息LC_LOAD_DYLINKER加載一個動態鏈接器,也就是加載dyldLC_UUIDapp的uuidLC_VERSION_MIN_IPHONEOS支持最低系統版本LC_MAIN設置程序主線程的入口地址LC_LOAD_DYLIB(動態庫名稱)加載相應的動態庫LC_FUNCTION_STARTS函數啟示地址表LC_CODE_SIGNATURE代碼簽名
loader.h文件中可查看命令的官方註釋。
- data部分的內容如圖所示:
![](https://news.xinpengboligang.com/upload/keji/090987897f611330fd15c9143208259b.jpeg)
如圖所示,data有2種段數據,一種為__TEXT段,一種為__DATA段。__text段是Mach-O文件中存儲代碼的一個特定段,它包含了程序的實際可執行代碼。在__text段中,存儲了程序的實際指令和函數定義。當程序被加載到內存中並執行時,操作系統會將__text段中的代碼加載到內存中,並按照指令逐條執行,從而實現程序的功能。 __text段通常是以隻讀方式存儲在Mach-O文件中,以確保代碼的完整性和安全性。這意味著在程序運行時,__text段中的代碼是不可被修改的,這有助於防止惡意軟件對程序代碼進行篡改。__data段是用來存儲程序的靜態數據的一個特定段。__data段包含了程序中的靜態全局變量、靜態局部變量和其他靜態數據,這些數據在程序運行時需要被初始化和使用。與代碼段__text不同,__data段存儲的是程序運行時需要進行讀寫操作的數據。
__text各個段的含義:
名稱作用TEXT.text隻有可執行的機器碼TEXT.cstring去重後的C字符串TEXT.const初始化過的常量TEXT.stubs符號樁。本質上是一小段會直接跳入lazybinding的表對應項指針指向的地址的代碼。TEXT.stub_helper輔助函數。上述提到的lazybinding的表中對應項的指針在沒有找到真正的符號地址的時候,都指向這。TEXT.unwind_info用於存儲處理異常情況信息TEXT.eh_frame調試輔助信息_objc_classname類名稱objc_methlist方法列表
__text段在mach-o中的釋義:
![](https://news.xinpengboligang.com/upload/keji/75672146d2be646fb054c05b45e3d703.jpeg)
__data 各個段的含義:
名稱 |
作用 |
DATA.data |
初始化過的可變的數據 |
DATA.nl_symbol_ptr |
非lazy-binding的指針表,dyld 加載會立即綁定 |
DATA.la_symbol_ptr |
lazy-binding的指針表,每個表項中的指針一開始指向stub_helper |
DATA.const |
沒有初始化過的常量 |
DATA.mod_init_func |
初始化函數,在main之前調用 |
DATA.mod_term_func |
終止函數,在main返回之後調用 |
DATA.bss |
沒有初始化的靜態變量 |
DATA.common |
沒有初始化過的符號聲明 |
DATA.__objc_nlclslist |
實現了 load 方法的類 |
__data 在mach-o中的展示:
![](https://news.xinpengboligang.com/upload/keji/79f0203652dca8a57f21119d8b171e9d.jpeg)
04
mach-o的應用
認識了mach-o,可以將其運用在統計啟動時期c static initializer 階段耗時。c static initializer 階段系統做了什麼,一個是c 的構造函數屬性函數,一個是非基礎類型的c
靜態全局變量的創建(通常是類或結構體)。在構造函數上打斷點,可以得到如圖:
![](https://news.xinpengboligang.com/upload/keji/050df134038372e152ea3c1beeaed2ab.jpeg)
從dyld的源碼中可以看到doModeInitFunction()
的具體執行。如下圖所示:
![](https://news.xinpengboligang.com/upload/keji/026c1fb38729be0eafc9fb40d758f873.jpeg)
從dyld的源碼中可以看出,取出mod_init_func section 中的元素並執行。可以看出,mod_init_func section中存儲的是函數地址,類型為initialize.了解這些之後,自己寫一個帶有計時的start和end函數,並在中間調用源函數地址。然後hook mod_init_func 中的所有地址,並替換執行自己的函數。步驟如下:
- hook mod_init_func中的所有地址 。因為__mod_init_func section 位於__DATAsegment.__DATA segment 是數據段,是可以在運行時被修改的。並且, load方法的執行是在dyld讀取這些initializer之前。所以hook mod_init_func中的所有地址是可行的;
- 修改mod_init_func數據。利用getsectiondata獲取到segment的每一個數據,將自己寫的方法替換表中的方法;
- 調用原來的initializer。自己的Initializer中逐個獲取每一個原函數地址,調用並計算耗時獲取。
通過以上步驟,我們可以得出這一項的耗時,從而做出優化。
認識mach-o,是註冊啟動任務的必備知識。做註冊啟動任務的必要性有兩點。
- 啟動代碼集中在AppDelegate中,代碼逐漸臃腫,易讀性降低,且代碼之間耦合度高;
- 各個業務方加啟動任務,都需要啟動業務配合。
我們利用mach-o結構的__DATA可讀寫性。所以可以通過clang的section函數在編譯階段寫入macho文件中一個__DATA段。__DATA段存儲函數指針的指針。具體的使用步驟為:
- 編寫註冊方法的宏,提供給外部使用;
- 業務方註冊任務,註冊的每個時機都會在編譯期間新增一個__DATA類型section,存儲任務函數;
- App運行,在註冊的時機函數中使用getsectbynamefromheader_64遍歷取出相應Section中的函數,並依次執行。
代碼如下:
static void Launch_Func_(void);\
__attribute__((used, section("__DATA, "#period""))) static const void * __Func__= Launch_Func_;\
static void Launch_Func_(void)
#define RegisterLaunchTaskOnWillFinishLaunchPeriod\
RegisterLaunchTask(willFinishLaunch)
#define RegisterLaunchTaskOnDidFinishLaunchPeriod\
RegisterLaunchTask(didFinishLaunch)
#define RegisterLaunchTaskOnDidFinishADPeriod\
RegisterLaunchTask(didFinishAD)
#define RegisterLaunchTaskOnDidFinishHomepagePeriod\
RegisterLaunchTask(didFinishHome)
各個業務的使用代碼如下:
RegisterLaunchTaskOnDidFinishHomepagePeriod{
/*do sth*/
}
參考資料:
1.https://everettjf.github.io/2017/02/06/a-method-of-hook-static-2.initializers/# https://github.com/fangshufeng/MachOView
作者:李贊
來源:微信公眾號:搜狐技術產品
出處
:https://mp.weixin.qq.com/s/c3bgebQUcXXetiFAdiXjsQ