本節(jié),我們給大家介紹一個(gè)偉大的架構(gòu)編譯器LLVM。
- 什么是編譯器
- LLVM概述
- LLVM案例體驗(yàn)
1 什么是編譯器?
1.1 Python案例
- 創(chuàng)建
python文件夾,新建helloDemo.py文件,內(nèi)容如下:
print("hello")
- 調(diào)用
python helloDemo.py執(zhí)行文件,打印出python
image.png
1.2 C 案例
-
vim創(chuàng)建helloDemo.c文件:
#include <stdio.h>
int main(int a, char * argv[]) {
printf("hello \n");
return 0;
}
-
clang helloDemo.c編譯,生成a.out文件。file a.out查看文件:
image.png
發(fā)現(xiàn).out文件是:64位的Mach-O可執(zhí)行文件,當(dāng)前clang出來(lái)的是x86_64架構(gòu), mac電腦可讀。 所以可以./a.out直接執(zhí)行:

Q:
解釋型語(yǔ)言與編譯型語(yǔ)言
python是解釋型語(yǔ)言,一邊翻譯一邊執(zhí)行。和js一樣,機(jī)器可直接執(zhí)行。C語(yǔ)言是編譯型語(yǔ)言,不能直接執(zhí)行,需要編譯器將其轉(zhuǎn)換成機(jī)器識(shí)別語(yǔ)言。
編譯型語(yǔ)言:編譯后輸出的是指令(0、1組合),cpu可直接執(zhí)行指令
解釋性語(yǔ)言:生成的是數(shù)據(jù),不是0、1組合,機(jī)器也能直接識(shí)別
編譯器的作用,就是將高級(jí)語(yǔ)言轉(zhuǎn)化為機(jī)器能夠識(shí)別的語(yǔ)言(可執(zhí)行文件)。
Q:匯編有指令嗎?
早期科學(xué)家,使用
0、1編碼。 比如00001111對(duì)應(yīng)call,00000111對(duì)應(yīng)bl。有了對(duì)應(yīng)關(guān)系后。 再手敲0和1就有點(diǎn)難受了。于是寫個(gè)中間解釋器,我們只用輸入call、bl這樣的標(biāo)記指令,經(jīng)過(guò)解釋器,變成0和1的組合,再交給機(jī)器去執(zhí)行。 這就是匯編的由來(lái)。而基于匯編往上,再
映射和封裝相關(guān)對(duì)應(yīng)關(guān)系。就跨時(shí)代性的c語(yǔ)言,再往上層封裝,就出現(xiàn)了高級(jí)語(yǔ)言oc、swift等語(yǔ)言。所以匯編執(zhí)行快,因?yàn)樗?code>直接轉(zhuǎn)換為機(jī)器語(yǔ)言的。但
匯編的指令集,是針對(duì)同一操作系統(tǒng)而言,它不支持跨平臺(tái)。機(jī)器指令是cpu的在識(shí)別。早期的計(jì)算機(jī)廠家非常多,雖然都用0和1的組合,但相同組合背后卻是相應(yīng)不同的指令。所以匯編無(wú)法跨平臺(tái),不同操作系統(tǒng)下,匯編指令是不同的。
2. LLVM概述
-
LLVM是架構(gòu)編譯器(compiler)的框架系統(tǒng),以c++編寫而成,用于優(yōu)化以任意程序語(yǔ)言編寫的程序的編譯時(shí)間(compile-time)、鏈接時(shí)間(link-time)、運(yùn)行時(shí)間(run-time)以及空閑時(shí)間(idle-time),對(duì)開(kāi)發(fā)者保持開(kāi)放,并兼任已有腳本。 - 2006年
Chris Lattner加盟Apple Inc.并致力于LLVM在Apple開(kāi)發(fā)體系中的應(yīng)用。Apple也是LLVM計(jì)劃的主要資助者。
目前LLVM已經(jīng)被蘋果iOS開(kāi)發(fā)工具、Xilinx Vivado、Facebook、Google等各大公司采用。
2.1 傳統(tǒng)編譯器的設(shè)計(jì)

