本文為L_Ares個人寫作,以任何形式轉(zhuǎn)載請表明原文出處。
本文接上一節(jié)——iOS用到的LLVM(一)。請對LLVM和Clang不熟悉的同學們移步上一節(jié),了解了基礎(chǔ)的信息之后再閱讀本節(jié)。
一、準備工作
步驟1 : 使用xcode新建一個空的macOS下的commond Line Tool命令行工具,下面稱之為工程1。

注意 :
- 這里因為用的是命令行(
commond Line Tool),所以初創(chuàng)的情況下沒有對其他的框架造成依賴。- 因為沒有依賴,所以以下的命令都是不引入其他
iOS框架的(包括也沒有引入Foundation框架)。- 如果想要引入其他的框架,那么就在
clang命令上添加框架的地址。下面是舉例的一個命令,引入內(nèi)容按照自己要使用的框架的情況進行修改即可。
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的SDK路徑) -fmodules -fsyntax-only -Xclang -ast-dump main.m
步驟2 : 打開terminal終端,進入到剛創(chuàng)建的這個項目中main.m所在的文件夾下。
步驟3 : 在terminal終端中輸入clang的查看詳細編譯步驟的指令。
clang -ccc-print-phases main.m
圖片未必看的清楚,我把內(nèi)容拷貝下來了,下面稱之為內(nèi)容1。
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
解釋
- 如
input、preprocessor、compiler、backend、assembler、linker、bind-arch,這些東西表示的是編譯中的操作名稱。- 如
main.m、{0}.....{5},這些東西表示的是這一步操作中要讀取的文件,也就是上一步操作的結(jié)果文件。- 如
objective-c、objective-c-cpp-output、ir、assembler、object、image等這些東西就是本步操作完成后,生成的文件,也就是上面2中說的上一步操作的結(jié)果文件。
對于這個工程,一共有0~6一共7個階段。這就是
main.m這個文件從源碼到機器語言的總的流程。下面開始按照流程來說。
二、源碼的編譯流程
2.1 編譯總流程
命令 :
clang -ccc-print-phases main.m
編譯總流程就是上面的內(nèi)容1。
先闡述總流程0~6中都是什么 :
0: 輸入文件 : "找到源文件"。
1: 預(yù)處理階段 : 這個階段處理了"宏的替換"和"頭文件的導(dǎo)入"。
2: 編譯階段 : 進行"詞法分析"、"語法分析","語義分析"。最重要的是要"
生成中間代碼IR"。
3: 后端 : LLVM在這里會"通過一個一個的Pass去優(yōu)化傳入的IR",每個Pass做一些事情,最終生成匯編代碼。
4: 生成匯編代碼。
5: 鏈接 : "鏈接需要的動態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件"。
6: 最后一步,"通過不同的架構(gòu),生成對應(yīng)的可執(zhí)行文件"。
這個步驟與之前經(jīng)常提及的編譯流程,0~6步分別對應(yīng)著 :
源文件(0)-->預(yù)編譯(1)-->編譯(2)-->匯編(3,4)-->鏈接(5)-->生成可執(zhí)行文件(6)
2.2 預(yù)處理階段
在2.1的總流程中說過,預(yù)處理階段要做的事情有兩件 :
- 宏的替換
- 頭文件的導(dǎo)入
舉例
- 打開
工程1,定義一個宏#define JD_NUM 10。- 因為
Xcode自帶的頭文件引入#import <Foundation/Foundation.h>是導(dǎo)入Foundation框架,Foundation框架太大了而且現(xiàn)在我們不需要用,所以頭文件引用就換成#import <stdio.h>。commond + s保存一下。- 打開
terminal終端,進入main.m所在的文件夾下。- 鍵入
clang指令查看預(yù)處理階段的詳細步驟。命令如下 (詳細的Clang命令解釋可以看上一節(jié)中的Clang常用指令)。- 操作圖如下圖2.2.0

Clang命令 :
clang -E main.m >> main2.m
解釋 :
現(xiàn)在
main.m的文件夾下就會出現(xiàn)main2.m文件,它就是經(jīng)過預(yù)處理階段操作之后的結(jié)果。如下圖2.2.1。

- 打開
main2.m文件,拉到文件的最后,找到main函數(shù)入口。結(jié)果如下圖2.2.2

