1. LLVM
1.1 LLVM概述
LLVM是架構編譯器的框架系統,以C++編寫而成,用於優化任意程序語言編寫的程序的編譯時間(compile-time)、鏈接時間(link-time)、運行時間(run-time)以及空閑時間(idle-time)。對開發者保持開放,並兼容已有腳本。目前LLVM已經被蘋果IOS開發工具,Xilinx Vivado, Facebook,Google等各大公司采用。
1.2 傳統編譯器設計
源碼 Source Code + 前端 Frontend + 優化器 Optimizer + 后端 Backend(代碼生成器 CodeGenerator)+ 機器碼 Machine Code,如下圖所示

-
前端Frontend:負責解析源代碼,它會進行:詞法分析、語法分析、語義分析,檢查源代碼是否存在錯誤。然后構建針對語言的抽象語法樹(AST:Abstract Syntax Tree。LLVM 的前端還會生成中間代碼(intermediate representation,簡稱IR)。 -
優化器 Optimizer:優化器負責進行各種優化,改善代碼的運行時間,例如消除冗余計算等; -
后端 Backend(代碼生成器 Code Generator):將代碼映射到目標指令集,生成機器代碼,並且進行機器代碼相關的代碼優化;
1.3 ios的編譯器架構
OC、C、C++使用的編譯器前端是Clang,Swift是swift,后端都是LLVM,如下圖所示

1.4 LLVM的設計
LLVM設計的最重要方面是,使用通用的代碼表示形式(IR),它是用來在編譯器中表示代碼的形式,所有LLVM可以為任何編程語言獨立編寫前端,並且可以為任意硬件架構獨立編寫后端,做到了前后端分離如下所示

傳統的編譯器,前端,優化器和后端是連在一起的,是一個項目。但是在llvm中,前端和后端分開了,兩者中間有一個通用的中間層,也就是IR。前端解析源代碼,然后詞法分析、語法分析、語義分析、AST等工作完成之后,生成IR輸出給優化器,優化器負責優化IR代碼,然后后端接受IR代碼后根據需要適配的設備生成X86、ARM64等。所以,當出現一個新設備,只需要研發一個新設備的后端。出現一個高級語言,就研發高級語言的前端。這樣就能支持所有的語言和設備。
1.5 Clang
clang是LLVM項目中的一個子項目,它是基於LLVM架構圖的輕量級編譯器,誕生之初是為了替代GCC,提供更快的編譯速度,它是負責C、C++、OC語言的編譯器,屬於整個LLVM架構中的 編譯器前端,對於開發者來說,研究Clang可以給我們帶來很多好處
2. 編譯流程
可以通過以下命令打印源碼的編譯階段:
clang -ccc-print-phases main.m
這里新建一個后通過命令打印源碼的編譯階段:

- 0 -
輸入文件:找到源文件 - 1 -
預處理階段:這個過程處理包括宏的替換,頭文件的導入 - 2 -
編譯階段:進行詞法分析、語法分析、檢測語法是否正確,最終生成IR - 3 -
后端:這里LLVM會通過一個一個的pass去優化,每個pass做一些事情,最終生成匯編代碼 - 4 -
匯編代碼生成目標文件 - 5 -
鏈接:鏈接需要的動態庫和靜態庫,生成可執行文件 - 6 -
綁定:通過不同的架構,生成對應架構的可執行文件
在main.m中輸入一些代碼。

然后通過 指令clang -E main.m >> main1.m生成預處理之后的文件。
開頭是一些宏展開和.h文件的展開。

然后最后看到main函數,這里成C沒有了,變成了30.

所以我們得出:
typedef不是預處理指令,也就是說:typedef可以給數據類型取別名,但是在預處理階段不會被替換掉。define則在預處理階段會被替換,所以經常被是用來進行代碼混淆,目的是為了app安全。
3. 編譯階段
編譯階段會進行詞法分析、語法分析、檢測語法是否正確,最終生成IR。
3.1 詞法分析
預處理完成后就會進行詞法分析,這里會把代碼切成一個個Token,比如大小括號,等於號還有字符串等。 通過下列指令來查看詞法分析
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
詞法分析結果:

3.2 語法分析
詞法分析完成后就是語法分析,它的任務是驗證語法是否正確,在詞法分析的基礎上將單詞序列組合成各類詞法短語,如程序、語句、表達式 等等,然后將所有節點組成抽象語法樹(Abstract Syntax Tree,AST),語法分析程序判斷程序在結構上是否正確。
通過下列指令來查看語法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
得到下面的結果(這里面的地址是虛擬地址,還沒開辟內存,可以看作是文件的偏移地址):

FunctionDecl:函數方法聲明。ParmVarDecl: 參數聲明。CompoundStmt:復合語句。CallExpr:函數調用。BinaryOperator: 運算符。ImplicitCastExpr:函數指針。DeclRefExpr:函數類型。
3.3 生成中間代碼IR
完成以上步驟后,就開始生成中間代碼IR了,代碼生成器(Code Generation)會將語法樹自頂向下遍歷逐步翻譯成LLVM IR。
簡化一下代碼:

然后通過下列指令來生成 .ll 的文本文件,查看IR代碼。
clang -S -fobjc-arc -emit-llvm main.m
生成IR代碼如下(這一步會進行語法檢查):

@:全局標識%:局部標識alloca: 開辟空間align: 內存對齊i32: 32bit,4個字節store: 寫入內存load: 讀取數據call: 調用函數ret: 返回
上面的IR代碼是沒有經過優化的,所以會比較長。 LLVM的優化級別分別是: -O0 , -O1 , -O2 , -O3 , -Os 。 可以在xcode里面 target -> Build Settings -> optimization Level 設置優化等級。

輸入下列指令來生成優化后的IR代碼。
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
下面是優化后的IR代碼,可以明顯看出來代碼少了很多。優化等級並不是越高越好的,一般情況下,debug模式下是不進行優化的,而在release模式下是-Os 優化等級。

3.4 bitCode
xcode7以后開啟bitcode,蘋果會做進一步優化,生成.bc的中間代碼,我們通過優化后的IR代碼生成.bc代碼。Bitcode的目的是根據不同的CPU架構,蘋果能夠在APPStore直接下載不同的架構的包。
輸入下列指令來生成bc代碼。
clang -emit-llvm -c main.ll -o main.bc
4. 生成匯編代碼
到了這一步,這里就到了backend。這里LLVM會通過一個一個的pass去優化,每個pass做一些事情,最終生成匯編代碼。
我們通過生成的.bc或者.ll代碼生成匯編代碼。
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
這里分別通過main.ll,main.bc,main.m來生成匯編之后進行對比。

main.bc生成的匯編代碼:

main.ll生成的匯編代碼:

main.m生成的匯編代碼:

這里發現通過main.bc 和 main.ll 生成的匯編代碼都是54行,說明並沒有額外進行代碼優化。main.m是沒有經過優化的源碼,轉化為匯編后則多了幾行代碼。那么這里的代碼是否還能進行優化呢?試一下。 輸入以下代碼
clang -Os -S -fobjc-arc main.bc -o main3.s
這是指令運行后得到的代碼,發現比之前的又少了幾行,這就說明:當選定了優化等級了之后,在不同的節點上,還能進行優化。

5. 生成目標文件(匯編器)
目標文件的生成,是匯編器以匯編代碼作為插入,將匯編代碼轉換為機器代碼,最后輸出目標文件(object file)。
通過以下指令來生產.o文件
clang -fmodules -c main.s -o main.o
可以通過nm命令,查看下main.o中的符號
$xcrun nm -nm main.o
指令執行后發現輸出下面的結果:

_printf函數是一個是undefined、external的undefined表示在當前文件暫時找不到符號_printfexternal表示這個符號是外部可以訪問的
這里為什么undefined了呢?因為這里調用了外部的方法,這個時候就需要鏈接了。
6. 生成可執行文件(鏈接)
鏈接主要是鏈接需要的動態庫和靜態庫,生成可執行文件。
連接器把編譯生成的.o文件和 .dyld .a文件鏈接,生成一個mach-o文件
clang main.o -o main
查看鏈接之后的符號
$xcrun nm -nm main
指令執行后得到下面的結果:

這里看到有兩個undefined,一個是_printf,一個是dyld_stub_binder,但是后面都有(from libSystem)。這里的dyld_stub_binder也是一個外部函數,在dyld里面,當mach-o 進入到內存之后,外部符號就會和binder進行綁定。這個過程是dyld強制綁定的,這里就是去綁定_printf。 鏈接就是要知道內部的符號是在外面的哪個庫里面。綁定就是將外面的函數的地址和內部的符號進行綁定。鏈接在編譯期,綁定在執行期。所以只要鏈接就一定有一個外部函數也就是dyld_stub_binder。
7. clang 插件
7.1 LLVM下載
由於國內網絡限制,需要借助鏡像下載llvm的源碼鏈接: [link](https://mirror.tuna.tsinghua.edu.cn/help/llvm/).
復制代碼
下載LLVM項目
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/llvm.git
在LLVM的tool目錄下下載Clang
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
在LLVM的projects目錄下下載compiler-rt、libcxx、libcxxabi
cd ../projects
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/compiler-rt.g it
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxx.git git clone
https://mirrors.tuna.tsinghua.edu.cn/git/llvm/libcxxabi.git
在Clang的tools下安裝extra工具
cd ../tools/clang/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang-tools-extra.git
7.2 LLVM 編譯
由於最新的LLVM只支持cmake來編譯,所以需要安裝cmake
查看brew是否安裝cmake,如果已經安裝,則跳過下面步驟
brew list
通過brew安裝cmake
brew install cmake
7.3 編譯LLVM
通過xcode編譯LLVM
- cmake編譯成Xcode項目
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
- 使用xcode編譯Clang
選擇手動創建schemes
編譯(CMD + B),選擇ALL_BUILD Secheme進行編譯,預計1+小時。
通過ninja編譯LLVM
使用ninja進行編譯則還需要安裝ninja,使用以下命令安裝ninja
brew install ninja
在LLVM源碼根目錄下新建一個build_ninja目錄,最終會在build_ninja目錄下生成``build.ninja`
在LLVM源碼根目錄下新建llvm_release目錄,最終編譯文件會在llvm_release文件夾路徑下
cd llvm_build
//注意DCMAKE_INSTALL_PREFIX后面不能有空格
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX= 安裝路徑(本機為/ Users/xxx/xxx/LLVM/llvm_release)
依次執行編譯,安裝指令
ninja
ninja install
7.4 創建插件
在/llvm/tools/clang/tools下新建插件LSPlugin

在/llvm/tools/clang/tools目錄下的CMakeLists.txt文件,新增add_clang_subdirectory(LSPlugin)。

在LSPlugin目錄下新建 LSPlugin.cpp 和CMakeLists.txt,並在CMakeLists.txt中加上以下代碼
add_llvm_library( HKPlugin MODULE BUILDTREE_ONLY
LSPlugin.cpp
)

接下來利用cmake重新生成Xcode項目,在build_xcode目錄下執行以下命令
cmake -G Xcode ../llvm
最后可以在LLVM的xcode項目中可以看到Loadable modules目錄下由自定義的LSPlugin目錄了,然后可以在里面編寫插件代碼了。