- 編譯器前端(Frontend):
編譯器的前端任務(wù)是解析源代碼。 會(huì)進(jìn)行詞法分析、語(yǔ)法分析、語(yǔ)義分析。檢查源代碼是否存在錯(cuò)誤,然后構(gòu)建抽象語(yǔ)法樹(shù)(Abstract Syntax Tree AST),LLVM前端還會(huì)生成中間代碼(intermediate representation, IR)
- 優(yōu)化器(Optimizer)
優(yōu)化器負(fù)責(zé)各種優(yōu)化。改善代碼的運(yùn)行時(shí)間,如消除冗余計(jì)算等
- 后端(Backkend)/ 代碼生成器(CodeGenerator)
將代碼映射到目標(biāo)指令集,生成機(jī)器語(yǔ)言,并進(jìn)行機(jī)器相關(guān)的代碼優(yōu)化 (目標(biāo)指不同操作系統(tǒng))
iOS的編譯器架構(gòu):
Objective C/C/C++使用的編譯器前端是Clang,Swift是swift,后端都是LLVM。
image.png
2.2 LLVM的設(shè)計(jì)
GCC是一個(gè)非常成功的編譯器,但由于它作為整體應(yīng)用程序設(shè)計(jì)的,用途受到了限制。LLVM最重要的地方:支持多種語(yǔ)言或多種硬件架構(gòu)。使用通用代碼表示形式:IR(用來(lái)在編譯器中表示代碼的形式)LLVM可以為任何編程語(yǔ)言獨(dú)立編寫前端,也可以為任何硬件架構(gòu)獨(dú)立編寫后端.所以LLVM
不是一個(gè)簡(jiǎn)單的編譯器,而是架構(gòu)編譯器,可以兼容所有前端和后端。

2.3 Clang
Clang是LLVM項(xiàng)目的一個(gè)子項(xiàng)目?;?code>LLVM架構(gòu)的輕量級(jí)編輯器,誕生之初就是為了替代GCC,提供更快的編譯速度。 他是負(fù)責(zé)編譯C、C++、Objecte-C語(yǔ)言的編譯器,它屬于整個(gè)LLVM架構(gòu)中的編譯器前端。
- 對(duì)于開(kāi)發(fā)者而言,
研究Clang可以給我們帶來(lái)很多好處。
3. LLVM案例體驗(yàn)
- 新建一個(gè)
Mac OS的命令行工程:
image.png -
沒(méi)有改動(dòng)代碼
image.png
3.1 編譯流程
- cd到
main.m的文件夾。使用下面命令查看main.m的編譯步驟:
clang -ccc-print-phases main.m