問 : typedef是不是預(yù)處理階段進行的處理?
其實這里通過對
#define和typedef本身的概念了解就知道是不一樣的,typedef本身是存儲類關(guān)鍵字,本質(zhì)上并不屬于宏或頭文件。預(yù)處理階段并不會對關(guān)鍵字做解釋。
簡單驗證一下 :
- 在
工程1中加入typedef int JD_USE_INT,將int類型創(chuàng)建別名為JD_USE_INT。以后工程1改叫工程2。commons + s保存工程2的代碼。- 依然使用
clang -E main.m >> main2.m指令,得到main2.m。- 打開
main2.m直接找到文本最后的main函數(shù)入口。- 操作圖如下圖2.2.3
- 結(jié)果圖如下圖2.2.4。


2.3 編譯階段
編譯階段的主要任務(wù)有3個 :
- 詞法分析 : 將預(yù)處理階段傳過來的源碼的字符序列一個一個的讀入源程序,然后根據(jù)構(gòu)詞規(guī)則轉(zhuǎn)換成單詞序列(
Token)。- 語法分析 : 在詞法分析的基礎(chǔ)上,將單詞序列組合成各類語法短句。例如 : 程序、語句、表達式等。然后將所有的語句節(jié)點抽象出來,生成
抽象語法樹(AST),再檢查源程序的結(jié)構(gòu)是否符合語法規(guī)則。- 生成中間代碼
IR: 完成上述步驟以后,代碼生成器會將抽象語法樹(AST)自上而下的遍歷,逐步將其轉(zhuǎn)換成LLVM IR。
舉例
1. 詞法分析
terminal終端cd進入新的工程2的main.m所在文件夾下。- 輸入以下
clang指令,查看詞法分析。- 源代碼圖為上圖2.2.3,
clang結(jié)果圖為下圖2.3.0- 這里注意,
空格也算一個位置。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

2. 語法分析
terminal終端cd進入工程2的main.m所在文件夾下。- 輸入以下
clang指令,查看語法分析。- 結(jié)果如下圖2.3.1
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

FunctionDecl: 方法節(jié)點
<line:7:1, line:13:1>:
方法節(jié)點的代碼范圍是第7行第1個字符到第13行第1個字符。line:7:5 main 'int (int, const char **)':
從第7行第5個字符的位置開始,是main方法的位置,第一個int表示main方法的返回值類型,(int, const char **)表示main方法的參數(shù)類型。
ParmVarDecl: 參數(shù)節(jié)點
<col:10, col:14>:
參數(shù)節(jié)點因為與main方法在同一行,所以不再說明是第7行。
直接說明第一個參數(shù)的位置是第10個字符開始,到第14個字符為止。argc 'int':
參數(shù)名稱是argc,參數(shù)類型是int。- 上述是第一個參數(shù)的解釋,下面的第二個參數(shù)相同,不再贅述。
CompoundStmt: 圍欄,也可以說是范圍,代表的就是main方法的{ }函數(shù)塊區(qū)域。
ObjCAutoreleasePoolStmt: 自動釋放池。
VarDecl: 變量節(jié)點。內(nèi)容比較簡單,可以自行理解一下。
CallExpr: 調(diào)用函數(shù),這一行后面的int代表這個函數(shù)的返回值的類型是int。這里借助一下圖片,如下圖2.3.2。

BinaryOperator: 函數(shù)的第二個參數(shù),這叫字節(jié)運算符。這一行表示這個字節(jié)運算符是加法運算。表明函數(shù)的第二個參數(shù)是一個加法運算的結(jié)果。再看下圖2.3.3

ReturnStmt: 返回節(jié)點。IntegerLiteral: 整型。- 語法分析階段會對代碼中的錯誤進行提示。例如將
工程1的代碼去掉一個;,重新運行語法分析的clang指令,結(jié)果如下圖2.3.4,明顯提示少了一個;,在第10行的第42個字符處。

3. 生成中間代碼LLVM IR
IR的基本語法
| 語法 | 釋義 |
|---|---|
| ; | 注釋 |
| @ | 全局標識 |
| % | 局部標識 |
| alloca | 開辟空間 |
| align | 內(nèi)存對齊 |
| i32 | 32bit,4字節(jié) |
| store | 寫入內(nèi)存 |
| load | 載入內(nèi)存 |
| call | 調(diào)用函數(shù) |
| ret | 返回 |
操作
- 修改
工程2的代碼,多添加一個函數(shù),方便把所有的IR代碼的語法都了解一遍。新的工程命名為工程3。工程3代碼如下 :
#import <stdio.h>
int sumFunc(int a, int b) {
return a + b + 3;
}
int main(int argc, const char * argv[]) {
int c = sumFunc(1, 2);
printf("%d",c);
return 0;
}
commond + S保存工程3的代碼。terminal終端cd進入工程3所在文件夾下。- 輸入以下
clang指令,生成IR文件。clang指令執(zhí)行完成后,會在main.m文件所在的文件夾下生成main.ll文件。- 生成
main.ll的結(jié)果如下圖2.3.5。
clang -S -fobjc-arc -emit-llvm main.m

