寫在前面
本文主要是理解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)換成機(jī)器識(shí)別語言
-
-
解釋性語言:生成的是數(shù)據(jù),不是0、1組合,機(jī)器也能直接識(shí)別-
python是解釋型語言,一邊翻譯一邊執(zhí)行.和js一樣,機(jī)器可直接執(zhí)行.
-
編譯器的作用,就是將高級(jí)語言轉(zhuǎn)化為機(jī)器能夠識(shí)別的語言(可執(zhí)行文件)
③.2 匯編有指令嗎?
- 早期科學(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)過解釋器,變成0和1的組合,再交給機(jī)器去執(zhí)行.這就是匯編的由來. - 而基于匯編往上,再
映射和封裝相關(guān)對(duì)應(yīng)關(guān)系.就跨時(shí)代性的c語言,再往上層封裝,就出現(xiàn)了高級(jí)語言oc、swift等語言.所以匯編執(zhí)行快,因?yàn)樗?code>直接轉(zhuǎn)換為機(jī)器語言. - 但
匯編的指令集,是針對(duì)同一操作系統(tǒng)而言,它不支持跨平臺(tái).機(jī)器指令是cpu的在識(shí)別.早期的計(jì)算機(jī)廠家非常多,雖然都用0和1的組合,但相同組合背后卻是相應(yīng)不同的指令.所以匯編無法跨平臺(tái),不同操作系統(tǒng)下,匯編指令是不同的.
二、LLVM概述
LLVM是架構(gòu)編譯器(compiler)的框架系統(tǒng),以C++編寫而成,用于優(yōu)化以任意程序語言編寫的程序的編譯時(shí)間(compile-time)、鏈接時(shí)間(link-time)、運(yùn)行時(shí)間(run-time)以及空閑時(shí)間(idle-time),對(duì)開發(fā)者保持開放,并兼任已有腳本.
LLVM計(jì)劃啟動(dòng)于2000年,最初由美國(guó)UIUC大學(xué)的Chris Lattner博士主持開展.
2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple開發(fā)體系中的應(yīng)用.Apple也是LLVM計(jì)劃的主要資助者.
目前LLVM已經(jīng)被蘋果iOS開發(fā)工具、Xilinx Vivado、Facebook、Google等各大公司采用.
三、傳統(tǒng)編譯器的設(shè)計(jì)
源碼 Source Code + 前端 Frontend + 優(yōu)化器 Optimizer + 后端 Backend(代碼生成器 CodeGenerator)+ 機(jī)器碼 Machine Code,如下圖所示

編譯器前端(Frontend)
編譯器前端的任務(wù)是解析源代碼(編譯階段),它會(huì)進(jìn)行 詞法分析、語法分析、語義分析、檢查源代碼是否存在錯(cuò)誤,然后構(gòu)建抽象語法樹(Abstract Syntax Tree AST),LLVM的前端還會(huì)生成中間代碼(intermediate representation,簡(jiǎn)稱IR),可以理解為LLVM是編譯器 + 優(yōu)化器, 接收的是IR中間代碼,輸出的還是IR,給后端,經(jīng)過后端翻譯成目標(biāo)指令集
優(yōu)化器(Optimizer)
優(yōu)化器負(fù)責(zé)進(jìn)行各種優(yōu)化,改善代碼的運(yùn)行時(shí)間,例如消除冗余計(jì)算等
后端(Backend)/(代碼生成器 Code Generator)
將代碼映射到目標(biāo)指令集,生成機(jī)器代語言,并且進(jìn)行機(jī)器代碼相關(guān)的代碼優(yōu)化
iOS的編譯器架構(gòu)
Objective C/C/C++ 使用的編譯器前端是Clang,Swift是swift,后端都是LLVM.

LLVM的設(shè)計(jì)
當(dāng)編譯器決定支持多種源語言或多種硬件架構(gòu)時(shí),LLVM最重要的地方就來了.其他的編譯器如GCC,它方法非常成功,但由于它是作為整體應(yīng)用程序設(shè)計(jì)的,因此它們的用途受到了很大的限制.
LLVM設(shè)計(jì)的最重要方面是,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式,所以LLVM可以為任何編程語言獨(dú)立編寫前端,并且可以為任意硬件架構(gòu)獨(dú)立編寫后端,如下所示