編譯流程分為以下7步:
-
0: input, "main.m", objective-c:
輸入文件:找到源文件 -
1: preprocessor, {0}, objective-c-cpp-output:
預(yù)處理:宏的展開(kāi),頭文件的導(dǎo)入 -
2: compiler, {1}, ir:
編譯:詞法、語(yǔ)法、語(yǔ)義分析,最終生成IR -
3: backend, {2}, assembler ():
匯編: LLVM通過(guò)一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事,最后生成匯編代碼 -
4: assembler, {3}, object:
目標(biāo)文件 -
5: linker, {4}, image:
鏈接: 鏈接需要的動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù),生成可執(zhí)行文件 -
6: bind-arch, "x86_64", {5}, image:
架構(gòu)可執(zhí)行文件:通過(guò)不同架構(gòu),生成對(duì)應(yīng)的可執(zhí)行文件
optimizer優(yōu)化并沒(méi)有作為一個(gè)獨(dú)立階段,在編譯階段內(nèi)部完成的
3.2 預(yù)處理階段
-
main.m中準(zhǔn)備測(cè)試代碼:
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
printf("%d", a + b + C);
}
return 0;
}
-
clang預(yù)編譯輸出main2.m文件:
clang -E main.m >> main2.m
-
打開(kāi)
main2.m,有575行。其中大部分是stdio庫(kù)的代碼:
image.png 我們發(fā)現(xiàn)測(cè)試代碼中的
宏C,在預(yù)編譯階段完成了替換,變成了30
預(yù)編譯階段: 1.
導(dǎo)入頭文件2.替換宏
- 修改測(cè)試代碼,給
int類型取個(gè)別名HT_INT_64,再次預(yù)編譯處理:
#define C 30
typedef int HT_INT_64;
int main(int argc, const char * argv[]) {
@autoreleasepool {
HT_INT_64 a = 10;
HT_INT_64 b = 20;
printf("%d", a + b + C);
}
return 0;
}
- 發(fā)現(xiàn)
typedef不會(huì)被替換
image.png
安全拓展:
- 使用
define將重要方法名稱進(jìn)行替換。比如#define Pay XXXTest這樣開(kāi)發(fā)者使用宏P(guān)ay開(kāi)發(fā)舒服,但是被hank時(shí),實(shí)際代碼是XXXTest,不容易被察覺(jué)。
(#define的真實(shí)內(nèi)容,不應(yīng)該寫成亂碼,會(huì)讓人有此地?zé)o銀三百兩的感覺(jué),最好弄成系統(tǒng)類似名稱或其他不經(jīng)意的名稱。這樣才容易被忽視,安全級(jí)別才更高??)
typedef沒(méi)有這個(gè)偷梁換柱的效果。define只影響預(yù)處理期。
3.3 編譯階段
3.3.1 詞法分析
- 編譯
main.m文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-詞法分析,就是根據(jù)空格和括號(hào)這些將代碼拆分成一個(gè)個(gè)Token。標(biāo)注了位置是第幾行的第幾個(gè)字符開(kāi)始的。

3.3.2 語(yǔ)法分析
-
語(yǔ)法分析是驗(yàn)證語(yǔ)法是否正確。
在詞法分析的基礎(chǔ)上,將單詞序列組合成各類語(yǔ)法短語(yǔ),如“程序”,“語(yǔ)句”,“表達(dá)式”等,然后將所有節(jié)點(diǎn)組成抽象語(yǔ)法樹(shù)(Abstract Syntax Tree,AST)。語(yǔ)法分析程序判斷源程序在結(jié)構(gòu)上是否正確。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
作用域、類型、運(yùn)算方式都十分清晰。( 語(yǔ)法樹(shù)一次只能處理一次計(jì)算。兩次運(yùn)算,就得多分一層級(jí)。)
image.png 語(yǔ)法分析,就是在生成語(yǔ)法樹(shù)時(shí)完成檢測(cè)的。
- 頭文件找不到時(shí),可以指定SDK:
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己SDK路徑) -fmodules -fsyntax-only -Xclang -ast-dump main.m
3.4 生成中間代碼IR(Intermediate representation)
3.4.1 生成中間代碼
完成以上步驟后,就開(kāi)始生成
中間代碼IR,代碼生成器(Code Generation)會(huì)將語(yǔ)法樹(shù)自頂向下遍歷逐步翻譯成LLVM的IR。便于理解,我們簡(jiǎn)化代碼:
#import <stdio.h>
int test(int a, int b) {
return a + b + 3;
}
int main(int argc, const char * argv[]) {
int a = test(1,2);
printf("%d",a);
return 0;
}
通過(guò)下面命令生成.ll文本文件,查看IR代碼:
clang -S -fobjc-arc -emit-llvm main.m
- IR基本語(yǔ)法
@全局標(biāo)識(shí)
%局部標(biāo)識(shí)
alloca開(kāi)辟空間
align內(nèi)存對(duì)齊
i3232個(gè)bit,4個(gè)字節(jié)
store寫入內(nèi)存
load讀取數(shù)據(jù)
call調(diào)用數(shù)據(jù)
ret返回
- 使用
VSCode或Sublime Text可以打開(kāi)代碼:(可以指定文件的語(yǔ)言,讓代碼有高亮色)

- Q:圖中為何
多創(chuàng)建那么多局部變量?(如test函數(shù)內(nèi)的a5、a6)- 因?yàn)樵谏弦浑A段(
編譯階段),我們將代碼編譯成了語(yǔ)法樹(shù)結(jié)構(gòu)。而此時(shí),我們只是沿著語(yǔ)法樹(shù)進(jìn)行讀取。 語(yǔ)法樹(shù)每一個(gè)層級(jí),都需要一個(gè)臨時(shí)變量來(lái)承接。再返回上一層級(jí)處理。- 所以會(huì)
產(chǎn)生那么多局部變量。
3.4.2 IR優(yōu)化
- 我們可以在
Xcode的Build Settings中搜索Optimization,可以看到優(yōu)化級(jí)別。
(Debug模式默認(rèn)None [O0]無(wú)優(yōu)化,Release模式默認(rèn)Fastest,Smallest [Os]最快最小)

LLVM的優(yōu)化級(jí)別分為
-O0、-O1、-O2、-O3、-Os(第一個(gè)字母是Optimization的O)。分別選擇
O0和Os兩個(gè)優(yōu)化等級(jí)進(jìn)行中間代碼的生成比較:
clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll // O0 無(wú)優(yōu)化
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll // Os 最快最小