- 可以利用
Sublime Text打開main.ll文件,并將Sunlime Text軟件右下角的Plain Text改成Objective-C的格式。結(jié)果如下圖2.3.6。

2.4 優(yōu)化器
我們知道了Clang是LLVM的前端,Clang做了2.2預(yù)處理階段和2.3編譯階段的事情,那么從哪里開始算是LLVM的后端?
優(yōu)化器
(Optimizer)和代碼生成器(CodeGenerator)都可以算作LLVM后端。
- 后端的作用 :
(1). 優(yōu)化。
將2.3編譯階段最后生成的LLVM IR代碼傳入一個一個的Pass進行IR優(yōu)化,每個Pass都會對傳入的IR進行本Pass要做的優(yōu)化。
(2). 生成匯編代碼。
完成所有所需Pass優(yōu)化的IR將會變成匯編代碼。- 什么是
Pass?
(1). 首先,Pass是節(jié)點。是LLVM優(yōu)化過程中的優(yōu)化邏輯所在之處。
(2). 其次,Pass是屬于LLVM的后端(Backend)的。
(3). 最后,LLVM的優(yōu)化是以節(jié)點(Pass)來完成的,是一個節(jié)點一個節(jié)點去完成的,所有節(jié)點一起合作之后,才完成了LLVM的優(yōu)化的轉(zhuǎn)化。
例如 : 有的節(jié)點是負責運算之后將冗余的代碼減去的,有的節(jié)點則是負責跳轉(zhuǎn)之后再減去冗余代碼的。- 什么是
bitCode?
(1). 蘋果在xcode7之后可以開啟bitCode,在iOS中,我們說bitCode是蘋果對LLVM在編譯階段生成的IR的一種特殊形式,本質(zhì)上bitCode也是IR,也是中間代碼,它以二進制形式存在,蘋果推出bitCode就是一種官方的優(yōu)化方式。
(2). 在經(jīng)過bitCode的優(yōu)化之后,IR代碼文件會轉(zhuǎn)化成.bc文件格式的中間代碼。
舉例
很明顯,通過2.3編譯階段生成的IR在閱讀理解上是很冗余的,短短的幾行簡單的代碼都變得很長,所以LLVM中存在對IR代碼進行一些適當?shù)膬?yōu)化,當然這個優(yōu)化在xcode上面是可選擇的。還是選擇以工程3為基本,如圖2.4.0。

xcode是帶有對IR代碼是否進行優(yōu)化的可視化界面的,一般情況下,Debug模式下默認都是沒有開啟代碼優(yōu)化,而Release模式下,則開啟了優(yōu)化。
4.1 LLVM的優(yōu)化級別
| 級別 | 釋義 |
|---|---|
| O0 | None,不進行IR優(yōu)化 |
| O1 | Fast |
| O2 | Faster |
| O3 | Fastest |
| Os | Fastest , Smallest |
| Ofast | 比Os還要更近一步的優(yōu)化 |
| Oz | 讓IR代碼體積最小的優(yōu)化 |
注釋 : 級別的中的
O是英文字母,不是數(shù)字0。
4.2 利用命令行對IR進行優(yōu)化的舉例
還是利用工程3,我們就不直接利用xcode的優(yōu)化了,為了看到優(yōu)化的IR代碼,利用終端的命令行對IR代碼進行優(yōu)化。
- 利用
終端,進入到工程3的main.m所在文件夾下。- 在
終端中輸入以下clang命令
clang -Os -S -fobjc-arc -emit-llvm main.m
- 依然利用
Sublime Text打開main.ll文件,調(diào)整成OC的語法格式。
- 結(jié)果如下圖2.4.1所示。

4.3 bitCode的生成
還是利用
工程3。
- 利用
終端進入工程3的main.m所在的文件夾下。- 在
終端中輸入以下clang命令,先生成IR的main.ll文件。
clang -S -fobjc-arc -emit-llvm main.m
- 再在
終端中輸入以下clang命令,利用main.ll文件生成main.bc文件。
clang -emit-llvm -c main.ll -o main.bc
- 生成的結(jié)果如下圖2.4.2所示。

2.5 匯編
2.5.1 直接生成匯編
直接利用上面
圖2.4.2中的3個文件。
.m格式的源文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.m -o main.s
.ll格式的IR代碼文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.ll -o main1.s
.bc格式的bitCode優(yōu)化后的文件轉(zhuǎn)化為匯編代碼,利用下述命令。
clang -S -fobjc-arc main.bc -o main2.s
結(jié)果如下圖2.5.0和2.5.1所示


