iOS底層學習——LLVM編譯流程

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

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容