LLVM是什么?
LLVM 是編譯器工具鏈技術(shù)的一個(gè)集合。而其中的 lld 項(xiàng)目,就是內(nèi)置鏈接器。
編譯器會(huì)對(duì)每個(gè)文件進(jìn)行編譯,生成 Mach-O(可執(zhí)行文件);鏈接器會(huì)將項(xiàng)目中的多個(gè)Mach-O 文件合并成一個(gè)。
Xcode運(yùn)行的過(guò)程就是執(zhí)行一些命令腳本,下面的截圖是Xcode編譯main.m的腳本,在bin目錄下找到clang命令 在后面加一些參數(shù) 比如什么語(yǔ)言 編譯到哪些架構(gòu)上,追加在Xcode設(shè)置的配置的參數(shù),最后輸出成.o文件。

LLVM 編譯器架構(gòu)

編譯器分為三部分,編譯器前端、通用優(yōu)化器、編譯器后端,中間的優(yōu)化器是不會(huì)變的
增加一種語(yǔ)言只需要處理好編譯器前端就行了
增加一種架構(gòu),只需要添加一種編譯器后端的架構(gòu)處理就可以了
clang在編譯器架構(gòu)中表示 C、C++、Objective-C的前端,在命令行中也作為一個(gè)“黑盒”的Driver,封裝了編譯管線、前端命令、LLVM命令、Toolchain命令等。
LLVM會(huì)執(zhí)行上述的整個(gè)編譯流程,大體流程如下:
- 你寫(xiě)好代碼后,LLVM會(huì)預(yù)處理你的代碼,比如把宏嵌入到對(duì)應(yīng)的位置。
- 預(yù)處理完后,LLVM 會(huì)對(duì)代碼進(jìn)行詞法分析和語(yǔ)法分析,生成 AST 。AST 是抽象語(yǔ)法樹(shù),結(jié)構(gòu)上比代碼更精簡(jiǎn),遍歷起來(lái)更快,所以使用 AST 能夠更快速地進(jìn)行靜態(tài)檢查,同時(shí)還能更快地生成 IR(中間表示)
- 最后 AST 會(huì)生成 IR,IR 是一種更接近機(jī)器碼的語(yǔ)言,區(qū)別在于和平臺(tái)有關(guān),通過(guò) IR 可以生成多份適合不同平臺(tái)的機(jī)器碼。對(duì)于 iOS 系統(tǒng),IR 生成的可執(zhí)行文件就是 Mach-O。
OC源文件的編譯過(guò)程
使用以下命令,查看OC源文件的編譯過(guò)程
clang -ccc-print-phases main.m

0:先找到main.m文件
1:預(yù)處理器,就是把include、import、宏定義給替換掉
2:編譯成IR中間代碼
3:把中間代碼給后端,生成匯編代碼
4:匯編生成目標(biāo)代碼
5:鏈接靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù)
6:適合某個(gè)架構(gòu)的代碼
預(yù)處理
使用以下命令,可以查看預(yù)處理階段所做的工作
clang -E main.m
預(yù)處理主要做了以下幾件事情:
1、刪除所有的#define,代碼中使用宏定義的地方會(huì)進(jìn)行替換
2、將#include包含的文件插入到文件的位置,這個(gè)插入的過(guò)程是遞歸的
3、刪除掉注釋符號(hào)及注釋
4、添加行號(hào)和文件標(biāo)識(shí),便于調(diào)試
編譯
編譯的過(guò)程就是把預(yù)處理后的文件進(jìn)行 詞法分析、語(yǔ)法分析、語(yǔ)義分析及優(yōu)化后產(chǎn)生相應(yīng)的匯編代碼
1、詞法分析
這一步把源文件中的代碼轉(zhuǎn)化為特殊的標(biāo)記流,源碼被分割成一個(gè)一個(gè)的字符和單詞,在行尾Loc中都標(biāo)記出了源碼所在的對(duì)應(yīng)源文件和具體行數(shù),方便在報(bào)錯(cuò)時(shí)定位問(wèn)題。
使用以下命令來(lái)進(jìn)行詞法分析
clang -Xclang -dump-tokens main.m
以下面這段代碼為例:

