寫在前面
本文主要是理解LLVM的編譯流程
一、什么是編譯器?
① Python案例
- 創(chuàng)建
Python
文件夾衙吩,新建helloDemo.py
文件笔刹,內(nèi)容print("hello\n")
- 調(diào)用
python helloDemo.py
執(zhí)行文件芥备,打印出hello
② C 案例
-
vim
創(chuàng)建helloDemo.c
文件
-
clang helloDemo.c
編譯,生成a.out
文件.
file a.out
查看文件,發(fā)現(xiàn).out
文件是:64位
的Mach-O
可執(zhí)行文件舌菜,當(dāng)前clang
出來的是x86_64
架構(gòu)萌壳,mac
電腦可讀. 所以可以./a.out
直接執(zhí)行:
③ 相關(guān)疑問
③.1 解釋型語言與編譯型語言
-
編譯型語言
:編譯后
輸出的是指令
(0、1組合
)日月,cpu
可直接執(zhí)行指令-
C語言
是編譯型語言
袱瓮,不能直接執(zhí)行,需要編譯器
將其轉(zhuǎn)換成機器識別語言
-
-
解釋性語言
:生成的是數(shù)據(jù)
爱咬,不是0尺借、1組合
,機器也能直接識別-
python
是解釋型語言
精拟,一邊翻譯一邊執(zhí)行
.和js
一樣燎斩,機器可直接執(zhí)行.
-
編譯器
的作用,就是將高級語言
轉(zhuǎn)化為機器
能夠識別的語言
(可執(zhí)行文件
)
③.2 匯編有指令嗎蜂绎?
- 早期科學(xué)家栅表,使用
0、1
編碼. 比如00001111
對應(yīng)call
师枣,00000111
對應(yīng)bl
.有了對應(yīng)關(guān)系后. 再手敲0和1
就有點難受了.于是寫個中間解釋器
怪瓶,我們只用輸入call
、bl
這樣的標(biāo)記指令
践美,經(jīng)過解釋器
洗贰,變成0和1的組合
,再交給機器去執(zhí)行.這就是匯編的由來
. - 而基于匯編往上陨倡,再
映射
和封裝
相關(guān)對應(yīng)關(guān)系
.就跨時代性
的c
語言敛滋,再往上層封裝,就出現(xiàn)了高級語言oc
玫膀、swift
等語言.所以匯編執(zhí)行快
矛缨,因為它是直接轉(zhuǎn)換
為機器語言
. - 但
匯編
的指令集
爹脾,是針對同一操作系統(tǒng)
而言帖旨,它不支持跨平臺.機器指令
是cpu
的在識別.早期的計算機廠家非常多,雖然都用0
和1
的組合灵妨,但相同組合背后卻是相應(yīng)不同的指令.所以匯編無法跨平臺
解阅,不同操作系統(tǒng)
下,匯編指令
是不同
的.
二泌霍、LLVM概述
LLVM
是架構(gòu)編譯器(compiler)的框架系統(tǒng)
货抄,以C++
編寫而成述召,用于優(yōu)化
以任意程序語言編寫的程序的編譯時間
(compile-time)、鏈接時間
(link-time)蟹地、運行時間
(run-time)以及空閑時間
(idle-time)积暖,對開發(fā)者保持開放,并兼任已有腳本.
LLVM
計劃啟動于2000年,最初由美國UIUC
大學(xué)的Chris Lattner
博士主持開展.
2006年Chris Lattner
加盟Apple Inc.
并致力于LLVM
在Apple開發(fā)體系
中的應(yīng)用.Apple
也是LLVM
計劃的主要資助者.
目前LLVM
已經(jīng)被蘋果iOS開發(fā)工具
怪与、Xilinx Vivado
夺刑、Facebook
、Google
等各大公司采用.
三分别、傳統(tǒng)編譯器的設(shè)計
源碼 Source Code
+ 前端 Frontend
+ 優(yōu)化器 Optimizer
+ 后端 Backend
(代碼生成器 CodeGenerator
)+ 機器碼 Machine Code
遍愿,如下圖所示
編譯器前端(Frontend)
編譯器前端
的任務(wù)是解析源代碼
(編譯階段),它會進行 詞法分析
耘斩、語法分析
沼填、語義分析
、檢查源代碼是否存在錯誤
括授,然后構(gòu)建抽象語法樹
(Abstract Syntax Tree AST)坞笙,LLVM
的前端還會生成中間代碼
(intermediate representation,簡稱IR)刽脖,可以理解為LLVM
是編譯器 + 優(yōu)化器
羞海, 接收的是IR
中間代碼,輸出的還是IR
曲管,給后端却邓,經(jīng)過后端翻譯成目標(biāo)指令集
優(yōu)化器(Optimizer)
優(yōu)化器負責(zé)進行各種優(yōu)化,改善代碼的運行時間院水,例如消除冗余計算等
后端(Backend)/(代碼生成器 Code Generator)
將代碼映射到目標(biāo)指令集腊徙,生成機器代語言,并且進行機器代碼相關(guān)的代碼優(yōu)化
iOS的編譯器架構(gòu)
Objective C/C/C++
使用的編譯器前端是Clang檬某,Swift是swift撬腾,后端都是LLVM.
LLVM的設(shè)計
當(dāng)編譯器決定支持多種源語言或多種硬件架構(gòu)時,LLVM
最重要的地方就來了.其他的編譯器如GCC
,它方法非常成功,但由于它是作為整體應(yīng)用程序設(shè)計的,因此它們的用途受到了很大的限制.
LLVM
設(shè)計的最重要方面是,使用通用的代碼表示形式(IR)
恢恼,它是用來在編譯器中表示代碼的形式民傻,所以LLVM
可以為任何編程語言獨立編寫前端
,并且可以為任意硬件架構(gòu)獨立編寫后端
场斑,如下所示
通俗的一句話理解就是:LLVM
的設(shè)計是前后端分離
的漓踢,無論前端還是后端發(fā)生變化,都不會影響另一個
Clang簡介
Clang
是LLVM
項目中的一個子項目漏隐,它是基于LLVM
架構(gòu)圖的輕量級編譯器
喧半,誕生之初是為了替代GCC
,提供更快的編譯速度青责,它是負責(zé)C挺据、C++取具、OC語言的編譯器
,屬于整個LLVM
架構(gòu)中的 編譯器前端
扁耐,對于開發(fā)者來說暇检,研究Clang
可以給我們帶來很多好處
四、LLVM編譯流程
- 新建一個
Mac OS
的命令行
工程:
-
沒有改動代碼
① 打印源碼的編譯階段
-
cd
到main.m
的文件夾.使用clang -ccc-print-phases main.m
命令查看main.m
的編譯步驟:
編譯流程
分為以下7步
:
-
0: input, "main.m", objective-c
:- 輸入文件:找到源文件
-
1: preprocessor, {0}, objective-c-cpp-output
:- 預(yù)處理:宏的展開婉称,頭文件的導(dǎo)入
-
2: compiler, {1}, ir
:- 編譯:詞法占哟、語法、語義分析酿矢,最終生成IR
-
3: backend, {2}, assembler ()
:- 匯編: LLVM通過一個個的Pass去優(yōu)化榨乎,每個Pass做一些事,最后生成匯編代碼
-
4: assembler, {3}, object
:- 目標(biāo)文件
-
5: linker, {4}, image
:- 鏈接: 鏈接需要的動態(tài)庫和靜態(tài)庫瘫筐,生成可執(zhí)行文件
-
6: bind-arch, "x86_64", {5}, image
:- 架構(gòu)可執(zhí)行文件:通過不同架構(gòu)蜜暑,生成對應(yīng)的可執(zhí)行文件
optimizer優(yōu)化并沒有作為一個獨立階段,在編譯階段內(nèi)部完成的
② 預(yù)處理階段
這個階段主要是處理包括宏的替換
策肝,頭文件的導(dǎo)入
肛捍,可以執(zhí)行如下命令,執(zhí)行完畢可以看到頭文件的導(dǎo)入和宏的替換
-
main.m
文件中準(zhǔn)備測試代碼:
-
clang
預(yù)編譯輸出main2.m
文件:通過指令clang -E main.m >> main2.m
- 打開
main2.m
文件其中大部分是stdio
庫的代碼:
我們發(fā)現(xiàn)測試代碼中的
宏C
之众,在預(yù)編譯階段
完成了替換
拙毫,變成了30
-
修改測試代碼,給
int類型
取個別名CJ_INT_64
棺禾,再次預(yù)編譯處理
:
- 發(fā)現(xiàn)
typedef
不會被替換
小結(jié):
-
typedef
在給數(shù)據(jù)類型取別名時缀蹄,在預(yù)處理階段不會被替換掉
-
define
則在預(yù)處理階段會被替換
,所以經(jīng)常被用來進行代碼混淆膘婶,目的是為了app安全缺前,實現(xiàn)邏輯是:將app中核心類、核心方法等用系統(tǒng)相似的名稱進行取別名
悬襟,然后在預(yù)處理階段就被替換了衅码,來達到代碼混淆的目的
③ 編譯階段
編譯階段主要是進行詞法、語法等的分析和檢查脊岳,然后生成中間代碼IR
③.1 詞法分析
預(yù)處理完成后就會進行詞法分析
逝段,這里會把代碼切成一個個Token
,比如大小括號割捅、等于號還有字符串等,而且還標(biāo)注了位置
是第幾行
的第幾個字符開始的.
- 可以通過
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
命令查看
③.2 語法分析
詞法分析完成后就是語法分析
奶躯,它的任務(wù)是驗證語法是否正確
,在詞法分析的基礎(chǔ)上將單詞序列組合成各類此法短語棺牧,如程序巫糙、語句朗儒、表達式 等等颊乘,然后將所有節(jié)點組成抽象語法樹
(Abstract Syntax Tree, AST)参淹,語法分析程序
判斷源程序
在結(jié)構(gòu)上
是否正確.
- 可以通過
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
命令查看語法分析的結(jié)果
其中,主要說明幾個關(guān)鍵字的含義
- -FunctionDecl 函數(shù)
- -ParmVarDecl 參數(shù)
- -CallExpr 調(diào)用一個函數(shù)
- -BinaryOperator 運算符
④ 生成中間代碼IR
完成以上步驟后乏悄,就開始生成中間代碼IR
了檩小,代碼生成器
(Code Generation)會將語法樹自頂向下
遍歷逐步翻譯成LLVM IR
.
- 通過
clang -S -fobjc-arc -emit-llvm main.m
命令可以生成.ll
的文本文件,查看IR
代碼.OC
代碼在這一步會進行runtime
橋接:property
合成规求、ARC
處理等
IR基本語法
-
@
全局標(biāo)識 -
%
局部標(biāo)識 -
alloca
開辟空間 -
align
內(nèi)存對齊 -
i32
32bit筐付,4個字節(jié) -
store
寫入內(nèi)存 -
load
讀取數(shù)據(jù) -
call
調(diào)用函數(shù) -
ret
返回
下面是生成的中間代碼.ll
文件
其中,test
函數(shù)的參數(shù)解釋為
圖中為何多創(chuàng)建那么多局部變量阻肿?(如test
函數(shù)內(nèi)的a5
瓦戚、a6
)
因為在上一階段(編譯階段
),我們將代碼
編譯成了語法樹結(jié)構(gòu)
.而此時丛塌,我們只是沿著語法樹進行讀取.語法樹
每一個層級较解,都需要一個臨時變量
來承接.再返回上一層級處理.所以會產(chǎn)生那么多局部變量
當(dāng)然,IR
文件在OC
中是可以進行優(yōu)化的赴邻,一般Xcode中設(shè)置是在target - Build Setting - Optimization Level
(優(yōu)化器等級)中設(shè)置.(Debug
模式默認None
[O0]無優(yōu)化
印衔,Release
模式默認Fastest
,Smallest [Os]最快最小
)
LLVM
的優(yōu)化級別分別是-O0 -O1 -O2 -O3 -Os
(第一個是大寫英文字母O
),下面是帶優(yōu)化的生成中間代碼IR的命令
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
這是優(yōu)化后的中間代碼優(yōu)化后的代碼姥敛,舒服多了.之前那些冗余的臨時局部變量奸焙,也都被優(yōu)化,代碼量減少很多.
-
xcode7
以后開啟bitcode
彤敛,蘋果會做進一步優(yōu)化忿偷,生成.bc
的中間代碼,我們通過優(yōu)化后的IR
代碼生成.bc
代碼.- 優(yōu)化指令
clang -emit-llvm -c main.ll -o main.bc
- 優(yōu)化指令
⑤ 生成匯編代碼
LLVM在后端主要是會通過一個個的Pass去優(yōu)化臊泌,每個Pass做一些事情鲤桥,最終生成匯編代碼
- 完成
中間代碼
的生成后,可以將代碼轉(zhuǎn)變?yōu)?code>匯編代碼了 - 此刻我們有
4種
不同程度的代碼(源代碼
->無優(yōu)化IR代碼
->Os優(yōu)化IR代碼
->bitcode優(yōu)化代碼
)
- 分別對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
可以看到在生成匯編代碼時渠概,只有選擇了優(yōu)化等級
茶凳,才能減少匯編代碼量
.
- 生成匯編代碼也可以進行優(yōu)化---即在
生成中間代碼
的前后,都可以
進行優(yōu)化
- ① 將
main.m
直接選擇Os
級別優(yōu)化生成.s
匯編文件--clang -Os -S -fobjc-arc main.m -o mainOs.s
- ② 將
main.m
生成無優(yōu)化的mainO0.ll
播揪,再mainO0.ll
選擇Os
級別優(yōu)化生成.s
匯編文件 --clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll
,clang -Os -S -fobjc-arc mainO0.ll -o mainO0Os.s
- ③ 將
main.m
選擇Os
級別優(yōu)化生成mainOs.ll
贮喧,再mainOs.ll
選擇無優(yōu)化級別生成.s
匯編文件 --clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
,clang -S -fobjc-arc mainOs.ll -o mainOsO0.s
- ④ 將
main.m
選擇Os
級別優(yōu)化生成mainOs.ll
,再mainOs.ll
選擇Os
級別優(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
- ① 將
⑥ 生成目標(biāo)文件(機器代碼)
目標(biāo)文件的生成猪狈,是匯編器
以匯編代碼作為插入
箱沦,將匯編代碼轉(zhuǎn)換為機器代碼
,最后輸出目標(biāo)文件(object file
)-- clang -fmodules -c main.s -o main.o
- 此時我們
file
對比一下main.s
匯編代碼和main.o
機器代碼.
-
可以通過
nm
命令雇庙,查看下main.o
中的符號 --xcrun nm -nm main.o
-
_printf
函數(shù)是一個是undefined 谓形、external
的 -
undefined
表示在當(dāng)前文件暫時找不到符號_printf
-
external
表示這個符號
是外部可以訪問的
-
所以當(dāng)前雖轉(zhuǎn)換成了機器代碼
.但是只是目標(biāo)文件
灶伊,并不能
直接執(zhí)行
,需要將所有資源鏈接起來寒跳,才可以執(zhí)行.
⑦ 生成可執(zhí)行文件(鏈接)
鏈接主要是鏈接需要的動態(tài)庫
和靜態(tài)庫
聘萨,生成可執(zhí)行文件,其中
- 靜態(tài)庫會和可執(zhí)行文件合并
- 動態(tài)庫是獨立的
連接器把編譯生成的.o
文件和 .dyld
童太、.a
文件鏈接米辐,生成一個mach-o文件
,接著輸入以下指令
clang main.o -o main // 將目標(biāo)文件轉(zhuǎn)成可執(zhí)行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符號
結(jié)果如下所示,其中的undefined
表示會在運行時進行動態(tài)綁定
對比main.o
目標(biāo)文件书释,此時生成的main
文件:
- 從
object
文件變成了executable
可執(zhí)行文件 - 雖然都有
undefined
翘贮,但是可執(zhí)行文件中指定了該符號的來源庫
.機器在運行時
,會從相應(yīng)的庫中取讀取該符號(printf
)
⑧ 綁定
綁定主要是通過不同的架構(gòu)爆惧,生成對應(yīng)的mach-o
格式可執(zhí)行文件
至此择膝,我們已完整分析了:從源代碼
到可執(zhí)行文件
的整個流程
.
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.