1. LLVM概念
1.1 編譯器
LLVM
與編譯器息息相關(guān)缓呛,究竟什么是編譯器呢蜡娶?帶著疑問往下看吧坟奥。
編譯器就是將一種語言
(通常為高級語言
)翻譯為另一種語言
(通常為低級語言
的程序免都。一個現(xiàn)代編譯器的主要工作流程:源代碼(source code)
→ 預(yù)處理器(preprocessor)
→ 編譯器(compiler)
→ 目標(biāo)代碼(object code)
→ 鏈接器(Linker)
→ 可執(zhí)行程序(executables)
源代碼一般為高級語言
(High-level language
)驯妄, 如C
食侮、C++
号涯、Java
、Objective-C
等或匯編語言
疙描,而目標(biāo)則是機(jī)器語言的目標(biāo)代碼
(Object copy
)诚隙,有時也稱作機(jī)器代碼
(Machine code
)。
-
解釋型語言
引入一個Python
代碼起胰,見下圖:
創(chuàng)建Python案例
創(chuàng)建一個FirstDemo.py
文件久又,里面只有一行代碼,print('Hello world for first time')
效五。通過解釋器指令python
地消,解釋這段代碼:
解析Python案例
通過上面的流程可以發(fā)現(xiàn)解釋型語言
的運(yùn)行流程:
解釋型語言編譯流程
解釋型語言特點(diǎn):邊解釋,邊執(zhí)行畏妖,運(yùn)行速度慢脉执,部分改動無需整體重新編譯,不可脫離解釋器環(huán)境運(yùn)行
戒劫。 -
編譯型語言
引入一個C
代碼半夷,見下圖:
創(chuàng)建C語言案例
創(chuàng)建一個firstDemoForC.c
文件,里面添加了一個main
函數(shù)迅细。首先通過clang
去讀取這個代碼:
讀取C代碼
讀取之后發(fā)現(xiàn)代碼并沒有立刻執(zhí)行巫橄,而是生成了一個a.out
文件。這個文件就是可執(zhí)行文件
茵典。通過./a.out
執(zhí)行這段代碼:
a.out執(zhí)行
通過上面的流程可以發(fā)現(xiàn)編譯型語言
的運(yùn)行流程:
編譯型語言運(yùn)行流程
編譯型語言特點(diǎn):先整體編譯湘换,再執(zhí)行,運(yùn)行速度快统阿,任意改動需重新編譯彩倚,可脫離編譯環(huán)境運(yùn)行。
注意:編譯型語言
編譯是先將代碼編譯成cpu
可讀的懂的二進(jìn)制
才能執(zhí)行
1.2 LLVM概述
LLVM
是構(gòu)架編譯器
(compiler
)的框架系統(tǒng)扶平,以C++
編寫而成帆离,用于優(yōu)化以任意程序語言編寫的程序的編譯時間
(compile-time
)、鏈接時間
(link-time
)蜻直、運(yùn)行時間
(run-time
)以及空閑時間
(idle-time
)盯质,對開發(fā)者保持開放袁串,并兼容已有腳本概而。
目前LLVM
已經(jīng)被Apple
呼巷、Microsoft
、Google
赎瑰、Facebook
等各大公司采用王悍。
1.2.1 傳統(tǒng)編譯器的設(shè)計(jì)
編譯器前端(
Frontend
)
編譯器前端的任務(wù)是解析源代碼
。它會進(jìn)行:詞法分析
餐曼、語法分析
压储、語義分析
,檢查源代碼是否存在錯誤源譬,然后構(gòu)建抽象語法樹
集惋,LLVM
的前端會生成中間代碼IR
。優(yōu)化器(
Optimizer
)
優(yōu)化器負(fù)責(zé)進(jìn)行各種優(yōu)化踩娘。改善運(yùn)行時間刮刑,例如消除冗余計(jì)算等。后端(
Backend
)
也可以叫代碼生成器
(CodeGenerator
)养渴,將代碼映射到目標(biāo)指令集
雷绢。生成機(jī)器語言
,并且進(jìn)行機(jī)器相關(guān)的代碼優(yōu)化
理卑。
補(bǔ)充:隨著高級語言越來越多翘紊,終端類型種類的增加,所使用的的CPU
架構(gòu)等也不盡相同藐唠,所以為了適配多種環(huán)境帆疟,不得不設(shè)計(jì)不同的編譯器,而這些編譯器前端
和后端
往往是捆綁在一起的宇立。
1.2.2 LLVM
的設(shè)計(jì)思路
LLVM
的設(shè)計(jì)之初踪宠,即將編譯器前端
(Frontend
)和后端
(Backend
)進(jìn)行了分離
。將前端和后端針對不同的架構(gòu)泄伪,按照獨(dú)立的項(xiàng)目進(jìn)行研發(fā)殴蓬,而它們均采用通用的代碼形式IR
。
當(dāng)編譯器決定支持多種語言或多種硬件架構(gòu)時蟋滴,
LLVM
最重要的地方就體現(xiàn)出來了染厅,使用通用的代碼表示形式(IR
),它是用來在編譯器中表示代碼的形式津函。所以LLVM
可以為任何編程語言獨(dú)立編寫前端肖粮,并且可以為任意硬件架構(gòu)獨(dú)立編寫后端。
1.2.3 iOS
編譯架構(gòu)
Objective C/C/C++
使用的編譯器前端是Clang
尔苦,Swift
是Swift
涩馆,后端都是LLVM
行施。
1.3 Clang
Clang
是LLVM
項(xiàng)目中的一個子項(xiàng)目
。它是基于LLVM
架構(gòu)的輕量級編譯器魂那,誕生之初是為了替代GCC
蛾号,提供更快的編譯速度。它是負(fù)責(zé)編譯C
涯雅、C++
鲜结、Objective-C
語言的編譯器,它屬于整個LLVM
架構(gòu)中活逆,編譯器前端
精刷。對于開發(fā)者來說,研究Clang
可以給我們帶來很多好處蔗候。
2. 編譯流程詳解
通過命令可以打印源碼的編譯階段怒允。引入下面一個案例,main.m
中添加代碼:
int main(int argc, const char * argv[]) {
return 0;
}
通過指令clang -ccc-print-phases main.m
锈遥,查看編譯流程:
流程說明:
0.輸入文件:找到源文件
1.
預(yù)處理階段
:這個過程處理包括宏的替換
纫事,頭文件的導(dǎo)入
2.
編譯階段
:進(jìn)行詞法分析
、語法分析
迷殿、檢測語法是否正確儿礼,最終生成IR
3.后端:這里
LLVM
會通過一個一個的Pass
(節(jié)點(diǎn)
)去優(yōu)化,每個Pass
做一些事情庆寺,最終生成匯編代碼
4.生成目標(biāo)文件
5.
鏈接
:鏈接需要的動態(tài)庫
和靜態(tài)庫
蚊夫,生成可執(zhí)行文件
6.通過
不同的架構(gòu)
,生成對應(yīng)的可行文件
2.1 預(yù)處理
執(zhí)行如下指令:clang -E main.m
懦尝,對源代碼進(jìn)行預(yù)處理
知纷。見下面流程:
在預(yù)處理之后,輸出
mainE.m
文件陵霉,查看mainE.m
文件:打開
mainE.m
源文件會發(fā)現(xiàn)琅轧,其進(jìn)行了宏的替換
,如上面案例中宏c
直接替換成了30
踊挠;進(jìn)行頭文件的導(dǎo)入乍桂。
2.2 編譯階段
2.2.1 詞法分析
預(yù)處理完成后就會進(jìn)行詞法分析,這里會把代碼切成一個個Token
效床,比如大小括號
睹酌,等于號
以及字符串
等。詞法分析指令為:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
參考下面的案例:
通過指令的輸出可以看到剩檀,語法分析會將源碼進(jìn)行
切割并檢測
憋沿。比如分號
,逗號
沪猴,int
等辐啄。
2.2.2 語法分析
詞法分析完成之后就是語法分析采章,它的任務(wù)是驗(yàn)證語法是否正確
。在詞法分析的基礎(chǔ)上將單詞序列組合成各類語法短語壶辜,如:程序
悯舟,語句
,表達(dá)式
等等士复,然后將所有節(jié)點(diǎn)組成抽象語法樹
(Abstract Syntax Tree图谷,AST
)翩活。語法分析程序判斷源程序在結(jié)構(gòu)上是否正確
阱洪。語法分析指令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
參考下面的案例:
通過上面的輸出可以發(fā)現(xiàn),其是一個樹結(jié)構(gòu)菠镇,比如下面的
FunctionDecl
冗荸,表示一個方法,在源碼的第五行利耍,名稱為main
蚌本,返回值為int
,傳入兩個參數(shù)一個是int
隘梨,一個是const char **
程癌。見下圖:注意:一旦生成抽象語法樹,如果源碼中存在語法錯誤轴猎,就會報(bào)錯嵌莉,而上面的預(yù)處理和詞法分析不會報(bào)錯。
如在源碼中設(shè)置一個語法錯誤捻脖,通過語法分析指令進(jìn)行進(jìn)行編譯锐峭,就會報(bào)錯,見下面案例:
2.2.3 生成中間代碼IR(intermediate representation)
完成以上步驟后可婶,就開始生成中間代碼IR
了沿癞,代碼生成器(Code Generation
)會將語法樹自頂向下遍歷逐步翻譯成LLVM IR
。通過下面指令可以生成.ll
的文本文件矛渴,查看IR
代碼椎扬。
clang -S -fobjc-arc -emit-llvm main.m
通過上面的指令獲取main.ll
文件,其結(jié)構(gòu)見下圖:
-
IR
的基本語法-
@
全局標(biāo)識 -
%
局部標(biāo)識 -
alloca
開辟空間 -
align
內(nèi)存對齊 -
i32 32
個bit
具温,4
個字節(jié) -
store
寫入內(nèi)存 -
load
讀取數(shù)據(jù) -
call
調(diào)用函數(shù) -
ret
返回
-
IR
優(yōu)化
上面生成的IR
代碼是沒有經(jīng)過優(yōu)化
的蚕涤,其實(shí)我們在平時閱讀代碼時,經(jīng)常會看下面的一些定義:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
-
fastpath
:可以理解為快速流程
桂躏,對更有可能執(zhí)行的流程進(jìn)行優(yōu)化
钻趋,提高運(yùn)行速度;
slowpath
:基本流程剂习,不被優(yōu)化的蛮位。
在XCode
中也有相應(yīng)的優(yōu)化設(shè)置入口:
LLVM
的優(yōu)化級別分別是-O0 -O1 -O2 -O3 -Os
(第一個是大寫英文字母O
)较沪。可以通過下面的指令獲取優(yōu)化后的IR
代碼失仁,也就是.ll文件:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
通過以上指令生成優(yōu)化后IR
代碼如下:
好明顯尸曼,相較于優(yōu)化前,代碼精簡了很多萄焦。
這里需要注意的是控轿,通常debug
模式下,優(yōu)化模式選擇None -O0
拂封,也就是不優(yōu)化茬射,避免一些保留代碼被屏蔽
,從而影響調(diào)試
冒签。而release
模式設(shè)置為Fastest,Smallest -Os
在抛。
-
bitCode
Xcode7
以后開啟bitCode
蘋果會做進(jìn)一步的優(yōu)化,生成.bc
的中間代碼萧恕。我們通過優(yōu)化后的IR
代碼生成.bc
代碼刚梭。對應(yīng)指令為:
clang -emit-llvm -c main.ll -o main.bc
2.3 后端生成匯編代碼
我們通過最終的.bc
或者.ll
代碼生成匯編代碼
:
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
生成匯編代碼也可以進(jìn)行優(yōu)化:
clang -Os -S -fobjc-arc main.m -o main.s
采用相同的案例,分別三種方式生成匯編代碼票唆,可以看到其優(yōu)化效果朴读。在進(jìn)行IR
優(yōu)化后生成的.ll
文件,依然可以進(jìn)行優(yōu)化生成回應(yīng)的匯編代碼走趋。在不同的節(jié)點(diǎn)上都可以進(jìn)行優(yōu)化衅金。見下圖:
2.4 生成目標(biāo)文件(匯編器)
目標(biāo)文件的生成,是匯編器以匯編代碼作為輸入吆视,將匯編代碼轉(zhuǎn)換為機(jī)器代碼
典挑,最后輸出目標(biāo)文件
(object file
)。指令為:
clang -fmodules -c main.s -o main.o
生成目標(biāo)文件啦吧,見下圖:
其中
main.o
文件即為目標(biāo)文件
您觉,但是此時生成的目標(biāo)文件是不可執(zhí)行
的。通過nm
命令授滓,查看下main.o
中的符號:
$xcrun nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
-
_printf
是一個undefifined external
的 -
undefifined
表示在當(dāng)前文件暫時找不到符號_printf
-
external
表示這個符號是外部可以訪問的
此時就需要鏈接
琳水,鏈接器
把編譯生成的.o
文件和(.dylib .a
)文件鏈接生成一個mach-o
文件。
clang main.o -o main
生成對應(yīng)的可執(zhí)行文件般堆,見下圖:
查看鏈接之后的符號:
$xcrun nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
000000100000f6d (__TEXT,__text) external _test
000000100000f77 (__TEXT,__text) external _main
可以發(fā)現(xiàn)此時的外部函數(shù)有2
個在孝,_printf
和dyld_stub_binder
,它們都來自libSystem
庫淮摔。dyld_stub_binder
這個函數(shù)的作用是進(jìn)行運(yùn)行時綁定流程
私沮。
鏈接是在編譯時,用來確定外部函數(shù)來自哪個動態(tài)庫和橙;綁定是在運(yùn)行時仔燕,將對應(yīng)方法的實(shí)現(xiàn)地址與符號進(jìn)行綁定造垛。
可執(zhí)行文件運(yùn)行結(jié)果: