本文為L_Ares個人寫作祖秒,以任何形式轉(zhuǎn)載請表明原文出處。
本文接上一節(jié)——iOS用到的LLVM(一)舟奠。請對LLVM
和Clang
不熟悉的同學(xué)們移步上一節(jié)竭缝,了解了基礎(chǔ)的信息之后再閱讀本節(jié)。
一沼瘫、準(zhǔn)備工作
步驟1 : 使用xcode
新建一個空的macOS
下的commond Line Tool
命令行工具抬纸,下面稱之為工程1
。
注意 :
- 這里因?yàn)橛玫氖敲钚?
commond Line Tool
)耿戚,所以初創(chuàng)的情況下沒有對其他的框架造成依賴湿故。- 因?yàn)闆]有依賴昂芜,所以以下的命令都是不引入其他
iOS框架
的(包括也沒有引入Foundation
框架)秩贰。- 如果想要引入其他的框架,那么就在
clang
命令上添加框架的地址肩祥。下面是舉例的一個命令飞几,引入內(nèi)容按照自己要使用的框架的情況進(jìn)行修改即可砚哆。
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
終端,進(jìn)入到剛創(chuàng)建的這個項(xiàng)目中main.m
所在的文件夾下。
步驟3 : 在terminal
終端中輸入clang
的查看詳細(xì)編譯步驟
的指令躁锁。
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
這個文件從源碼到機(jī)器語言的總的流程。下面開始按照流程來說乡小。
二阔加、源碼的編譯流程
2.1 編譯總流程
命令 :
clang -ccc-print-phases main.m
編譯總流程就是上面的內(nèi)容1
。
先闡述總流程0~6中都是什么 :
0: 輸入文件 : "找到源文件"满钟。
1: 預(yù)處理階段 : 這個階段處理了"宏的替換"和"頭文件的導(dǎo)入"胜榔。
2: 編譯階段 : 進(jìn)行"詞法分析"、"語法分析"湃番,"語義分析"夭织。最重要的是要"
生成中間代碼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
。- 因?yàn)?code>Xcode自帶的頭文件引入
#import <Foundation/Foundation.h>
是導(dǎo)入Foundation框架
壶笼,Foundation框架
太大了而且現(xiàn)在我們不需要用神僵,所以頭文件引用就換成#import <stdio.h>
。commond + s
保存一下拌消。- 打開
terminal終端
挑豌,進(jìn)入main.m
所在的文件夾下安券。- 鍵入
clang
指令查看預(yù)處理階段的詳細(xì)步驟墩崩。命令如下 (詳細(xì)的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ù)處理階段進(jìn)行的處理练对?
其實(shí)這里通過對
#define
和typedef
本身的概念了解就知道是不一樣的遍蟋,typedef
本身是存儲類關(guān)鍵字,本質(zhì)上并不屬于宏或頭文件螟凭。預(yù)處理階段并不會對關(guān)鍵字做解釋虚青。
簡單驗(yà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ǔ)上,將單詞序列組合成各類語法短句宪赶。例如 : 程序宗弯、語句、表達(dá)式等搂妻。然后將所有的語句節(jié)點(diǎn)抽象出來蒙保,生成
抽象語法樹(AST)
,再檢查源程序的結(jié)構(gòu)是否符合語法規(guī)則欲主。- 生成中間代碼
IR
: 完成上述步驟以后邓厕,代碼生成器會將抽象語法樹(AST)
自上而下的遍歷,逐步將其轉(zhuǎn)換成LLVM IR
扁瓢。
舉例
1. 詞法分析
terminal
終端cd
進(jìn)入新的工程2
的main.m
所在文件夾下详恼。- 輸入以下
clang
指令,查看詞法分析引几。- 源代碼圖為上圖2.2.3昧互,
clang
結(jié)果圖為下圖2.3.0- 這里注意,
空格
也算一個位置伟桅。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
2. 語法分析
terminal
終端cd
進(jìn)入工程2
的main.m
所在文件夾下敞掘。- 輸入以下
clang
指令,查看語法分析楣铁。- 結(jié)果如下圖2.3.1
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
FunctionDecl
: 方法節(jié)點(diǎn)
<line:7:1, line:13:1>
:
方法節(jié)點(diǎn)的代碼范圍是第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é)點(diǎn)
<col:10, col:14>
:
參數(shù)節(jié)點(diǎn)因?yàn)榕cmain
方法在同一行溃列,所以不再說明是第7行
劲厌。
直接說明第一個參數(shù)
的位置是第10個字符
開始,到第14個字符
為止听隐。argc 'int'
:
參數(shù)名稱是argc
补鼻,參數(shù)類型是int
。- 上述是第一個參數(shù)的解釋遵绰,下面的第二個參數(shù)相同辽幌,不再贅述。
CompoundStmt
: 圍欄椿访,也可以說是范圍乌企,代表的就是main
方法的{ }
函數(shù)塊區(qū)域。
ObjCAutoreleasePoolStmt
: 自動釋放池成玫。
VarDecl
: 變量節(jié)點(diǎn)加酵。內(nèi)容比較簡單拳喻,可以自行理解一下。
CallExpr
: 調(diào)用函數(shù)猪腕,這一行后面的int
代表這個函數(shù)的返回值的類型是int
冗澈。這里借助一下圖片,如下圖2.3.2陋葡。
BinaryOperator
: 函數(shù)的第二個參數(shù)亚亲,這叫字節(jié)運(yùn)算符。這一行表示這個字節(jié)運(yùn)算符是加法運(yùn)算腐缤。表明函數(shù)的第二個參數(shù)是一個加法運(yùn)算的結(jié)果捌归。再看下圖2.3.3
ReturnStmt
: 返回節(jié)點(diǎn)。IntegerLiteral
: 整型岭粤。- 語法分析階段會對代碼中的錯誤進(jìn)行提示惜索。例如將
工程1
的代碼去掉一個;
,重新運(yùn)行語法分析的clang
指令剃浇,結(jié)果如下圖2.3.4巾兆,明顯提示少了一個;
,在第10行的第42個字符處虎囚。
3. 生成中間代碼LLVM IR
IR的基本語法
語法 | 釋義 |
---|---|
; | 注釋 |
@ | 全局標(biāo)識 |
% | 局部標(biāo)識 |
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
進(jìn)入工程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
進(jìn)行IR
優(yōu)化驼仪,每個Pass
都會對傳入的IR
進(jìn)行本Pass
要做的優(yōu)化掸犬。
(2). 生成匯編代碼。
完成所有所需Pass
優(yōu)化的IR
將會變成匯編代碼
绪爸。- 什么是
Pass
?
(1). 首先湾碎,Pass
是節(jié)點(diǎn)。是LLVM
優(yōu)化過程中的優(yōu)化邏輯所在之處奠货。
(2). 其次介褥,Pass
是屬于LLVM
的后端(Backend
)的。
(3). 最后递惋,LLVM
的優(yōu)化是以節(jié)點(diǎn)(Pass
)來完成的呻顽,是一個節(jié)點(diǎn)一個節(jié)點(diǎn)去完成的,所有節(jié)點(diǎn)一起合作之后丹墨,才完成了LLVM
的優(yōu)化的轉(zhuǎn)化廊遍。
例如 : 有的節(jié)點(diǎn)是負(fù)責(zé)運(yùn)算之后將冗余的代碼減去的,有的節(jié)點(diǎn)則是負(fù)責(zé)跳轉(zhuǎn)之后再減去冗余代碼的贩挣。- 什么是
bitCode
?
(1). 蘋果在xcode7
之后可以開啟bitCode
喉前,在iOS
中,我們說bitCode
是蘋果對LLVM
在編譯階段生成的IR
的一種特殊形式王财,本質(zhì)上bitCode
也是IR
卵迂,也是中間代碼,它以二進(jìn)制形式存在绒净,蘋果推出bitCode
就是一種官方的優(yōu)化方式。
(2). 在經(jīng)過bitCode
的優(yōu)化之后挂疆,IR
代碼文件會轉(zhuǎn)化成.bc
文件格式的中間代碼改览。
舉例
很明顯,通過2.3編譯階段
生成的IR
在閱讀理解上是很冗余的缤言,短短的幾行簡單的代碼都變得很長宝当,所以LLVM
中存在對IR
代碼進(jìn)行一些適當(dāng)?shù)膬?yōu)化,當(dāng)然這個優(yōu)化在xcode
上面是可選擇的胆萧。還是選擇以工程3
為基本庆揩,如圖2.4.0。
xcode
是帶有對IR
代碼是否進(jìn)行優(yōu)化的可視化界面的跌穗,一般情況下订晌,Debug
模式下默認(rèn)都是沒有開啟代碼優(yōu)化,而Release
模式下蚌吸,則開啟了優(yōu)化锈拨。
4.1 LLVM的優(yōu)化級別
級別 | 釋義 |
---|---|
O0 | None,不進(jìn)行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進(jìn)行優(yōu)化的舉例
還是利用工程3
鹤耍,我們就不直接利用xcode
的優(yōu)化了,為了看到優(yōu)化的IR
代碼验辞,利用終端的命令行對IR
代碼進(jìn)行優(yōu)化稿黄。
- 利用
終端
,進(jìn)入到工程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
陵珍。
- 利用
終端
進(jìn)入工程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 生成匯編可進(jìn)行優(yōu)化
生成匯編進(jìn)行的優(yōu)化是對機(jī)器語言的優(yōu)化许起。
我們已經(jīng)知道十偶,源碼變成匯編的過程要經(jīng)過 : 源碼 --> IR --> bitcode --> 匯編,其實(shí)除了在源碼 --> IR
的時候可以進(jìn)行優(yōu)化街氢,在生成匯編的時候扯键,系統(tǒng)還是會進(jìn)行一步優(yōu)化睦袖,我們在上一節(jié)的傳統(tǒng)優(yōu)化器的設(shè)計(jì)中說過后端/代碼生成器也有優(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
因?yàn)槲覀兊脑创a只有最簡單的11行,所以優(yōu)化的效果不會有那么的大董习,但也可以看得出來優(yōu)化的效果還是很好的烈和。
但是!C罅堋招刹!這里我們正常的情況下是不可以手動的進(jìn)行調(diào)節(jié)的恬试。
對比
IR
的優(yōu)化來看,IR
的優(yōu)化我們可以在xcode
中就可以進(jìn)行配置疯暑,就是上面的圖2.4.0训柴,而生成匯編的時候進(jìn)行的優(yōu)化,我們沒有辦法人工的干預(yù)妇拯。
2.6 生成目標(biāo)文件和生成可執(zhí)行文件(鏈接)
以下所有的操作都是以工程3
為基礎(chǔ)的幻馁。
2.6.1 生成目標(biāo)文件
目標(biāo)文件的生成是匯編器以
匯編代碼
作為輸入
,將匯編代碼
轉(zhuǎn)換成機(jī)器代碼
越锈,最后輸出目標(biāo)文件(object file)
仗嗦。
常用命令是 :
clang -fmodules -c main.s -o main.o
命令結(jié)果 :
查看目標(biāo)文件main.o
的符號的命令 :
xcrun nm -nm main.o
命令結(jié)果 :
undefined
: 表示在當(dāng)前文件,暫時找不到某個符號甘凭,比如在上圖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í)行文件的符號刚操,對比目標(biāo)文件來看。
查看可執(zhí)行文件的符號的命令 :
xcrun nm -nm main
結(jié)果圖 :
從圖3.6.3中可以看到再芋,雖然undefined
標(biāo)識是依然存在的菊霜,但是后面的括號中已經(jīng)告訴我們_printf
符號是來自于libSystem
的。
那為什么要有這個from libSystem
呢济赎?
因?yàn)楫?dāng)這個可執(zhí)行文件
main
要被執(zhí)行的時候鉴逞,main
內(nèi)部有一個符號_printf
是來自于外部,當(dāng)要調(diào)用這個_printf
的時候司训,dyld
會在加載的時候進(jìn)行綁定构捡,而如何綁定呢?就會根據(jù)符號提供的位置壳猜,也就是(from libSystem)
來確定_printf
符號是來自于libSystem
的勾徽,這時iOS
的操作系統(tǒng)中的libSystem
動態(tài)庫就會把_printf
的地址告訴dyld
,然后進(jìn)行符號的綁定统扳。所以說喘帚,這個符號是在運(yùn)行的時候動態(tài)綁定的畅姊。這也是為什么
fishhook
可以去hook
一些外部函數(shù)的原因。
當(dāng)main
這個可執(zhí)行文件生成之后吹由,我們就可以直接執(zhí)行這個main
涡匀,命令行如下 :
./main
結(jié)果如下圖 :
也可以查看一下main
的基本信息,比如它的格式溉知、版本信息陨瘩、運(yùn)行所需的系統(tǒng)要求等,命令行 :
file main
結(jié)果如下圖 :
可以看到main
的文件格式是Mach-O
级乍,是64位的x86架構(gòu)下可運(yùn)行的舌劳,也就是說main
是一個單一架構(gòu)的文件不是胖二進(jìn)制文件。