第11行的這段源碼
int main(int argc, char * argv[]) {
通過(guò)詞法分析,會(huì)轉(zhuǎn)化為以下的特殊標(biāo)記
int 'int' [StartOfLine] Loc=<main.m:11:1>
identifier 'main' [LeadingSpace] Loc=<main.m:11:5>
l_paren '(' Loc=<main.m:11:9>
int 'int' Loc=<main.m:11:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:11:14>
comma ',' Loc=<main.m:11:18>
char 'char' [LeadingSpace] Loc=<main.m:11:20>
star '*' [LeadingSpace] Loc=<main.m:11:25>
identifier 'argv' [LeadingSpace] Loc=<main.m:11:27>
l_square '[' Loc=<main.m:11:31>
r_square ']' Loc=<main.m:11:32>
r_paren ')' Loc=<main.m:11:33>
l_brace '{' [LeadingSpace] Loc=<main.m:11:35>
2、語(yǔ)法分析
這一步就是根據(jù)詞法分析的標(biāo)記流,解析成一個(gè)語(yǔ)法樹(shù),在Clang中由Parser和Sema兩個(gè)模塊配合完成
在這里面每一個(gè)節(jié)點(diǎn)也都標(biāo)記了自己在源碼中的位置
驗(yàn)證語(yǔ)法是否正確,比如少一個(gè);報(bào)一個(gè)錯(cuò)誤提示
根據(jù)當(dāng)前語(yǔ)言的語(yǔ)法,生成語(yǔ)義節(jié)點(diǎn),并將所有的節(jié)點(diǎn)組合成抽象語(yǔ)法樹(shù)
使用以下命令來(lái)進(jìn)行語(yǔ)法分析
clang -Xclang -ast-dump -fsyntax-only main.m
會(huì)解析成以下的語(yǔ)法樹(shù)
-FunctionDecl 0x7ffe251a8ce0 <main.m:11:1, line:20:1> line:11:5 main 'int (int, char **)'
|-ParmVarDecl 0x7ffe251a8b00 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7ffe251a8bc0 <col:20, col:32> col:27 argv 'char **':'char **'
`-CompoundStmt 0x7ffe251a9200 <col:35, line:20:1>
|-ObjCAutoreleasePoolStmt 0x7ffe251a91b8 <line:13:5, line:18:5>
| `-CompoundStmt 0x7ffe251a9188 <line:13:22, line:18:5>
| |-DeclStmt 0x7ffe251a8e30 <line:14:9, col:32>
| | `-VarDecl 0x7ffe251a8da8 <col:9, line:9:21> line:14:13 used eight 'int' cinit
| | `-IntegerLiteral 0x7ffe251a8e10 <line:9:21> 'int' 8
| |-DeclStmt 0x7ffe251a8ee8 <line:15:9, col:20>
| | `-VarDecl 0x7ffe251a8e60 <col:9, col:19> col:13 used six 'int' cinit
| | `-IntegerLiteral 0x7ffe251a8ec8 <col:19> 'int' 6
| |-DeclStmt 0x7ffe251a9010 <line:16:9, col:31>
| | `-VarDecl 0x7ffe251a8f18 <col:9, col:28> col:13 used rank 'int' cinit
| | `-BinaryOperator 0x7ffe251a8ff0 <col:20, col:28> 'int' '+'
| | |-ImplicitCastExpr 0x7ffe251a8fc0 <col:20> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7ffe251a8f80 <col:20> 'int' lvalue Var 0x7ffe251a8da8 'eight' 'int'
| | `-ImplicitCastExpr 0x7ffe251a8fd8 <col:28> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7ffe251a8fa0 <col:28> 'int' lvalue Var 0x7ffe251a8e60 'six' 'int'
| `-CallExpr 0x7ffe251a9128 <line:17:9, col:30> 'void'
| |-ImplicitCastExpr 0x7ffe251a9110 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7ffe251a9028 <col:9> 'void (id, ...)' Function 0x7ffe20b20e88 'NSLog' 'void (id, ...)'
| |-ImplicitCastExpr 0x7ffe251a9158 <col:15, col:16> 'id':'id' <BitCast>
| | `-ObjCStringLiteral 0x7ffe251a9068 <col:15, col:16> 'NSString *'
| | `-StringLiteral 0x7ffe251a9048 <col:16> 'char [8]' lvalue "rank-%d"
| `-ImplicitCastExpr 0x7ffe251a9170 <col:26> 'int' <LValueToRValue>
| `-DeclRefExpr 0x7ffe251a9088 <col:26> 'int' lvalue Var 0x7ffe251a8f18 'rank' 'int'
`-ReturnStmt 0x7ffe251a91f0 <line:19:5, col:12>
`-IntegerLiteral 0x7ffe251a91d0 <col:12> 'int' 0

3、靜態(tài)分析(通過(guò)語(yǔ)法樹(shù)進(jìn)行代碼靜態(tài)分析,找出非語(yǔ)法性錯(cuò)誤)
1、錯(cuò)誤檢查
如出現(xiàn)方法被調(diào)用但是未定義、定義但是未使用的變量
2、類型檢查
一般會(huì)把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查。編寫(xiě)代碼時(shí)可以向任意對(duì)象發(fā)送任何消息,在運(yùn)行時(shí),才會(huì)檢查對(duì)象是否能夠響應(yīng)這些消息。
4、CodeGen - IR代碼生成
1、CodeGen 負(fù)責(zé)將語(yǔ)法樹(shù)從頂至下遍歷,翻譯成 LLVM IR
2、LLVM IR是Frontend的輸出,也是LLVM Backend的輸入,前后端的橋接語(yǔ)言
3、與Objective-C Runtime 橋接
與Objective-C Runtime 橋接的應(yīng)用
1、在Objective-C中的 Class / Meta Class / Protocol /Category 這些結(jié)構(gòu)體的內(nèi)存結(jié)構(gòu)就是在這一步生成的,并放在了Mach-O指定的Section中(如 Class: _DATA, _objc _classrefs),這個(gè) DATA段也會(huì)存放一些static變量
2、objct對(duì)象發(fā)送一個(gè)消息最終會(huì)編譯成什么樣子啊,會(huì)編譯成objc_msgSend調(diào)用就發(fā)生在這一步,將語(yǔ)法樹(shù)中的ObjCMessageExpr翻譯成相應(yīng)版本的objc_msgSend,對(duì)super關(guān)鍵字的調(diào)用翻譯成objc_msgSendSuper
3、根據(jù)修飾符strong / weak /copy /atomic 合成@property自動(dòng)實(shí)現(xiàn)的getter / setter、處理@synthesize也是這一步做的
4、生成block_layout的數(shù)據(jù)結(jié)構(gòu)、變量的capture(
__block/ 和__weak),生成_block_invoke函數(shù)都發(fā)生在這一步5、之前總說(shuō)ARC是編譯器幫我們插入一些內(nèi)存管理的代碼,具體也是在這一步完成的
ARC: 分析對(duì)象的引用關(guān)系,將objc_StoreStrong / Objc_StoreWeak等ARC代碼的插入
將ObjCAutotreleasePoolStmt轉(zhuǎn)譯成objc_autoreleasePoolPush/Pop
實(shí)現(xiàn)自動(dòng)調(diào)用[super dealloc]
為每個(gè)擁有ivar的Class 合成.cxx_destructor 方法來(lái)自動(dòng)釋放類的成員變量,代替MRC時(shí)代的 “self.xxx = nil”
LLVM的中間產(chǎn)物及優(yōu)化
使用以下命令,生成LLVM中間產(chǎn)物IR(Intermediate Representation),把這個(gè)過(guò)程打印出來(lái)
clang -O3 -S -emit-llvm main.m -o main.ll
使用以下命令,會(huì)使用LLVM對(duì)代碼進(jìn)行優(yōu)化。
//針對(duì)全局變量?jī)?yōu)化、循環(huán)優(yōu)化、尾遞歸優(yōu)化等。
//在 Xcode 的編譯設(shè)置里也可以設(shè)置優(yōu)化級(jí)別-01,-03,-0s,還可以寫(xiě)些自己的 Pass。
clang -emit-llvm -c main.m -o main.bc
生成匯編代碼
使用以下命令,生成相對(duì)應(yīng)的匯編代碼。
clang -S -fobjc-arc main.m -o main.s
至此,編譯階段完成,將書(shū)寫(xiě)代碼轉(zhuǎn)換成了機(jī)器可以識(shí)別的匯編代碼,匯編器是將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令。根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就可以了。
使用以下命令,生成對(duì)應(yīng)的目標(biāo)文件。
clang -fmodules -c main.m -o main.o
后來(lái)的Xcode新建的工程里并沒(méi)有pch文件,為什么呢?
pch文件就是把UIKit、Foundation這些庫(kù)用pch文件import一下,這樣就不用在每個(gè)源文件中去解析這么多東西了,現(xiàn)在iOS這邊亂搞把一些全局的變量,自己模塊的一些東西都放在里面。
Xcode里面出了一個(gè)modules的概念,各個(gè)setting里面也是打開(kāi)的,默認(rèn)把庫(kù)打成一個(gè)modules的形式,尤其是UIKit、Foundation這些庫(kù)全部都是modules,好處就是我加這個(gè)參數(shù)(fmodules)以后它就會(huì)自動(dòng)把#import變成@import,現(xiàn)在的編譯就會(huì)比最早的那種連pch都沒(méi)有的快很多,因?yàn)樗某霈F(xiàn)pch就不會(huì)默認(rèn)出現(xiàn)了
$clang -E -fmodules main.m //加入fmodules參數(shù)生成可執(zhí)行文件
鏈接
這一階段是將上個(gè)階段生成的目標(biāo)文件和引用的靜態(tài)庫(kù)鏈接起來(lái),最終生成可執(zhí)行文件,鏈接器解決了目標(biāo)文件和庫(kù)之間的鏈接。
編譯時(shí)鏈接器做了什么?
1、Mach-O里面主要是代碼和數(shù)據(jù),代碼是函數(shù)的定義,數(shù)據(jù)是全局變量的定義,不管是代碼還是數(shù)據(jù)都是通過(guò)符號(hào)關(guān)聯(lián)起來(lái)的。
2、Mach-O里面的代碼,要操作的變量和函數(shù)要綁定到各自的地址上,鏈接器的作用就是完成變量和函數(shù)的符號(hào)和其地址的綁定。
為什么要做符號(hào)綁定?
1、如果地址和符號(hào)不做綁定的話,要讓機(jī)器知道你在操作什么地址,就需要寫(xiě)代碼的時(shí)候設(shè)置好內(nèi)存地址。
2、可讀性差,修改代碼后要重新對(duì)地址進(jìn)行維護(hù)
3、需要針對(duì)不同平臺(tái)寫(xiě)多份代碼,相當(dāng)于直接寫(xiě)匯編
為什么還要把項(xiàng)目中的多個(gè)Mach-O合并成一個(gè)?
1、多個(gè)文件之間的變量和接口是相互依賴的,就需要鏈接器把項(xiàng)目中多個(gè)Mach-O文件符號(hào)和地址綁定起來(lái)。
2、不綁定的話單個(gè)文件生成的Mach-O就是無(wú)法運(yùn)行的,運(yùn)行時(shí)遇到調(diào)用其他文件的函數(shù)實(shí)現(xiàn)時(shí),就會(huì)找不到函數(shù)地址。
3、鏈接多個(gè)目標(biāo)文件就會(huì)創(chuàng)建一個(gè)符號(hào)表,記錄所有已定義和未定義的符號(hào),如果出現(xiàn)相同符號(hào)的情況,就會(huì)出現(xiàn)“l(fā)d: dumplicate symbols”的錯(cuò)誤信息,如果在目標(biāo)文件中沒(méi)有找到符號(hào),就會(huì)提示“Undefined symbols”的錯(cuò)誤信息。
鏈接器對(duì)代碼主要做了哪幾件事?
1、去代碼文件中查找沒(méi)有定義的變量
2、將所有符號(hào)定義和引用地址收集起來(lái),并放到全局符號(hào)表中
3、計(jì)算合并后的長(zhǎng)度及位置,生成同類型的段進(jìn)行合并,建立綁定
4、對(duì)項(xiàng)目中不同文件里的變量進(jìn)行地址重定位
鏈接器如何去除無(wú)用的函數(shù),保證Mach-O的大???
鏈接器在整理函數(shù)的調(diào)用關(guān)系時(shí),會(huì)以main函數(shù)為源頭跟隨每個(gè)引用并將其標(biāo)記為live,跟隨完成后那些未被標(biāo)記為live的就是無(wú)用函數(shù)。
總結(jié):一個(gè)源文件的編譯過(guò)程

代碼實(shí)踐
#import <Foundation/Foundation.h>
int main() {
NSLog(@"hello world!");
return 0;
}
1、生成Mach-O可執(zhí)行文件
clang -fmodules main.m -o main
2、生成抽象語(yǔ)法樹(shù)
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
3、生成匯編代碼
clang -S main.m -o main.s
裝載與鏈接
一個(gè)App從可執(zhí)行文件到真正啟動(dòng)運(yùn)行代碼,基本需要經(jīng)過(guò)裝載和動(dòng)態(tài)庫(kù)鏈接兩個(gè)步驟。
程序運(yùn)行起來(lái)會(huì)擁有獨(dú)立的虛擬地址空間,在操作系統(tǒng)上會(huì)同時(shí)運(yùn)行多個(gè)進(jìn)程,彼此之間的虛擬地址空間是隔離的。
裝載就是把可執(zhí)行文件映射到虛擬內(nèi)存中的過(guò)程,由于內(nèi)存資源稀缺,只將程序最常用的部分駐留在內(nèi)存里,不太常用的數(shù)據(jù)放在磁盤(pán)里,這也是動(dòng)態(tài)裝載的過(guò)程。
裝載的過(guò)程就是進(jìn)程建立的過(guò)程,操作系統(tǒng)主要做了3件事:
1、創(chuàng)建一個(gè)獨(dú)立的虛擬地址
2、讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系
3、將CPU的寄存區(qū)設(shè)置成可執(zhí)行文件的入口地址,啟動(dòng)運(yùn)行
靜態(tài)庫(kù)
靜態(tài)庫(kù)是編譯時(shí)鏈接的庫(kù),需要鏈接進(jìn)你的Mach-O文件里,如果需要更新就重新編譯一次,無(wú)法動(dòng)態(tài)的加載和更新。
動(dòng)態(tài)庫(kù)
動(dòng)態(tài)庫(kù)是運(yùn)行時(shí)鏈接的庫(kù),使用dyld就可以實(shí)現(xiàn)動(dòng)態(tài)加載,iOS中的系統(tǒng)庫(kù)都是動(dòng)態(tài)鏈接的。
共享緩存
Mach-O是編譯后的產(chǎn)物,而動(dòng)態(tài)庫(kù)在運(yùn)行時(shí)才會(huì)被鏈接,所有Mach-O中并沒(méi)有動(dòng)態(tài)庫(kù)的符號(hào)定義。
Mach-O中動(dòng)態(tài)庫(kù)中的符號(hào)是未定義的,但他們的名字和對(duì)應(yīng)的庫(kù)的路徑會(huì)被記錄下來(lái)。
運(yùn)行時(shí)dlopen 和 dlsym 導(dǎo)入動(dòng)態(tài)庫(kù)時(shí),先根據(jù)記錄的庫(kù)路徑找到對(duì)應(yīng)的庫(kù),再通過(guò)記錄的名字符號(hào)找到綁定的地址。
優(yōu)點(diǎn):
代碼共用、易于維護(hù)、減少可執(zhí)行文件的體積
參考資料:
LLVM框架/LLVM編譯流程/Clang前端/LLVM IR/LLVM應(yīng)用與實(shí)踐