通俗的一句話理解就是:LLVM的設(shè)計(jì)是前后端分離的,無論前端還是后端發(fā)生變化,都不會(huì)影響另一個(gè)
Clang簡(jiǎn)介
Clang是LLVM項(xiàng)目中的一個(gè)子項(xiàng)目,它是基于LLVM架構(gòu)圖的輕量級(jí)編譯器,誕生之初是為了替代GCC,提供更快的編譯速度,它是負(fù)責(zé)C、C++、OC語言的編譯器,屬于整個(gè)LLVM架構(gòu)中的 編譯器前端,對(duì)于開發(fā)者來說,研究Clang可以給我們帶來很多好處
四、LLVM編譯流程
- 新建一個(gè)
Mac OS的命令行工程:
-
沒有改動(dòng)代碼
① 打印源碼的編譯階段
-
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通過一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事,最后生成匯編代碼
-
4: assembler, {3}, object:- 目標(biāo)文件
-
5: linker, {4}, image:- 鏈接: 鏈接需要的動(dòng)態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件
-
6: bind-arch, "x86_64", {5}, image:- 架構(gòu)可執(zhí)行文件:通過不同架構(gòu),生成對(duì)應(yīng)的可執(zhí)行文件
optimizer優(yōu)化并沒有作為一個(gè)獨(dú)立階段,在編譯階段內(nèi)部完成的
② 預(yù)處理階段
這個(gè)階段主要是處理包括宏的替換,頭文件的導(dǎo)入,可以執(zhí)行如下命令,執(zhí)行完畢可以看到頭文件的導(dǎo)入和宏的替換
-
main.m文件中準(zhǔn)備測(cè)試代碼:
-
clang預(yù)編譯輸出main2.m文件:通過指令clang -E main.m >> main2.m - 打開
main2.m文件其中大部分是stdio庫的代碼:
我們發(fā)現(xiàn)測(cè)試代碼中的
宏C,在預(yù)編譯階段完成了替換,變成了30-
修改測(cè)試代碼,給
int類型取個(gè)別名CJ_INT_64,再次預(yù)編譯處理:
- 發(fā)現(xiàn)
typedef不會(huì)被替換
小結(jié):
-
typedef在給數(shù)據(jù)類型取別名時(shí),在預(yù)處理階段不會(huì)被替換掉 -
define則在預(yù)處理階段會(huì)被替換,所以經(jīng)常被用來進(jìn)行代碼混淆,目的是為了app安全,實(shí)現(xiàn)邏輯是:將app中核心類、核心方法等用系統(tǒng)相似的名稱進(jìn)行取別名,然后在預(yù)處理階段就被替換了,來達(dá)到代碼混淆的目的
③ 編譯階段
編譯階段主要是進(jìn)行詞法、語法等的分析和檢查,然后生成中間代碼IR
③.1 詞法分析
預(yù)處理完成后就會(huì)進(jìn)行詞法分析,這里會(huì)把代碼切成一個(gè)個(gè)Token,比如大小括號(hào)、等于號(hào)還有字符串等,而且還標(biāo)注了位置是第幾行的第幾個(gè)字符開始的.
- 可以通過
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m命令查看
③.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命令查看語法分析的結(jié)果
其中,主要說明幾個(gè)關(guān)鍵字的含義
- -FunctionDecl 函數(shù)
- -ParmVarDecl 參數(shù)
- -CallExpr 調(diào)用一個(gè)函數(shù)
- -BinaryOperator 運(yùn)算符
④ 生成中間代碼IR
完成以上步驟后,就開始生成中間代碼IR了,代碼生成器(Code Generation)會(huì)將語法樹自頂向下遍歷逐步翻譯成LLVM IR.

