從編譯到鏈接,Mach-O到fishhook的應用

原文鏈接: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指明了可執行文件的目標架構

ncmdssizeofcmds用于命令加載,其指明了文件的邏輯結構和在虛擬內存中的布局。

我們可以看到每個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方法,進而完成方法替換。

參考文檔

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370