本節,我們給大家介紹一個偉大的架構編譯器LLVM
。
- 什么是編譯器
- LLVM概述
- LLVM案例體驗
1 什么是編譯器?
1.1 Python案例
- 創建
python
文件夾,新建helloDemo.py
文件,內容如下:
print("hello")
- 調用
python helloDemo.py
執行文件,打印出python
image.png
1.2 C 案例
-
vim
創建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
發現.out
文件是:64位的Mach-O
可執行文件,當前clang
出來的是x86_64
架構, mac
電腦可讀。 所以可以./a.out
直接執行:
Q:
解釋型
語言與編譯型
語言
python
是解釋型語言
,一邊翻譯
一邊執行
。和js
一樣,機器可直接執行。C
語言是編譯型語言
,不能直接執行,需要編譯器
將其轉換
成機器識別語言
。
編譯型語言
:編譯后
輸出的是指令
(0、1組合),cpu可直接執行指令
解釋性語言
:生成的是數據
,不是0、1組合
,機器也能直接識別
編譯器
的作用,就是將高級語言
轉化為機器
能夠識別
的語言
(可執行文件
)。
Q:匯編有指令嗎?
早期科學家,使用
0、1編碼
。 比如00001111
對應call
,00000111
對應bl
。有了對應關系
后。 再手敲
0和1就有點難受
了。于是寫個中間解釋器
,我們只用輸入call
、bl
這樣的標記指令
,經過解釋器
,變成0和1的組合,再交給機器去執行。 這就是匯編的由來
。而基于匯編往上,再
映射
和封裝
相關對應關系
。就跨時代性
的c
語言,再往上
層封裝,就出現了高級語言oc
、swift
等語言。所以匯編執行快
,因為它是直接轉換
為機器語言
的。但
匯編
的指令集
,是針對同一操作系統
而言,它不
支持跨平臺
。機器指令
是cpu
的在識別
。早期的計算機廠家
非常多
,雖然都用0
和1
的組合
,但相同組合背后卻是相應不同
的指令
。所以匯編無法跨平臺
,不同操作系統
下,匯編指令
是不同
的。
2. LLVM概述
-
LLVM
是架構編譯器
(compiler
)的框架系統
,以c++
編寫而成,用于優化
以任意程序語言
編寫的程序的編譯時間
(compile-time
)、鏈接時間
(link-time
)、運行時間
(run-time
)以及空閑時間
(idle-time
),對開發者保持開放,并兼任已有腳本。 - 2006年
Chris Lattner
加盟Apple Inc.
并致力于LLVM
在Apple開發體系
中的應用。Apple
也是LLVM計劃
的主要資助者
。
目前LLVM
已經被蘋果iOS開發工具
、Xilinx Vivado
、Facebook
、Google
等各大公司采用。
2.1 傳統編譯器的設計
- 編譯器前端(Frontend):
編譯器的前端任務
是解析源代碼
。 會進行詞法分析
、語法分析
、語義分析
。檢查源代碼
是否存在錯誤
,然后構建抽象語法樹
(Abstract Syntax Tree AST),LLVM前端
還會生成中間代碼
(intermediate representation, IR)
- 優化器(Optimizer)
優化器負責各種優化
。改善
代碼的運行時間
,如消除冗余計算
等
- 后端(Backkend)/ 代碼生成器(CodeGenerator)
將代碼映射
到目標指令集
,生成機器語言
,并進行機器相關
的代碼優化
(目標指不同操作系統
)
iOS的編譯器架構:
Objective C
/C
/C++
使用的編譯器前端
是Clang
,Swift
是swift
,后端都是LLVM
。
image.png
2.2 LLVM的設計
GCC
是一個非常成功
的編譯器
,但由于它作為整體應用程序
設計的,用途
受到了限制
。LLVM
最重要的地方:支持多種語言
或多種硬件架構
。使用通用代碼
表示形式:IR
(用來在編譯器中表示代碼的形式)LLVM
可以為任何編程語言
獨立編寫前端
,也可以為任何硬件架構
獨立編寫后端
.所以LLVM
不是
一個簡單的編譯器
,而是架構編譯器
,可以兼容
所有前端
和后端
。
2.3 Clang
Clang
是LLVM項目
的一個子項目
。基于LLVM架構
的輕量級編輯器
,誕生之初
就是為了替代GCC
,提供更快
的編譯速度
。 他是負責編譯C
、C++
、Objecte-C
語言的編譯器
,它屬于
整個LLVM架構
中的編譯器前端
。
- 對于開發者而言,
研究Clang
可以給我們帶來很多好處
。
3. LLVM案例體驗
- 新建一個
Mac OS
的命令行
工程:
image.png -
沒有改動代碼
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
:
預處理:宏的展開,頭文件的導入 -
2: compiler, {1}, ir
:
編譯:詞法、語法、語義分析,最終生成IR -
3: backend, {2}, assembler ()
:
匯編: LLVM通過一個個的Pass去優化,每個Pass做一些事,最后生成匯編代碼 -
4: assembler, {3}, object
:
目標文件 -
5: linker, {4}, image
:
鏈接: 鏈接需要的動態庫和靜態庫,生成可執行文件 -
6: bind-arch, "x86_64", {5}, image
:
架構可執行文件:通過不同架構,生成對應的可執行文件
optimizer優化
并沒有
作為一個獨立階段
,在編譯階段
內部完成
的
3.2 預處理階段
-
main.m
中準備測試代碼
:
#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
預編譯輸出main2.m
文件:
clang -E main.m >> main2.m
-
打開
main2.m
,有575行
。其中大部分是stdio
庫的代碼:
image.png 我們發現測試代碼中的
宏C
,在預編譯階段
完成了替換
,變成了30
預編譯階段: 1.
導入頭文件
2.替換宏
- 修改測試代碼,給
int類型
取個別名HT_INT_64
,再次預編譯處理
:
#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;
}
- 發現
typedef
不會被替換
image.png
安全拓展:
- 使用
define
將重要方法
名稱進行替換
。比如#define Pay XXXTest
這樣開發者使用宏Pay
開發舒服,但是被hank
時,實際代碼是XXXTest
,不容易被察覺。
(#define
的真實內容
,不應該
寫成亂碼
,會讓人有此地無銀三百兩
的感覺,最好
弄成系統類似名稱
或其他不經意
的名稱
。這樣才容易
被忽視
,安全級別
才更高
??)
typedef
沒有這個偷梁換柱的效果。define
只影響預處理期。
3.3 編譯階段
3.3.1 詞法分析
- 編譯
main.m
文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
-詞法分析
,就是根據空格
和括號
這些將代碼拆分
成一個個Token
。標注了位置
是第幾行
的第幾個字符
開始的。
3.3.2 語法分析
-
語法分析
是驗證語法
是否正確
。
在詞法分析的基礎上,將單詞
序列組合
成各類語法短語
,如“程序”,“語句”,“表達式”等,然后將所有節點組成抽象語法樹
(Abstract Syntax Tree,AST)。語法分析程序
判斷源程序
在結構
上是否正確
。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
作用域
、類型
、運算方式
都十分清晰
。( 語法樹一次只能處理一次計算。兩次運算,就得多分一層級。)
image.png 語法分析
,就是在生成語法樹
時完成檢測
的。
- 頭文件找不到時,可以指定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 生成中間代碼
完成以上步驟后,就開始生成
中間代碼IR
,代碼生成器(Code Generation)會將語法樹自頂向下
遍歷逐步翻譯成LLVM
的IR
。便于理解,我們簡化代碼:
#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;
}
通過下面命令生成.ll
文本文件,查看IR代碼:
clang -S -fobjc-arc -emit-llvm main.m
- IR基本語法
@
全局標識
%
局部標識
alloca
開辟空間
align
內存對齊
i32
32個bit,4個字節
store
寫入內存
load
讀取數據
call
調用數據
ret
返回
- 使用
VSCode
或Sublime Text
可以打開代碼:(可以指定文件
的語言
,讓代碼
有高亮色
)
- Q:圖中為何
多創建
那么多局部變量
?(如test函數內的a5、a6)- 因為在上一階段(
編譯階段
),我們將代碼
編譯成了語法樹結構
。而此時,我們只是沿
著語法樹
進行讀取
。 語法樹每一個層級
,都需要
一個臨時變量
來承接
。再返回上一層級處理
。- 所以會
產生
那么多局部變量
。
3.4.2 IR優化
- 我們可以在
Xcode
的Build Settings
中搜索Optimization
,可以看到優化級別。
(Debug模式
默認None [O0]
無優化,Release模式
默認Fastest,Smallest [Os]
最快最小)
LLVM的優化級別分為
-O0
、-O1
、-O2
、-O3
、-Os
(第一個字母是Optimization的O)。分別選擇
O0
和Os
兩個優化等級進行中間代碼的生成比較:
clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll // O0 無優化
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll // Os 最快最小
-
優化后
的代碼,舒服
多了。之前那些冗余
的臨時局部變量
,也都被優化
,代碼量減少
很多。
3.4.3 bitCode再優化
-
Xcode7之后
,開啟bitCode
蘋果會再進一步優化
,生成.bc
的中間代碼
。
優化體現
:上傳APPstore的包
,針對不同型號手機
做了區分
,不同型號手機下載
時,包
的大小不同
。
clang -emit-llvm -c main.ll -o main.bc
3.5 生成匯編代碼
完成
中間代碼
的生成后,可以將代碼轉變
為匯編代碼
了。-
此刻我們有
4種
不同程度的代碼(源代碼
->無優化IR代碼
->Os優化IR代碼
->bitcode優化代碼
):
image.png 分別對
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
可以看到在生成匯編代碼
時,只有選擇
了優化等級
,才能減少
匯編代碼量
。
【拓展】在
生成中間代碼
的前后
,都可以
進行優化
。
- [嘗試一] 將
main.m
直接選擇Os級別
優化生成.s
匯編文件clang -Os -S -fobjc-arc main.m -o mainOs.s
- [嘗試二] 將
main.m
生成無優化
的main.s
,再main.s
選擇Os級別
優化生成.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級別
優化生成main.s
,再main.s
選擇無優化
級別生成.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級別
優化生成main.s
,再main.s
選擇Os級別
優化生成.s
匯編文件clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
- 內容比較:
image.png
3.6 生成目標文件(機器代碼)
-
生成匯編文件
后,匯編器
以匯編代碼
作為輸入
,將匯編代碼轉換
為機器代碼
,輸出
目標文件(object file
)
clang -fmodules -c main.s -o main.o
file
對比一下main.s
匯編代碼和main.o
機器代碼:file main3.m file main.o
image.png
-
xcrun
執行nm
命令查看main.o
文件中的符號
:
xcrun nm -nm main.o
- 此時只是把
當前文件
編譯為了機器碼
,外部符號
(如printf
)無法識別。
undefined:
表示當前文件
暫時找不到符號
。
external:
表示這個符號
是外部可以訪問
的。(實現
不在我這,在外部
的某個地方
)
所以當前雖轉換
成了機器代碼
。但是只是目標文件
,并不能
直接執行
,需要將
所有資源鏈接
起來,才可以執行
。
3.7 生成可執行文件(鏈接)
- 通過
鏈接器
把編譯產生的.o
文件和.dylib
、.a
文件鏈接關聯
起來,生成真正的mach-o可執行文件
clang main.o -o main // 將目標文件轉成可執行文件
file main // 查看文件
xcrun nm -nm main // 查看main的符號
- 對比
main.o
目標文件,此時生成的main
文件:
- 從
object
文件變成了executable
可執行文件- 雖然都有
undefined
,但是可執行文件
中指定了該符號
的來源庫
。機器在運行時
,會從相應的庫
中取讀取
該符號
(printf
)
至此,我們已完整分析:源代碼
到可執行文件
的整個流程
:
-
下一節
,我們嘗試玩LLVM
。(創建插件
,增加代碼規范
,有效智能提示
)
(ps:LLVM源碼下載
和編譯教程
,都在OC底層原理三十二:LLVM插件(Copy修飾符檢測)中)