2.5.2 生成匯編可進行優(yōu)化
生成匯編進行的優(yōu)化是對機器語言的優(yōu)化。
我們已經(jīng)知道,源碼變成匯編的過程要經(jīng)過 : 源碼 --> IR --> bitcode --> 匯編,其實除了在源碼 --> IR的時候可以進行優(yōu)化,在生成匯編的時候,系統(tǒng)還是會進行一步優(yōu)化,我們在上一節(jié)的傳統(tǒng)優(yōu)化器的設(shè)計中說過后端/代碼生成器也有優(yōu)化能力。
還是利用
工程3的源碼。并且優(yōu)化的級別統(tǒng)一選定為最高級別Os,其他的級別自行更換嘗試。
- 源碼直接生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.m -o main3.s
對比main.m未經(jīng)過優(yōu)化和經(jīng)過優(yōu)化分別生成的匯編main.s和main3.s :

IR生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.ll -o main4.s

bc生成匯編的優(yōu)化
clang -Os -S -fobjc-arc main.bc -o main5.s

因為我們的源碼只有最簡單的11行,所以優(yōu)化的效果不會有那么的大,但也可以看得出來優(yōu)化的效果還是很好的。
但是?。?!這里我們正常的情況下是不可以手動的進行調(diào)節(jié)的。
對比
IR的優(yōu)化來看,IR的優(yōu)化我們可以在xcode中就可以進行配置,就是上面的圖2.4.0,而生成匯編的時候進行的優(yōu)化,我們沒有辦法人工的干預(yù)。
2.6 生成目標文件和生成可執(zhí)行文件(鏈接)
以下所有的操作都是以工程3為基礎(chǔ)的。
2.6.1 生成目標文件
目標文件的生成是匯編器以
匯編代碼作為輸入,將匯編代碼轉(zhuǎn)換成機器代碼,最后輸出目標文件(object file)。
常用命令是 :
clang -fmodules -c main.s -o main.o
命令結(jié)果 :

查看目標文件main.o的符號的命令 :
xcrun nm -nm main.o
命令結(jié)果 :

undefined: 表示在當前文件,暫時找不到某個符號,比如在上圖2.6.1中就是說找不到_printf這個符號,也就是找不到printf這個方法。
external: 表示這個符號是外部可以訪問的。比如上圖的2.6.1中找不到的_printf這個符號是可以在外部訪問的到的,也就是說printf這個方法不是本文件的方法,但是是可以經(jīng)過外部的文件找得到的方法。
2.6.2 生成可執(zhí)行文件(鏈接)
我們知道,可執(zhí)行文件的生成就是由很多的.o文件來完成的。這些.o文件要集合在一起需要要存在一些的聯(lián)系,而這個聯(lián)系就是由鏈接(linker)來做到的。
連接器把編譯產(chǎn)生的.o文件和.dylib或.a文件生成一個mach-o文件。
用下述命令生成可執(zhí)行文件 :
clang main.o -o main
生成可執(zhí)行文件的結(jié)果 :

鏈接之后,我們再查看可執(zhí)行文件的符號,對比目標文件來看。
查看可執(zhí)行文件的符號的命令 :
xcrun nm -nm main
結(jié)果圖 :

從圖3.6.3中可以看到,雖然undefined標識是依然存在的,但是后面的括號中已經(jīng)告訴我們_printf符號是來自于libSystem的。
那為什么要有這個from libSystem呢?
因為當這個可執(zhí)行文件
main要被執(zhí)行的時候,main內(nèi)部有一個符號_printf是來自于外部,當要調(diào)用這個_printf的時候,dyld會在加載的時候進行綁定,而如何綁定呢?就會根據(jù)符號提供的位置,也就是(from libSystem)來確定_printf符號是來自于libSystem的,這時iOS的操作系統(tǒng)中的libSystem動態(tài)庫就會把_printf的地址告訴dyld,然后進行符號的綁定。所以說,這個符號是在運行的時候動態(tài)綁定的。這也是為什么
fishhook可以去hook一些外部函數(shù)的原因。
當main這個可執(zhí)行文件生成之后,我們就可以直接執(zhí)行這個main,命令行如下 :
./main
結(jié)果如下圖 :

也可以查看一下main的基本信息,比如它的格式、版本信息、運行所需的系統(tǒng)要求等,命令行 :
file main
結(jié)果如下圖 :

可以看到main的文件格式是Mach-O,是64位的x86架構(gòu)下可運行的,也就是說main是一個單一架構(gòu)的文件不是胖二進制文件。