- 通過
clang -S -fobjc-arc -emit-llvm main.m命令可以生成.ll的文本文件,查看IR代碼.OC代碼在這一步會(huì)進(jìn)行runtime橋接:property合成、ARC處理等
IR基本語法
-
@全局標(biāo)識(shí) -
%局部標(biāo)識(shí) -
alloca開辟空間 -
align內(nèi)存對(duì)齊 -
i3232bit,4個(gè)字節(jié) -
store寫入內(nèi)存 -
load讀取數(shù)據(jù) -
call調(diào)用函數(shù) -
ret返回
下面是生成的中間代碼.ll文件

其中,test函數(shù)的參數(shù)解釋為

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

LLVM的優(yōu)化級(jí)別分別是-O0 -O1 -O2 -O3 -Os(第一個(gè)是大寫英文字母O),下面是帶優(yōu)化的生成中間代碼IR的命令
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
這是優(yōu)化后的中間代碼
優(yōu)化后的代碼,舒服多了.之前那些冗余的臨時(shí)局部變量,也都被優(yōu)化,代碼量減少很多.
-
xcode7以后開啟bitcode,蘋果會(huì)做進(jìn)一步優(yōu)化,生成.bc的中間代碼,我們通過優(yōu)化后的IR代碼生成.bc代碼.- 優(yōu)化指令
clang -emit-llvm -c main.ll -o main.bc
- 優(yōu)化指令
⑤ 生成匯編代碼
LLVM在后端主要是會(huì)通過一個(gè)個(gè)的Pass去優(yōu)化,每個(gè)Pass做一些事情,最終生成匯編代碼
- 完成
中間代碼的生成后,可以將代碼轉(zhuǎn)變?yōu)?code>匯編代碼了 - 此刻我們有
4種不同程度的代碼(源代碼->無優(yōu)化IR代碼->Os優(yōu)化IR代碼->bitcode優(yōu)化代碼)
- 分別對(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)化---即在
生成中間代碼的前后,都可以進(jìn)行優(yōu)化- ① 將
main.m直接選擇Os級(jí)別優(yōu)化生成.s匯編文件--clang -Os -S -fobjc-arc main.m -o mainOs.s - ② 將
main.m生成無優(yōu)化的mainO0.ll,再mainO0.ll選擇Os級(jí)別優(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級(jí)別優(yōu)化生成mainOs.ll,再mainOs.ll選擇無優(yōu)化級(jí)別生成.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級(jí)別優(yōu)化生成mainOs.ll,再mainOs.ll選擇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
- ① 將
⑥ 生成目標(biāo)文件(機(jī)器代碼)
目標(biāo)文件的生成,是匯編器以匯編代碼作為插入,將匯編代碼轉(zhuǎn)換為機(jī)器代碼,最后輸出目標(biāo)文件(object file)-- clang -fmodules -c main.s -o main.o
- 此時(shí)我們
file對(duì)比一下main.s匯編代碼和main.o機(jī)器代碼.
-
可以通過
nm命令,查看下main.o中的符號(hào) --xcrun nm -nm main.o-
_printf函數(shù)是一個(gè)是undefined 、external的 -
undefined表示在當(dāng)前文件暫時(shí)找不到符號(hào)_printf -
external表示這個(gè)符號(hào)是外部可以訪問的
-
所以當(dāng)前雖轉(zhuǎn)換成了機(jī)器代碼.但是只是目標(biāo)文件,并不能直接執(zhí)行,需要將所有資源鏈接起來,才可以執(zhí)行.
⑦ 生成可執(zhí)行文件(鏈接)
鏈接主要是鏈接需要的動(dòng)態(tài)庫和靜態(tài)庫,生成可執(zhí)行文件,其中
- 靜態(tài)庫會(huì)和可執(zhí)行文件合并
- 動(dòng)態(tài)庫是獨(dú)立的
連接器把編譯生成的.o文件和 .dyld 、.a文件鏈接,生成一個(gè)mach-o文件,接著輸入以下指令
clang main.o -o main // 將目標(biāo)文件轉(zhuǎn)成可執(zhí)行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符號(hào)
結(jié)果如下所示,其中的undefined表示會(huì)在運(yùn)行時(shí)進(jìn)行動(dòng)態(tài)綁定

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

寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.














