1. LLVM概念
1.1 編譯器
LLVM
與編譯器息息相關,究竟什么是編譯器呢?帶著疑問往下看吧。
編譯器就是將一種語言
(通常為高級語言
)翻譯為另一種語言
(通常為低級語言
的程序。一個現代編譯器的主要工作流程:源代碼(source code)
→ 預處理器(preprocessor)
→ 編譯器(compiler)
→ 目標代碼(object code)
→ 鏈接器(Linker)
→ 可執行程序(executables)
源代碼一般為高級語言
(High-level language
), 如C
、C++
、Java
、Objective-C
等或匯編語言
,而目標則是機器語言的目標代碼
(Object copy
),有時也稱作機器代碼
(Machine code
)。
-
解釋型語言
引入一個Python
代碼,見下圖:
創建Python案例
創建一個FirstDemo.py
文件,里面只有一行代碼,print('Hello world for first time')
。通過解釋器指令python
,解釋這段代碼:
解析Python案例
通過上面的流程可以發現解釋型語言
的運行流程:
解釋型語言編譯流程
解釋型語言特點:邊解釋,邊執行,運行速度慢,部分改動無需整體重新編譯,不可脫離解釋器環境運行
。 -
編譯型語言
引入一個C
代碼,見下圖:
創建C語言案例
創建一個firstDemoForC.c
文件,里面添加了一個main
函數。首先通過clang
去讀取這個代碼:
讀取C代碼
讀取之后發現代碼并沒有立刻執行,而是生成了一個a.out
文件。這個文件就是可執行文件
。通過./a.out
執行這段代碼:
a.out執行
通過上面的流程可以發現編譯型語言
的運行流程:
編譯型語言運行流程
編譯型語言特點:先整體編譯,再執行,運行速度快,任意改動需重新編譯,可脫離編譯環境運行。
注意:編譯型語言
編譯是先將代碼編譯成cpu
可讀的懂的二進制
才能執行
1.2 LLVM概述
LLVM
是構架編譯器
(compiler
)的框架系統,以C++
編寫而成,用于優化以任意程序語言編寫的程序的編譯時間
(compile-time
)、鏈接時間
(link-time
)、運行時間
(run-time
)以及空閑時間
(idle-time
),對開發者保持開放,并兼容已有腳本。
目前LLVM
已經被Apple
、Microsoft
、Google
、Facebook
等各大公司采用。
1.2.1 傳統編譯器的設計
編譯器前端(
Frontend
)
編譯器前端的任務是解析源代碼
。它會進行:詞法分析
、語法分析
、語義分析
,檢查源代碼是否存在錯誤,然后構建抽象語法樹
,LLVM
的前端會生成中間代碼IR
。優化器(
Optimizer
)
優化器負責進行各種優化。改善運行時間,例如消除冗余計算等。后端(
Backend
)
也可以叫代碼生成器
(CodeGenerator
),將代碼映射到目標指令集
。生成機器語言
,并且進行機器相關的代碼優化
。
補充:隨著高級語言越來越多,終端類型種類的增加,所使用的的CPU
架構等也不盡相同,所以為了適配多種環境,不得不設計不同的編譯器,而這些編譯器前端
和后端
往往是捆綁在一起的。
1.2.2 LLVM
的設計思路
LLVM
的設計之初,即將編譯器前端
(Frontend
)和后端
(Backend
)進行了分離
。將前端和后端針對不同的架構,按照獨立的項目進行研發,而它們均采用通用的代碼形式IR
。
當編譯器決定支持多種語言或多種硬件架構時,
LLVM
最重要的地方就體現出來了,使用通用的代碼表示形式(IR
),它是用來在編譯器中表示代碼的形式。所以LLVM
可以為任何編程語言獨立編寫前端,并且可以為任意硬件架構獨立編寫后端。
1.2.3 iOS
編譯架構
Objective C/C/C++
使用的編譯器前端是Clang
,Swift
是Swift
,后端都是LLVM
。
1.3 Clang
Clang
是LLVM
項目中的一個子項目
。它是基于LLVM
架構的輕量級編譯器,誕生之初是為了替代GCC
,提供更快的編譯速度。它是負責編譯C
、C++
、Objective-C
語言的編譯器,它屬于整個LLVM
架構中,編譯器前端
。對于開發者來說,研究Clang
可以給我們帶來很多好處。
2. 編譯流程詳解
通過命令可以打印源碼的編譯階段。引入下面一個案例,main.m
中添加代碼:
int main(int argc, const char * argv[]) {
return 0;
}
通過指令clang -ccc-print-phases main.m
,查看編譯流程:
流程說明:
0.輸入文件:找到源文件
1.
預處理階段
:這個過程處理包括宏的替換
,頭文件的導入
2.
編譯階段
:進行詞法分析
、語法分析
、檢測語法是否正確,最終生成IR
3.后端:這里
LLVM
會通過一個一個的Pass
(節點
)去優化,每個Pass
做一些事情,最終生成匯編代碼
4.生成目標文件
5.
鏈接
:鏈接需要的動態庫
和靜態庫
,生成可執行文件
6.通過
不同的架構
,生成對應的可行文件
2.1 預處理
執行如下指令:clang -E main.m
,對源代碼進行預處理
。見下面流程:
在預處理之后,輸出
mainE.m
文件,查看mainE.m
文件:打開
mainE.m
源文件會發現,其進行了宏的替換
,如上面案例中宏c
直接替換成了30
;進行頭文件的導入。
2.2 編譯階段
2.2.1 詞法分析
預處理完成后就會進行詞法分析,這里會把代碼切成一個個Token
,比如大小括號
,等于號
以及字符串
等。詞法分析指令為:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
參考下面的案例:
通過指令的輸出可以看到,語法分析會將源碼進行
切割并檢測
。比如分號
,逗號
,int
等。
2.2.2 語法分析
詞法分析完成之后就是語法分析,它的任務是驗證語法是否正確
。在詞法分析的基礎上將單詞序列組合成各類語法短語,如:程序
,語句
,表達式
等等,然后將所有節點組成抽象語法樹
(Abstract Syntax Tree,AST
)。語法分析程序判斷源程序在結構上是否正確
。語法分析指令:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
參考下面的案例:
通過上面的輸出可以發現,其是一個樹結構,比如下面的
FunctionDecl
,表示一個方法,在源碼的第五行,名稱為main
,返回值為int
,傳入兩個參數一個是int
,一個是const char **
。見下圖:注意:一旦生成抽象語法樹,如果源碼中存在語法錯誤,就會報錯,而上面的預處理和詞法分析不會報錯。
如在源碼中設置一個語法錯誤,通過語法分析指令進行進行編譯,就會報錯,見下面案例:
2.2.3 生成中間代碼IR(intermediate representation)
完成以上步驟后,就開始生成中間代碼IR
了,代碼生成器(Code Generation
)會將語法樹自頂向下遍歷逐步翻譯成LLVM IR
。通過下面指令可以生成.ll
的文本文件,查看IR
代碼。
clang -S -fobjc-arc -emit-llvm main.m
通過上面的指令獲取main.ll
文件,其結構見下圖:
-
IR
的基本語法-
@
全局標識 -
%
局部標識 -
alloca
開辟空間 -
align
內存對齊 -
i32 32
個bit
,4
個字節 -
store
寫入內存 -
load
讀取數據 -
call
調用函數 -
ret
返回
-
IR
優化
上面生成的IR
代碼是沒有經過優化
的,其實我們在平時閱讀代碼時,經常會看下面的一些定義:
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
-
fastpath
:可以理解為快速流程
,對更有可能執行的流程進行優化
,提高運行速度;
slowpath
:基本流程,不被優化的。
在XCode
中也有相應的優化設置入口:
LLVM
的優化級別分別是-O0 -O1 -O2 -O3 -Os
(第一個是大寫英文字母O
)。可以通過下面的指令獲取優化后的IR
代碼,也就是.ll文件:
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
通過以上指令生成優化后IR
代碼如下:
好明顯,相較于優化前,代碼精簡了很多。
這里需要注意的是,通常debug
模式下,優化模式選擇None -O0
,也就是不優化,避免一些保留代碼被屏蔽
,從而影響調試
。而release
模式設置為Fastest,Smallest -Os
。
-
bitCode
Xcode7
以后開啟bitCode
蘋果會做進一步的優化,生成.bc
的中間代碼。我們通過優化后的IR
代碼生成.bc
代碼。對應指令為:
clang -emit-llvm -c main.ll -o main.bc
2.3 后端生成匯編代碼
我們通過最終的.bc
或者.ll
代碼生成匯編代碼
:
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
生成匯編代碼也可以進行優化:
clang -Os -S -fobjc-arc main.m -o main.s
采用相同的案例,分別三種方式生成匯編代碼,可以看到其優化效果。在進行IR
優化后生成的.ll
文件,依然可以進行優化生成回應的匯編代碼。在不同的節點上都可以進行優化。見下圖:
2.4 生成目標文件(匯編器)
目標文件的生成,是匯編器以匯編代碼作為輸入,將匯編代碼轉換為機器代碼
,最后輸出目標文件
(object file
)。指令為:
clang -fmodules -c main.s -o main.o
生成目標文件,見下圖:
其中
main.o
文件即為目標文件
,但是此時生成的目標文件是不可執行
的。通過nm
命令,查看下main.o
中的符號:
$xcrun nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
-
_printf
是一個undefifined external
的 -
undefifined
表示在當前文件暫時找不到符號_printf
-
external
表示這個符號是外部可以訪問的
此時就需要鏈接
,鏈接器
把編譯生成的.o
文件和(.dylib .a
)文件鏈接生成一個mach-o
文件。
clang main.o -o main
生成對應的可執行文件,見下圖:
查看鏈接之后的符號:
$xcrun nm -nm main
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
000000100000f6d (__TEXT,__text) external _test
000000100000f77 (__TEXT,__text) external _main
可以發現此時的外部函數有2
個,_printf
和dyld_stub_binder
,它們都來自libSystem
庫。dyld_stub_binder
這個函數的作用是進行運行時綁定流程
。
鏈接是在編譯時,用來確定外部函數來自哪個動態庫;綁定是在運行時,將對應方法的實現地址與符號進行綁定。
可執行文件運行結果: