本節(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)難受
了儿咱。于是寫(xiě)個(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++
編寫(xiě)而成,用于優(yōu)化
以任意程序語(yǔ)言
編寫(xiě)的程序的編譯時(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)被蘋(pí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ú)立編寫(xiě)前端
,也可以為任何硬件架構(gòu)
獨(dú)立編寫(xiě)后端
.所以LLVM
不是
一個(gè)簡(jiǎn)單的編譯器
再愈,而是架構(gòu)編譯器
榜苫,可以兼容
所有前端
和后端
。
2.3 Clang
Clang
是LLVM項(xiàng)目
的一個(gè)子項(xiàng)目
翎冲〈共牵基于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類(lèi)型
取個(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
將重要方法
名稱(chēng)進(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)該
寫(xiě)成亂碼
,會(huì)讓人有此地?zé)o銀三百兩
的感覺(jué)止吁,最好
弄成系統(tǒng)類(lèi)似名稱(chēng)
或其他不經(jīng)意
的名稱(chē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ǔ)上,將單詞
序列組合
成各類(lèi)語(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
-
作用域
威兜、類(lèi)型
、運(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ì)齊
i32
32個(gè)bit粪躬,4個(gè)字節(jié)
store
寫(xiě)入內(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
蘋(píng)果會(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.o
image.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è))中)