前言
- 今天來學習一下牛逼的架構編譯器LLVM
學習大綱
- 簡單了解編譯器
- LLVM概述
- LLVM案例體驗
- LLVM源碼 & 編譯流程
準備
github上的官方源碼:https://github.com/llvm/llvm-project(國內網絡限制) ,需要注意的是,官方源碼不能直接編譯,需要下載
clang
、compiler-rt
、libcxx
、libcxxabi
這4個庫。建議使用上面Gitee源。
一 、簡單了解編譯器
-
解釋型語言
與編譯型語言
- 編譯型語言:編譯后輸出的是
指令
(0、1組合),cpu可直接執行指令 - 解釋性語言:生成的是數據,不是0、1組合,機器也能直接識別
- 編譯型語言:編譯后輸出的是
python
是解釋型語言
,一邊翻譯一邊執行。和js一樣,機器可直接執行
。
C語言
是編譯型語言
,不能直接執行,需要編譯器
將其轉換成機器識別語言
。
編譯器的
作用
,就是將高級語言轉化為機器能夠識別的語言(可執行文件)。匯編指令
- 早期科學家,使用0、1編碼。 比如 00001111 對應 call, 00000111 對應bl。有了對應關系后。 再手敲0和1就有點難受了。于是寫個中間解釋器,我們只用輸入call、bl這樣的標記指令,經過解釋器,變成0和1的組合,再交給機器去執行。 這就是匯編的由來。
- 而基于匯編往上,再
映射
和封裝
相關對應關系。就跨時代性的c語言,再往上層封裝,就出現了高級語言oc、swift等語言。所以匯編執行快,因為它是直接轉換為機器語言的。- 但
匯編的指令集
,是針對同一操作系統
而言,它不支持跨平臺
。機器指令是cpu的在識別。早期的計算機廠家非常多,雖然都用0和1的組合,但相同組合背后卻是相應不同的指令。所以匯編無法跨平臺
,不同操作系統
下,匯編指令
是不同
的。
- 案例創建體驗
- Python 案例:創建python文件夾,新建helloDemo.py文件,
內容:
print("hello")
可以看出 Python文件內容 可直接執行
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查看文件:
發現.out文件是:64位的
Mach-O
可執行文件,當前clang出來的是x86_64
架構, mac電腦可讀。 所以可以./a.out直接執行:可以看出 C 文件內容 需要編譯
二 、 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等各大公司采用。
1. 傳統編譯器的設計
- 編譯器前端(Frontend):
編譯器的前端任務是解析源代碼。 會進行詞法分析、語法分析、語義分析。
檢查源代碼
是否存在錯誤,然后構建抽象語法樹
(Abstract Syntax Tree AST),LLVM前端還會生成中間代碼
(intermediate representation, IR)
- 優化器(Optimizer)
優化器
負責各種優化
。改善代碼的運行時間,如消除冗余計算等
- 后端(Backkend)/ 代碼生成器(CodeGenerator)
將
代碼
映射到目標指令集
,生成機器語言
,并進行機器相關的代碼優化
(目標指不同操作系統)
2. iOS的編譯器架構
Objective C / C / C++ 使用的編譯器前端是Clang,Swift是swift,后端都是LLVM。
3. LLVM的設計
傳統編譯器(如
CGG
)的前端和后端沒有完全分離,耦合在了一起,因而如果要支持一門新的語言或硬件平臺,需要做大量的工作。LLVM
最重要的地方:支持多種語言
或多種硬件架構
。使用通用代碼表示形式:IR
(用來在編譯器中表示代碼的形式)LLVM
可以為任何編程語言獨立編寫前端
,也可以為任何硬件架構獨立編寫后端
.所以
LLVM
不是一個簡單的編譯器,而是架構編譯器
,可以兼容所有前端和后端。LLVM
同時支持AOT
預先編譯和JIT
即時編譯
- 不同的前端后端使用統一的中間代碼LLVM Intermediate Representation (LLVM IR)
- LLVM IR格式以 .ll結尾、以 .bc 的二進制格式結尾、內存格式
- Bitcode(Xcode 7之后)就是以.bc結尾的中間代碼,是LLVM-IR在磁盤上的一種二進制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
- 如果要轉換成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
- 蘋果單獨對 Bitcode 進行了額外的優化.
- i) 應用上傳到 AppStore時,Xcode會將程序對應的 Bitcode一起上傳;
- ii) AppStore會將 Bitcode重新編譯為可執行程序,供用戶下載;
- iii) Bitcode被Xcode打包成 xar文檔,嵌入的 MachO中。
- 如果需要支持一種新的編程語言、硬件設備,那么只需要實現一個新的前后端
4. Clang簡介
Clang
是LLVM項目
的一個子項目
。基于LLVM架構的輕量級編輯器,誕生之初就是為了替代GCC,提供更快的編譯速度。 他是負責編譯
C、C++、Objecte-C語言的編譯器,它屬于整個LLVM架構中的編譯器前端
。
對于開發者而言,研究Clang可以給我們帶來很多好處。
三 、 LLVM案例體驗
-
新建一個
Mac OS
的命令行
工程: 沒有改動代碼
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
庫的代碼: 我們發現測試代碼中的
宏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
不會被替換
使用安全拓展:
- 使用
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
。標注了位置
是第幾行
的第幾個字符
開始的。
Last login: Mon Nov 16 16:43:13 on ttys000
ios@HJ ~ % cd /Users/ios/Desktop/學習資料/hu/TestDemo/TestDemo
ios@HJ TestDemo % clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
annot_module_include '#import <Foundation/Foundation.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepoo' Loc=<main.m:8:1>
int 'int' [StartOfLine] Loc=<main.m:11:1>
identifier 'main' [LeadingSpace] Loc=<main.m:11:5>
l_paren '(' Loc=<main.m:11:9>
int 'int' Loc=<main.m:11:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:11:14>
comma ',' Loc=<main.m:11:18>
const 'const' [LeadingSpace] Loc=<main.m:11:20>
char 'char' [LeadingSpace] Loc=<main.m:11:26>
star '*' [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:11:33>
l_square '[' Loc=<main.m:11:37>
r_square ']' Loc=<main.m:11:38>
r_paren ')' Loc=<main.m:11:39>
l_brace '{' [LeadingSpace] Loc=<main.m:11:41>
at '@' [StartOfLine] [LeadingSpace] Loc=<main.m:12:5>
identifier 'autoreleasepool' Loc=<main.m:12:6>
l_brace '{' [LeadingSpace] Loc=<main.m:12:22>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:13:9>
identifier 'a' [LeadingSpace] Loc=<main.m:13:13>
equal '=' [LeadingSpace] Loc=<main.m:13:15>
numeric_constant '10' [LeadingSpace] Loc=<main.m:13:17>
semi ';' Loc=<main.m:13:19>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:14:9>
identifier 'b' [LeadingSpace] Loc=<main.m:14:13>
equal '=' [LeadingSpace] Loc=<main.m:14:15>
numeric_constant '20' [LeadingSpace] Loc=<main.m:14:17>
semi ';' Loc=<main.m:14:19>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:15:9>
l_paren '(' Loc=<main.m:15:15>
string_literal '"%d"' Loc=<main.m:15:16>
comma ',' Loc=<main.m:15:20>
identifier 'a' [LeadingSpace] Loc=<main.m:15:22>
plus '+' [LeadingSpace] Loc=<main.m:15:24>
identifier 'b' [LeadingSpace] Loc=<main.m:15:26>
plus '+' [LeadingSpace] Loc=<main.m:15:28>
numeric_constant '30' [LeadingSpace] Loc=<main.m:15:30 <Spelling=main.m:9:11>>
r_paren ')' Loc=<main.m:15:31>
semi ';' Loc=<main.m:15:32>
r_brace '}' [StartOfLine] [LeadingSpace] Loc=<main.m:16:5>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:17:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:17:12>
semi ';' Loc=<main.m:17:13>
r_brace '}' [StartOfLine] Loc=<main.m:18:1>
eof '' Loc=<main.m:18:2>
ios@HJ TestDemo %
3.3.2 語法分析
-
語法分析
是驗證語法
是否正確
。
在詞法分析的基礎上,將單詞
序列組合
成各類語法短語
,如“程序”,“語句”,“表達式”等,然后將所有節點組成抽象語法樹
(Abstract Syntax Tree,AST)。語法分析程序
判斷源程序
在結構
上是否正確
。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
作用域
、類型
、運算方式
都十分清晰
。( 語法樹一次只能處理一次計算。兩次運算,就得多分一層級。)
-
語法分析
,就是在生成語法樹
時完成檢測
的。
- 頭文件找不到時,可以指定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 分別對
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
在生成匯編代碼
時,只有選擇
了優化等級
,才能減少
匯編代碼量
。
3.6 生成目標文件(機器代碼)
-
生成匯編文件
后,匯編器
以匯編代碼
作為輸入
,將匯編代碼轉換
為機器代碼
,輸出
目標文件(object file
)
clang -fmodules -c main.s -o main.o
-
file
對比一下main.s
匯編代碼和main.o
機器代碼:
file main3.m
file main.o
-
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源碼 & 編譯流程
【注意】
LLVM
源碼2.29G
,編譯后
文件30G
,請確保
電腦硬盤空間足夠
;編譯時
,電腦溫度會飆升90多度
,請用空調伺候
著,可能
會黑屏
;編譯時間
長達1個多小時
,請合理安排時間。
如果以上3點
,你確定能接受
,那我們就開始
吧。
4.1 LLVM下載
-
Gitee
上有已配置
好關聯庫
的源碼
,可直接下載: https://gitee.com/mirrors/LLVM
4.2 LLVM編譯
- 最新的LLVM只支持
cmake
編譯,需要使用Homebrew
安裝cmake
:
4.2.1 安裝cmake
- 查看
brew列表
,檢查是否安裝過cmake
,如果有,就跳過此步驟
brew list
- 如果沒有,就使用
brew安裝
:
brew install cmake
如果報權限錯誤,可sudo chown -R
whoami:admin /usr/local/share
放開權限
4.2.2 編譯llvm
-
cmake
編譯成Xcode
項目
cd llvm-project
mkdir build
cd build
cmake -G Xcode ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm
-
成功之后,可以看到生成的
Xcode
文件:image -
打開
LLVM.xcodeproj
,選擇自動創建Schemes
。image -
自動創建完成
后,選擇ALL_BUILD
進行編譯(耗時0.5-1小時,CPU滿負荷運轉)image 編譯完成。接下來我們開始創建插件。