原文鏈接:http://www.cocoachina.com/special/20161130/18243.html
我看見原文是用markdown寫的,但是cocoachina沒有用markdown處理,所以復制在簡書,方便自己閱讀。
實例
實例分析當然選用最經典的helloworld了,我們可以通過終端,輸出helloworld。
- 創建一個目錄,同時創建文件,打開。
mkdir mach-o-helloworld
touch helloworld.c
open -e helloworld.c
- 在helloworld.c中寫入代碼,并保存。
#include
int main(int argc, char *argv[])
{
printf("Hello World! ");
return 0;
}
- 終端執行下面代碼
xcrun clang helloworld.c -o helloworld.out
./helloworld.out
[圖片上傳失敗...(image-be52b3-1557459281771)]
編譯器
編譯器主要有以下兩個任務:
把OC代碼轉換成低級的代碼
分析代碼,確保其沒有任何錯誤
XCode搭載了clang作為編譯器,clang可以獲取OC代碼,分析,并將其轉化成類似于匯編語言的低級語言的代碼,LLVM Intermediate Representation, LLVM IR無系統依賴,LLVM負責構建并將其編譯成指定平臺的本地bytecode。這樣做的好處之一就是這些構建可以在任何LLVM支持的平臺上面運行。比如你寫了一個iOS App,它可以自動的在Intel和ARM平臺上運行,LLVM會將IR代碼轉成不同這些平臺的bytecode。
LLVM
LLVM有三層體系
支持大量的輸入語言,C, C++,等等
共享優化器,用于優化LLVM IR
-
不同的平臺比如ARM,PowerPC等
[圖片上傳失敗...(image-7a8bc8-1557459281771)]
當編譯一個源文件的時候,如上圖所示,其流程細化總結下來一共有這幾個步驟:
輸入文件
預處理
-
include的展開
宏的展開
符號化
詞法分析與語義分析
將預處理的符號翻譯入一個解析樹
將語義分析應用到解析樹
輸出一個抽象語法樹(AST)
代碼生成和優化
將抽象語法樹(AST)翻譯成LLVM IR
優化生成的代碼
生成目標代碼
輸出目標代碼
匯編
將目標代碼輸入到一個目標文件
鏈接
將多個目標文件合并成一個可執行文件
預處理
當源文件進入到了預處理階段,首先的第一件事就是處理宏,比如你寫了#import
,當預處理器分析到這行的時候,會將其替換成為這個文件的內容,如果.h文件里包含其他的宏定義,也會進行替換。這就解釋了為什么你的.h文件需要盡可能少的import其他頭文件,而你應該采用@class的方式。
我們再弄一個例子,創建一個OC文件,helloworld.m,對其執行預處理,其命令為clang -E helloworld.m | less
,可以看到其內容為下圖,代碼行數為8:
[圖片上傳失敗...(image-771bd6-1557459281771)]
如果我們在helloworld.m中引入Foundation頭文件,然后用上述命令進行預處理,可以看到文件行數為
[圖片上傳失敗...(image-5e32a5-1557459281771)]
我們可以打開預編譯后的文件看下其結構,xcrun clang -E helloworld.m | open -f
,類似這樣。
[圖片上傳失敗...(image-b7e1a7-1557459281771)]
- include展開我們都用過#include和#import。它們就是告訴預處理器在#include語句的地方插入引入的.h的內容。在剛剛的文件里就是插入了一個以#開頭的行標記。跟在#后面的數字是在源文件中的行號。每一行最后的數字是在新文件中的行號。接下來是系統頭文件,或者extern “C”的文件。在Xcode中,你可以通過使用Product->Perform Action-> Preprocess來查看任何一個文件的預處理輸出。
[圖片上傳失敗...(image-8f3a7b-1557459281771)]
-
宏展開宏展開會直接將指定變量代碼替換為宏指定的代碼段,但是宏容易引發錯誤,很難定位,因此可以采用
static inline
,它與宏有相同的性能。
符號化
經歷過預處理之后,每個.m文件都會從字符串轉化成一系列的標記,我們可以使用clang生成這些標簽查看,具體命令為clang -Xclang -dump-tokens helloworld.m
[圖片上傳失敗...(image-8d2896-1557459281771)]
分析
AST生成
這步操作會將之前的符號流轉化成一個AST, ObjC的復雜性就決定了分析的過程不會特別快,分析結束之后這步就會生成一個抽象語法樹(AST),類似下圖,抽象語法樹的一個好處是能極大方便編譯,分析和優化。
[圖片上傳失敗...(image-994ac2-1557459281771)]
我們采用以下命令clang -Xclang -ast-dump -fsyntax-only hello.m
,我們會得到以下
[圖片上傳失敗...(image-321eb9-1557459281771)]
每個AST上面節點都對應有源碼的位置,所以一旦有問題,就會幫我們定位到具體的位置。
靜態分析
生成語法樹之后,編譯器就可以通過分析語法樹來進行類型檢查,或者其它分析來幫助你檢查錯誤,比如某個對象是否實現了某個方法,等等。
- 類型檢查
類型檢查會檢查一個對象是否支持某個方法,如果不支持則會報出error,某個對象類型是否正確,編譯器會爆出警告,等等。
動態類型檢查:runtime中進行類型檢查,用于決定是否某個對象可以響應某個方法。
靜態類型檢查:編譯過程中進行類型檢查,工程設置為ARC的時候,編譯器會進行大量的類型檢查。
- 其他分析
編譯器還會做很多其他的分析,比如檢查使用了某個變量,或者初始化方法檢查等。
代碼生成
經過符號化,代碼分析等,編譯器就會將代碼生成LLVM IR。比如可以使用下面的命令來查看,clang -O3 -S -emit-llvm helloworld.m -o helloworld.ll
,生成的代碼如下:
[圖片上傳失敗...(image-e7e3-1557459281771)]
代碼優化
代碼生成之后就會進行代碼優化,我們用菲波那切數列的遞歸實現為例來看這個比較,可以使用如下命令。
clang -O0 -S -emit-llvm helloworld.m -o helloworld.ll
clang -O3 -S -emit-llvm helloworld.m -o helloworld.ll
[圖片上傳失敗...(image-9e1654-1557459281771)]
[圖片上傳失敗...(image-1ab7a6-1557459281771)]
可以看一下遞歸函數已經被展開優化,這樣進行代碼優化之后就可以提升編譯速度。
生成目標代碼
可以采用xcrun clang -S -o - helloworld.c | open -f
,這樣我們就可以得到匯編代碼,類似這樣
[圖片上傳失敗...(image-ddb726-1557459281771)]
其中帶.的為匯編指令,其它的則為匯編程序。具體內容我們不做分析。XCode通過 Product->Perform Action -> Assemble可以查看任何文件的匯編代碼。
匯編
就是簡單的將匯編代碼轉化成機器碼,其創建了一個目標文件。XCode里面的.o文件都存在derived data目錄下的Objects-normal文件夾。
鏈接
鏈接器主要是確定.o與libraries中間的符號問題。比如當你調用一個方法的時候,最后的執行需要知道這個方法對應的符號地址,以及這個方法在內存中的地址。連接器將會讀取所有的目標文件,然后將它們編碼進最后的可執行文件,然后輸出最后的可執行文件。
至此我們就完成了可執行文件的生成。
可執行文件分析
Section
一個可執行文件有多個Section,每個Section都會被包含進一個Segment。當我們執行xcrun size -x -l -m helloworld.out
時候可以看到輸出以下內容:
[圖片上傳失敗...(image-7a613a-1557459281771)]
當我們執行這個可執行文件的時候,虛擬內存會映射segments到程序的內存地址,虛擬內存會通過分段或者分頁等方式將可執行文件載入內存,不會一次性將所有文件全部載入。當虛擬內存做好映射后,segmengs和sections會根據權限等進行不同的映射。
__PAGEZERO,不可寫,不可讀,不可執行,一共4G,用于指明程序的地址空間。
__TEXT代碼段,可讀,可執行的,不可修改,因此它始終是干凈的。
__DATA數據段,可讀,可寫,不可執行,我們會隨時更新里面的內容。
每個Segment包含了一些Sections,不同部分具有不同的含義。
__TEXT:可以看到__text包含了復雜的機器碼,__stubs和__stub_helper用于動態鏈接,__const用于靜態變量,__cstring用于字符串變量。
__DATA:包含了讀寫數據,__nl_symbol_ptr以及__la_symbol_ptr,_const在__DATA段內是通用的,主要為不可變數據。_bss片段包含了沒有被初始化的靜態變量例如static int a;_common片段包含了被動態鏈接器使用的占位符片段。
Section
我們可以通過如下命令xcrun otool -v -t helloworld.out
來查看反匯編的代碼,如下。
[圖片上傳失敗...(image-2335a5-1557459281771)]
當然我們也可以查看其它片段,比如
[圖片上傳失敗...(image-e52e61-1557459281771)]
Mach-O
OSX和iOS指明了可執行文件的格式為Mach-O
[圖片上傳失敗...(image-a55d6f-1557459281771)]
接下來我們可以對Mach-O進行分析,使用otool可以查看Mach-o的頭部信息。
[圖片上傳失敗...(image-3bf6bd-1557459281771)]
cputype,cpusubtype指明了可執行文件的目標架構
ncmds,sizeofcmds用于命令加載,其指明了文件的邏輯結構和在虛擬內存中的布局。
我們可以看到每個segment以及每個section的位置,xcrun otool -v -l helloworld.out | open -f
[圖片上傳失敗...(image-eaff3d-1557459281771)]
dyld
在程序中,每個函數,變量等都是通過符號的方式來展現的,當我們的可執行文件鏈接到目標文件的時候,鏈接器會尋找目標文件和動態庫之間的所有符號,因此在可執行文件和目標文件中都存在符號表用來記錄和保存這些符號,類似這樣
[圖片上傳失敗...(image-50c2f1-1557459281771)]
一些動態庫和運行時的符號有可能是不確定的,但是符號表中會記錄它們去哪里尋找動態庫,而這些未定義的符號將會在運行時被dyld指定。由于一個程序會依賴大量的動態鏈接庫,因此會有無數的符號需要指定,iOS上面使用了一個共享緩存的概念,存在著一個文件里面包含了大多數的動態鏈接庫,其互相連接且符號已經指定,當一個Mach-O被加載的時候,首先檢查共享緩存,這大大的優化了程序的加載時間。
Fish Hook
fishhook是facebook開源的用于動態方法替換的一個開源庫,在iOS開發過程中,它通過動態重綁定Mach-O二進制可執行文件內的標識,完成方法替換。
工作原理
依據上文中的介紹,Mach-O的二進制文件中存在著__DATA segment,而dyld通過更新sections中的指針來綁定lazy和non-lazy的標識。fishhook通過rebind_symbols的方法來重新綁定這些標識的名字和實現。
[圖片上傳失敗...(image-7f6db6-1557459281771)]
__DATA segment包含兩個動態標識綁定相關的sections,__nl_symbol_ptr和__la_symbol_ptr,__nl_symbol_ptr是一個non-lazily綁定數據的指針數組,其在庫被加載的時候就被綁定,__la_symbol_ptr也是一個指針數組,它在第一次用到標識的時候,調用dyld_stub_binder來導入方法
[圖片上傳失敗...(image-2d3b5b-1557459281771)]
如上圖所示中提供了reserved1字段用作間接符號表的偏移,間接符號表,存在于__LINKEDIT segment,它是一個符號表的索引數組,符號表的順序與non-lazy和lazy符號section中的指針相同,所以nl_symbol_ptr在符號表中的首地址的相應位置就是間接符號表中的nl_symbol_ptr->reserved1,符號表本身就是一個結構體,如下圖
[圖片上傳失敗...(image-9d919f-1557459281771)]
每一個nlist包含一個索引,指向__LINKEDIT上的字符表,__LINKEDIT則是真正的符號名稱存在的地方,所以對于每一個__la_symbol_ptr,__nl_symbol_ptr,我們都可以找到相應的標識以及通過比對標識名,我們就能找到其相應的字符串,進而可以替代section中的指針。
具體用法
具體用法如下。
[圖片上傳失敗...(image-d62002-1557459281771)]
首先orig_free = dlsym(RTLD_DEFAULT, "free");
會將系統的free方法,指定給orig_free,當orig_free的時候就會執行原來的系統free方法。
之后rebind_symbols((struct rebinding[1]){{"free", myfree, (void *)&orig_free}}, 1);
會用myfree這個方法動態hook free方法,進而完成方法替換。