-
優(yōu)化后的代碼,舒服多了。之前那些冗余的臨時(shí)局部變量,也都被優(yōu)化,代碼量減少很多。
3.4.3 bitCode再優(yōu)化
-
Xcode7之后,開(kāi)啟bitCode蘋果會(huì)再進(jìn)一步優(yōu)化,生成.bc的中間代碼。
優(yōu)化體現(xiàn):上傳APPstore的包,針對(duì)不同型號(hào)手機(jī)做了區(qū)分,不同型號(hào)手機(jī)下載時(shí),包的大小不同。
clang -emit-llvm -c main.ll -o main.bc
3.5 生成匯編代碼
完成
中間代碼的生成后,可以將代碼轉(zhuǎn)變為匯編代碼了。-
此刻我們有
4種不同程度的代碼(源代碼->無(wú)優(yōu)化IR代碼->Os優(yōu)化IR代碼->bitcode優(yōu)化代碼):
image.png 分別對(duì)
4種程度的代碼輸出匯編文件:
clang -S -fobjc-arc main.m -o main.s
clang -S -fobjc-arc main.ll -o mainO0.s
clang -S -fobjc-arc mainOs.ll -o mainOs.s
clang -S -fobjc-arc main.bc -o mainbc.s

可以看到在生成匯編代碼時(shí),只有選擇了優(yōu)化等級(jí),才能減少匯編代碼量。
【拓展】在
生成中間代碼的前后,都可以進(jìn)行優(yōu)化。
- [嘗試一] 將
main.m直接選擇Os級(jí)別優(yōu)化生成.s匯編文件clang -Os -S -fobjc-arc main.m -o mainOs.s
- [嘗試二] 將
main.m生成無(wú)優(yōu)化的main.s,再main.s選擇Os級(jí)別優(yōu)化生成.s匯編文件clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll clang -Os -S -fobjc-arc mainO0.ll -o mainOoOs.s
- [嘗試三] 將
main.m選擇Os級(jí)別優(yōu)化生成main.s,再main.s選擇無(wú)優(yōu)化級(jí)別生成.s匯編文件clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll clang -S -fobjc-arc mainOs.ll -o mainOsOo.s
- [嘗試四] 將
main.m選擇Os級(jí)別優(yōu)化生成main.s,再main.s選擇Os級(jí)別優(yōu)化生成.s匯編文件clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
- 內(nèi)容比較:
image.png
3.6 生成目標(biāo)文件(機(jī)器代碼)
-
生成匯編文件后,匯編器以匯編代碼作為輸入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼,輸出目標(biāo)文件(object file)
clang -fmodules -c main.s -o main.o
file對(duì)比一下main.s匯編代碼和main.o機(jī)器代碼:file main3.m file main.oimage.png
-
xcrun執(zhí)行nm命令查看main.o文件中的符號(hào):
xcrun nm -nm main.o

- 此時(shí)只是把
當(dāng)前文件編譯為了機(jī)器碼,外部符號(hào)(如printf)無(wú)法識(shí)別。
undefined:表示當(dāng)前文件暫時(shí)找不到符號(hào)。
external:表示這個(gè)符號(hào)是外部可以訪問(wèn)的。(實(shí)現(xiàn)不在我這,在外部的某個(gè)地方)
所以當(dāng)前雖轉(zhuǎn)換成了機(jī)器代碼。但是只是目標(biāo)文件,并不能直接執(zhí)行,需要將所有資源鏈接起來(lái),才可以執(zhí)行。
3.7 生成可執(zhí)行文件(鏈接)
- 通過(guò)
鏈接器把編譯產(chǎn)生的.o文件和.dylib、.a文件鏈接關(guān)聯(lián)起來(lái),生成真正的mach-o可執(zhí)行文件
clang main.o -o main // 將目標(biāo)文件轉(zhuǎn)成可執(zhí)行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符號(hào)

- 對(duì)比
main.o目標(biāo)文件,此時(shí)生成的main文件:
- 從
object文件變成了executable可執(zhí)行文件- 雖然都有
undefined,但是可執(zhí)行文件中指定了該符號(hào)的來(lái)源庫(kù)。機(jī)器在運(yùn)行時(shí),會(huì)從相應(yīng)的庫(kù)中取讀取該符號(hào)(printf)
至此,我們已完整分析:源代碼到可執(zhí)行文件的整個(gè)流程:

-
下一節(jié),我們嘗試玩LLVM。(創(chuàng)建插件,增加代碼規(guī)范,有效智能提示)
(ps:LLVM源碼下載和編譯教程,都在OC底層原理三十二:LLVM插件(Copy修飾符檢測(cè))中)










