LLVM概念
1.編譯器
在學習LLVM之前我們先了解一下什么是編譯器?
簡單講,編譯器就是將一種語言(通常為高級語言)翻譯為另一種語言(通常為低級[語言]的程序。
一個現代編譯器的主要工作流程:源代碼(source code) →?預處理器(preprocessor) → 編譯器(compiler) →??目標代碼(object code) →?鏈接器(Linker) → 可執行程序(executables)
源代碼一般為高級語言(High-level language), 如C、C++、Java、Objective-C等或匯編語言,而目標則是機器語言的目標代碼(Object copy),有時也稱作機器代碼(Machine code)。
我們用兩種語言來做個對比:解釋型語言和編譯型語言。
解釋型語言
下面引入一個Python代碼,見下圖:
創建一個FirstDemo.py文件,里面只有一行代碼,print('Hello world for first time')。通過解釋器指令pythop,解釋這段代碼:
通過上面的流程可以發現解釋型語言的運行流程:
解釋型語言特點:邊解釋,邊執行,運行速度慢,部分改動無需整體重新編譯,不可脫離解釋器環境運行。
編譯型語言
下面引入一個C代碼,見下圖:
創建一個firstDemoForC.c文件,里面添加了一個main函數。首先通過clang去讀取這個代碼:
讀取之后發現代碼并沒有立刻執行,而是生成了一個a.out文件。這個文件就是可執行文件。通過./a.out執行這段代碼:
通過上面的流程可以發現編譯型語言的運行流程:
編譯型語言特點:先整體編譯,再執行,運行速度快,任意改動需重新編譯,可脫離編譯環境運行。
解釋型語言:讀到相應代碼就直接執行
編譯型語言:先將代碼編譯成cpu可讀的懂的二進制才能執行
2.LLVM概述
LLVM是構架編譯器(compiler)的框架系統,以C++編寫而成。
用于優化以任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)、運行時間(run-time)以及空閑時間(idle-time),對開發者保持開放,并兼容已有腳本。
LLVM計劃啟動于2000年,最初由美國UIUC大學的Chris Lattner博士主持開展。
2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple開發體系中的應用。Apple也是LLVM計劃的主要資助者。
目前LLVM已經被Apple、Microsoft、Google、Facebook等各大公司采用。
傳統編譯器的設計
編譯器前端(Frontend)
編譯器前端的任務是解析源代碼。它會進行:詞法分析、語法分析、語義分析,檢查源代碼是否存在錯誤,然后構建抽象語法樹,LLVM的前端會生成中間代碼IR。
優化器(Optimizer)
優化器負責進行各種優化。改善運行時間,例如消除冗余計算等。
后端(Backend)
也可以叫代碼生成器(CodeGenerator),將代碼映射到目標指令集。生成機器語言,并且進行機器相關的代碼優化。
隨著高級語言越來越多,終端類型種類的增加,所使用的的CPU架構等也不盡相同。
所以為了適配多種環境,不得不設計不同的編譯器,而這些編譯器前端和后端往往是捆綁在一起的。
LLVM設計思路
LLVM的設計之初,即將編譯器前端(Frontend)和后端(Backend)進行了分離。
將前端和后端針對不同的架構,按照獨立的項目進行研發,而它們均采用通用的代碼形式IR。
當編譯器決定支持多種語言或多種硬件架構時,LLVM最重要的地方就體現出來了,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式。
所以LLVM可以為任何編程語言獨立編寫前端,并且可以為任意硬件架構獨立編寫后端。
iOS編譯架構
Objective C/C/C++使用的編譯器前端是Clang,Swift是Swift,后端都是LLVM。
3.Clang
Clang是LLVM項目中的一個子項目。它是基于LLVM架構的輕量級編譯器,誕生之初是為了替代GCC,提供更快的編譯速度。
它是負責編譯C、C++、Objective-C語言的編譯器,它屬于整個LLVM架構中,編譯器前端。對于開發者來說,研究Clang可以給我們帶來很多好處。
編譯流程
通過命令可以打印源碼的編譯階段。引入下面一個案例,main.m中添加代碼
int main(int argc, const char * argv[]) {? ? return 0;}
(滑動顯示更多)
通過指令clang -ccc-print-phases main.m,查看編譯流程:
流程說明:
輸入文件:找到源文件
預處理階段:這個過程處理包括宏的替換,頭文件的導入
編譯階段:進行詞法分析、語法分析、檢測語法是否正確,最終生成IR
后端:這里LLVM會通過一個一個的Pass(節點)去優化,每個Pass做一些事情,最終生成匯編代碼
生成目標文件
鏈接:鏈接需要的動態庫和靜態庫,生成可執行文件
通過不同的架構,生成對應的可行文件
1.預處理
執行如下指令:clang -E main.m,對源代碼進行預處理。見下面流程:
在預處理之后,輸出mainE.m文件,查看mainE.m文件:
打開mainE.m源文件會發現,其進行了宏的替換,如上面案例中宏c直接替換成了30;進行頭文件的導入。
2.編譯階段
詞法分析
預處理完成后就會進行詞法分析,這里會把代碼切成一個個Token,比如大小括號,等于號以及字符串等。詞法分析指令為:
clang?-fmodules?-fsyntax-only?-Xclang?-dump-tokens?main.m
(滑動顯示更多)
參考下面的案例:
通過指令的輸出可以看到,語法分析會將源碼進行切割并檢測。比如分號,逗號,int等。
語法分析
詞法分析完成之后就是語法分析,它的任務是驗證語法是否正確。在詞法分析的基礎上將單詞序列組合成各類語法短語。
如:程序,語句,表達式等等,然后將所有節點組成抽象語法樹(Abstract Syntax Tree,AST)。語法分析程序判斷源程序在結構上是否正確。語法分析指令:
clang?-fmodules?-fsyntax-only?-Xclang?-ast-dump?main.m
(滑動顯示更多)
語法分析輸出結果:
通過上面的輸出可以發現,其是一個樹結構,比如下面的FunctionDecl,表示一個方法,在源碼的第五行,名稱為main,返回值為int,傳入兩個參數一個是int,一個是const char **。見下圖:
這里需要注意的是,一旦生成抽象語法樹,如果源碼中存在語法錯誤,就會報錯,而上面的預處理和詞法分析不會報錯。
如在源碼中設置一個語法錯誤,通過語法分析指令進行進行編譯,就會報錯,見下面案例:
生成中間代碼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
(滑動顯示更多)
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文件,依然可以進行優化生成回應的匯編代碼。
在不同的節點上都可以進行優化。見下圖:
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_header000000100000f6d (__TEXT,__text)?external?_test000000100000f77?(__TEXT,__text)?external?_main
(滑動顯示更多)
可以發現此時的外部函數有2個,_printf和dyld_stub_binder,它們都來自libSystem庫。dyld_stub_binder這個函數的作用是進行運行時綁定流程。
鏈接是在編譯時,用來確定外部函數來自哪個動態庫;綁定是在運行時,將對應方法的實現地址與符號進行綁定。
可執行文件運行結果:
原文作者:gufs_鏡像
原文鏈接:https://juejin.cn/post/7001511391379062791