LLVM

前沿

瞻仰大佬

Chris Lattner

三大傑做:

  • Clang
  • LLVM
  • Swift

2010年開始編寫 Swift語言,並且一我的實現了Swift的大部分基礎架構;他也是 LVVM 以及 Clang的主要開發者。


html

什麼是LLVM

LLVM官網前端

  • The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
  • LLVM是一個模塊化和可重用的編譯器和工具鏈技術的集合。

做用:用於優化以任意程序語言編寫的程序的編譯時間(compile-time)連接時間(link-time)運行時間(run-time)以及空閒時間(idle-time).在2000年,Chris Lattner開發了這一套編譯器工具庫套件.後來隨着LLVM的發展,LLVM能夠用於常規編譯器,JIT編譯器,彙編器,調試器,靜態分析工具等一系列跟編程語言相關的工做。ios

2012年,LLVM 得到美國計算機學會 ACM 的軟件系統大獎,和 UNIX,WWW,TCP/IP,Apache,JAVA, Eclipse等齊名。c++

:LLVM工程包含了一組模塊化,可複用的編輯器和工具鏈。同其名字原意(Low Level Virtual Machine)不一樣的是,LLVM不是一個首字母縮寫,而是工程的名字。

git

Xcode版本的相對應編譯器的變遷

Xcode版本 編譯器版本
Xcode3以前 GCC
Xcode3 GCC與 LLVM混合編譯器
Xcode4 LLVM-GCC 成爲默認編譯器
Xcode4.2 LLVM3.0成爲默認編譯器
Xcode5 LLVM5.0, 完成 GCC到LLVM的過渡

GCC -> LLVM 簡介

GCC是 Xcode早期使用的一個強大的編譯器.這個編譯器被移植到各類系統中,其中就是 Mac OSX 操做系統,因此這就反映在 Xcode中,在早期的 Xcode 調試代碼的一個工具就是 GDB,它是GNU調試器.github

爲何從GCC變遷到LLVM?

Apple(包括中後期的NeXT)一直使用GCC做爲官方的編譯器。GCC做爲開源世界的編譯器標準一直作得不錯,但Apple對編譯工具會提出更高的要求。 一方面,是Apple對Objective-C語言(甚至後來對C語言)新增不少特性,但GCC開發者並不買Apple的賬——不給實現,所以索性後來二者分紅兩條分支分別開發,這也形成Apple的編譯器版本遠落後於GCC的官方版本。另外一方面,GCC的代碼耦合度過高,很差獨立,並且越是後期的版本,代碼質量越差,但Apple想作的不少功能(好比更好的IDE支持)須要模塊化的方式來調用GCC,但GCC一直不給作。甚至最近,《GCC運行環境豁免條款(英文版)》從根本上限制了LLVM-GCC的開發。 因此,這種不和讓Apple一直在尋找一個高效的、模塊化的、協議更放鬆的開源替代品.

objective-c

目前LLVM包含的主要子項目包括:

  1. LLVM Core:包含一個如今的源代碼/目標設備無關的優化器,一集一個針對不少主流(甚至於一些非主流)的CPU的彙編代碼生成支持。
  2. Clang:一個C/C++/Objective-C編譯器,致力於提供使人驚訝的快速編譯,極其有用的錯誤和警告信息,提供一個可用於構建很棒的源代碼級別的工具.
  3. dragonegg: gcc插件,可將GCC的優化和代碼生成器替換爲LLVM的相應工具。
  4. LLDB:基於LLVM提供的庫和Clang構建的優秀的本地調試器。
  5. libc++、libc++ ABI: 符合標準的,高性能的C++標準庫實現,以及對C++11的完整支持。
  6. compiler-rt:針對__fixunsdfdi和其餘目標機器上沒有一個核心IR(intermediate representation)對應的短原生指令序列時,提供高度調優過的底層代碼生成支持。
  7. OpenMP: Clang中對多平臺並行編程的runtime支持。
  8. vmkit:基於LLVM的Java和.NET虛擬機實
  9. polly: 支持高級別的循環和數據本地化優化支持的LLVM框架。
  10. libclc: OpenCL標準庫的實現
  11. klee: 基於LLVM編譯基礎設施的符號化虛擬機
  12. SAFECode:內存安全的C/C++編譯器
  13. lld: clang/llvm內置的連接器



LLVM編譯架構

傳統的靜態編譯器分爲三個階段:前端、優化和後端。 算法

典型例子:GCC編譯器, 如何作到解耦? 編程

LLVM Three-Phase 編譯器器架構:

架構優勢:

 不一樣的前端後端使用統一的中間代碼LLVM Intermediate Representation (LLVM IR)

 若是須要支持一種新的編程語言,那麼只須要實現一個新的前端

 若是須要支持一種新的硬件設備,那麼只須要實現一個新的後端

 優化階段是一個通用的階段,它針對的是統一的LLVM IR,不管是支持新的編程語言,仍是支持新的硬件設備,都不須要對優化階段作修改

 LLVM如今被做爲實現各類靜態和運行時編譯語言的通用基礎結構(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)ubuntu


Clang/Swift - LLVM 編譯器器架構:

Frontend:前端

  • 詞法分析、語法分析、語義分析、生成中間代碼

Optimizer:優化器

  • 中間代碼優化

Backend:後端

  • 生成機器碼

做爲LLVM提供的編譯器前端,clang可將用戶的源代碼(C/C++/Objective-C)編譯成語言/目標設備無關的IR(Intermediate Representation)實現。其可提供良好的插件支持,允許用戶在編譯時,運行額外的自定義動做。

Xcode



編譯過程

在列出完整步驟以前能夠先看個簡單例子。看看是如何完成一次編譯的。

#import <Foundation/Foundation.h>
#define DEFINEEight 8

int main(){
   @autoreleasepool {
       int eight = DEFINEEight;
       int six = 6;
       NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
       int rank = eight + six;
       NSLog(@"%@ rank %d", site, rank);
   }
   return 0;
}
複製代碼
  • 查看oc的c實現:
$ xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o main-arm64.cpp
複製代碼

生成的c++文件以下

int main(){
   /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
       int eight = 8;
       int six = 6;
       NSString* site = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), (const char *)"starming");
       int rank = eight + six;
       NSLog((NSString *)&__NSConstantStringImpl__var_folders_c__8jb7vhc96p1bhvf5gl7zw_9sj925cz_T_main_9c278d_mi_0, site, rank);
   }
   return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製代碼

Clang 命令

  • Clang 在概念上是編譯器前端,同時,在命令行中也做爲一個「黑盒」的 Driver。
  • 封裝了了編譯管線、前端命令、LLVM 命令、Toolchain 命令等,一 個 Clang ⾛走天下。
  • ⽅便從 gcc 遷移過來。

例如上面的查看oc的c語言實現,能夠利用clang重寫objc:

$ clang -rewrite-objc mian.m
複製代碼

利用clang命令查看整個編譯過程

$ clang -ccc-print-phases main.m
複製代碼
  • 能夠看到編譯源文件須要的幾個不一樣的階段
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output // 預處理    
2: compiler, {1}, ir    // 編譯生成IR(中間代碼)
3: backend, {2}, assembler  // 彙編器生成彙編代碼
4: assembler, {3}, object   // 生成機器碼(目標文件)
5: linker, {4}, image   // 連接
6: bind-arch, "x86_64", {5}, image  // 根據運行平臺,生成鏡像文件(Image),也就是最後的可執行文件
複製代碼

想看清clang前端的所有過程?接下來能夠繼續經過clang命令查看各階段都作了哪些處理。


1⃣ Preprocess -預處理

$ clang -E main.m
複製代碼
/*
 ... 頭文件
 
 # 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2

int main(){
    @autoreleasepool {
        int eight = 8;
        int six = 6;
        NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
        int rank = eight + six;
        NSLog(@"%@ rank %d", site, rank);
    }
    return 0;
}
複製代碼

這個過程的處理包括宏的替換,頭文件的導入,以及相似#if的處理。


2⃣ Lexical Analysis - 詞法分析

  • 詞法分析,也做 Lex 或者 Tokenization
  • 將預處理理過的代碼⽂文本轉化成 Token 流
  • 不校驗語義

預處理完成後就會進行詞法分析,這裏會把代碼切成一個個 Token,好比大小括號,等於號還有字符串等。

$ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
複製代碼

3⃣ Semantic Analysis - 語法分析

  • 語法分析,在 Clang 中由 Parser 和 Sema 兩個模塊配合完 成
  • 驗證語法是否正確
  • 根據當前語⾔言的語法,⽣生成語意節點,並將全部節點組合成 抽象語法樹(AST)

以下例子:

int testAST(int a, int b) {
   while (b != 0) {
       
       if (a > b) {
           a = a - b;
       } else {
           b = b - a;
       }
   }
   return a;
}
複製代碼

驗證語法是否正確,而後將全部節點組成抽象語法樹 AST 。

$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
複製代碼

生成以下語法樹:

`-FunctionDecl 0x7ffd8f84d678 <main.m:3:1, line:13:1> line:3:5 test 'int (int, int)'
 |-ParmVarDecl 0x7ffd8f84d4f8 <col:10, col:14> col:14 used a 'int'
 |-ParmVarDecl 0x7ffd8f84d570 <col:17, col:21> col:21 used b 'int'
 `-CompoundStmt 0x7ffd8f84dba8 <col:24, line:13:1>
   |-WhileStmt 0x7ffd8f84db30 <line:4:5, line:11:5>
   | |-<<<NULL>>>
   | |-BinaryOperator 0x7ffd8f84d7d8 <line:4:12, col:17> 'int' '!='
   | | |-ImplicitCastExpr 0x7ffd8f84d7c0 <col:12> 'int' <LValueToRValue>
   | | | `-DeclRefExpr 0x7ffd8f84d778 <col:12> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
   | | `-IntegerLiteral 0x7ffd8f84d7a0 <col:17> 'int' 0
   | `-CompoundStmt 0x7ffd8f84db10 <col:20, line:11:5>
   |   `-IfStmt 0x7ffd8f84dad8 <line:6:9, line:10:9>
   |     |-<<<NULL>>>
   |     |-<<<NULL>>>
   |     |-BinaryOperator 0x7ffd8f84d880 <line:6:13, col:17> 'int' '>'
   |     | |-ImplicitCastExpr 0x7ffd8f84d850 <col:13> 'int' <LValueToRValue>
   |     | | `-DeclRefExpr 0x7ffd8f84d800 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
   |     | `-ImplicitCastExpr 0x7ffd8f84d868 <col:17> 'int' <LValueToRValue>
   |     |   `-DeclRefExpr 0x7ffd8f84d828 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
   |     |-CompoundStmt 0x7ffd8f84d9a0 <col:20, line:8:9>
   |     | `-BinaryOperator 0x7ffd8f84d978 <line:7:13, col:21> 'int' '='
   |     |   |-DeclRefExpr 0x7ffd8f84d8a8 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
   |     |   `-BinaryOperator 0x7ffd8f84d950 <col:17, col:21> 'int' '-'
   |     |     |-ImplicitCastExpr 0x7ffd8f84d920 <col:17> 'int' <LValueToRValue>
   |     |     | `-DeclRefExpr 0x7ffd8f84d8d0 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
   |     |     `-ImplicitCastExpr 0x7ffd8f84d938 <col:21> 'int' <LValueToRValue>
   |     |       `-DeclRefExpr 0x7ffd8f84d8f8 <col:21> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
   |     `-CompoundStmt 0x7ffd8f84dab8 <line:8:16, line:10:9>
   |       `-BinaryOperator 0x7ffd8f84da90 <line:9:13, col:21> 'int' '='
   |         |-DeclRefExpr 0x7ffd8f84d9c0 <col:13> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
   |         `-BinaryOperator 0x7ffd8f84da68 <col:17, col:21> 'int' '-'
   |           |-ImplicitCastExpr 0x7ffd8f84da38 <col:17> 'int' <LValueToRValue>
   |           | `-DeclRefExpr 0x7ffd8f84d9e8 <col:17> 'int' lvalue ParmVar 0x7ffd8f84d570 'b' 'int'
   |           `-ImplicitCastExpr 0x7ffd8f84da50 <col:21> 'int' <LValueToRValue>
   |             `-DeclRefExpr 0x7ffd8f84da10 <col:21> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
   `-ReturnStmt 0x7ffd8f84db90 <line:12:5, col:12>
     `-ImplicitCastExpr 0x7ffd8f84db78 <col:12> 'int' <LValueToRValue>
       `-DeclRefExpr 0x7ffd8f84db50 <col:12> 'int' lvalue ParmVar 0x7ffd8f84d4f8 'a' 'int'
複製代碼


4⃣ Static Analysis - 靜態分析

  • 經過語法樹進行代碼靜態分析,找出非語法性錯誤
  • 模擬代碼執行路徑,分析出 control-flow graph (CFG)【MRC下會分析出引用計數的錯誤】
  • 預置了經常使用 Checker(檢查器)

5⃣ 中間代碼生成

  • CodeGen 負責將語法樹從頂至下遍歷,翻譯成 LLVM IR
  • LLVM IR 是 Frontend 的輸出,也是 LLVM Backend 的輸 入,先後端的橋接語言
  • 與 Objective-C Runtime 橋接
$ clang -S -fobjc-arc -emit-llvm main.m -o main.ll
複製代碼

到這一步,LLVM前段編譯器clang的工做已經基本作完了。



LLVM IR

  • LLVM IR有3種表示形式,但本質是等價的:
  • text:便於閱讀的文本格式,相似於彙編語言,拓展名.ll
$ clang -S -emit-llvm main.m -o main.ll
複製代碼
  • memory:內存格式
  • bitcode:二進制格式,拓展名.bc,
$ clang -c -emit-llvm main.m -o main.bc
複製代碼

Optimizer優化器

SSA(Static Single Assignment)靜態單一賦值優化

概念

In compiler design, static single assignment form (often abbreviated as SSA form or simply SSA) is a property of an intermediate representation (IR), which requires that each variable is assigned exactly once, and every variable is defined before it is used.
                               – From Wikipedia

從上面的描述能夠看出,SSA 形式的 IR 主要特徵是每一個變量只賦值一次。相比而言,非SSA形式的IR裏一個變量能夠賦值屢次。

優勢

 能夠簡化不少編譯優化方法的過程;
 對不少編譯優化方法來講,能夠得到更好的優化結果,

下面給出一個例子:

int main() {
    int x, y;
    x = 1;
    x = 2;
    y = x;
}
複製代碼
  • 非SSA
y := 1
y := 2
x := y
複製代碼

顯然,咱們一眼就能夠看出,上述代碼第一行的賦值行爲是多餘的,第三行使用的 y 值來自於第二行中的賦值。對於採用非 SSA 形式 IR 的編譯器來講,它須要作數據流分析(具體來講是到達-定義分析)來肯定選取哪一行的 y 值。可是對於 SSA 形式來講,就不存在這個問題了。以下所示:

  • SSA
y1 := 1
y2 := 2
x1 := y2
複製代碼

咱們不須要作數據流分析就能夠知道第三行中使用的y來自於第二行的定義,這個例子很好地說明了SSA的優點。除此以外,還有許多其餘的優化算法在採用SSA形式以後優化效果獲得了極大提升。甚至,有部分優化算法只能在SSA上作。

  • 這裏 LLVM 會去作些優化工做,在Xcode的編譯設置裏也能夠設置優化級別-01,-03,-0s,還能夠寫些本身的 Pass,官方有比較完整的 Pass 教程: Writing an LLVM Pass — LLVM 5 documentation

優化IR:(級別-03)

clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
複製代碼
  • Pass 是 LLVM 優化工做的一個節點,一個節點作些事,一塊兒加起來就構成了 LLVM 完整的優化和轉化。

若是開啓了 bitcode 蘋果會作進一步的優化,有新的後端架構仍是能夠用這份優化過的 bitcode 去生成。

生成字節碼:

clang -emit-llvm -c main.m -o main.bc
複製代碼

注:xx.bc文件是位流格式,因爲是二進制的,因此直接看就是一堆亂碼,查看bitcode最好的方式是用hexdump工具。


6⃣ Assemble -生成Target相關彙編

clang -S -fobjc-arc main.m -o main.s
複製代碼

7⃣ Assemble - 生成 Target 相關機器碼 Object (Mach-O)

clang -fmodules -c main.m -o main.o
複製代碼

8⃣ Link 生成 Executable 可執行文件

clang main.o -o main
執行
./main
輸出
starming rank 14
複製代碼

總結:Clang-LLVM 下,一個源文件的編譯過程:

整個工做流程猶如:

完整的編譯過程

1.優先編譯cocopods裏面的全部依賴文件

2.編譯信息寫入輔助信息,建立編譯後的文件架構
3.處理打包信息。例如development環境下處理xxxx.entitlements的打包信息
4.執行cocopods編譯前腳本 checkPods Manifest.lock
5.編譯包內全部m文件 (使用Compile和Clang的幾個主要命令)
6.連接須要的framework,例如AFNetworking.framework,Masonry.framework等信息

7.編譯xib文件
8.copy Xib文件,圖片等資源文件放到結果目錄
9.編譯imageAsserts
10.處理infoplist
11.執行Cocoapods腳本
12.copy標準庫
13.建立.app文件和簽名

個人深圳編譯流程:


clang插件開發

準備工做

  • 安裝brew

brew是一個軟件包管理工具,相似於centos下的yum或者ubuntu下的apt-get,很是方便,免去了本身手動編譯安裝的不便
brew 安裝目錄 /usr/local/Cellar
brew 配置目錄 /usr/local/etc
brew 命令目錄 /usr/local/bin
注:homebrew在安裝完成後自動在/usr/local/bin加個軟鏈接,因此日常都是用這個路徑

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 
複製代碼
  • 安裝cmake

CMake是一個跨平臺的編譯(Build)工具,能夠用簡單的語句來描述全部平臺的編譯過程。

$ brew install cmake
複製代碼
  • 安裝ninja

Ninja 是一個構建系統,與 Make 相似。做爲輸入,你須要描述將源文件處理爲目標文件這一過程所需的命令。 Ninja 使用這些命令保持目標處於最新狀態。 Ninja 的主要設計目標是速度。

$ brew install ninja
複製代碼

下載編譯工具

  • 下載LLVM
// 大小648.2M
$ git clone https://git.llvm.org/git/llvm.git/
複製代碼
  • 下載clang
// 大小240.6M
$ cd llvm/tools
$ git clone https://git.llvm.org/git/clang.git/
複製代碼
  • 在LLVM源碼同級目錄下新建一個【llvm_build】目錄(最終會在【llvm_build】目錄下生成【build.ninja】)
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安裝路徑
複製代碼
  • 依次執行編譯、安裝指令
$ ninja
複製代碼

編譯完畢後, 【llvm_build】目錄大概 21.05 G(僅供參考)

$ ninja install
複製代碼

安裝完畢後,安裝目錄大概 11.92 G(僅供參考)

  • 在llvm同級目錄下新建一個【llvm_xcode】目錄
$ cd llvm_xcode
$ cmake -G Xcode ../llvm
複製代碼

開始開發插件

  • 在【llvm/tools/clang/tools】源碼目錄下新建一個插件目錄,假設叫作【yb-plugin】

  • 在【llvm/tools/clang/tools/CMakeLists.txt】最後加入內容: add_clang_subdirectory(yb-plugin),小括號裏是插件目錄名

# libclang may require clang-tidy in clang-tools-extra.
add_clang_subdirectory(libclang)
add_clang_subdirectory(yb-plugin)
複製代碼
  • 在【yb-plugin】目錄下新建一個【YBPlugin.cpp】和【CMakeLists.txt】,並在【CMakeLists.txt】文件裏面添加以下內容:
add_llvm_loadable_module(YBPlugin YBPlugin.cpp)
複製代碼

MJPlugin是插件名,MJPlugin.cpp是源代碼文件


編譯插件

生成xcode項目

  • 利用cmake生成的Xcode項目來編譯插件(第一次編寫完插件,須要利用cmake從新生成一下Xcode項目)

編寫插件

  • 插件源代碼在【Sources/Loadable modules】目錄下能夠找到,這樣就能夠直接在Xcode裏編寫插件代碼

編譯插件生成動態庫文件

  • 選擇MJPlugin這個target進行編譯,編譯完會生成一個動態庫文件,將動態庫文件存放在桌面。

加載插件

  • 在Xcode項目中指定加載插件動態庫:BuildSettings > OTHER_CFLAGS
-Xclang -load -Xclang 動態庫路徑 -Xclang -add-plugin -Xclang 插件名稱
複製代碼


使用插件

  • 首先要對Xcode進行Hack,才能修改默認的編譯器

下載【XcodeHacking.zip】,解壓,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的內容,設 置一下本身編譯好的clang的路徑

  • 而後在XcodeHacking目錄下進行命令行,將XcodeHacking的內容剪切到Xcode內部
$ sudo mv HackedClang.xcplugin `xcode-select-print- path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
$ sudo mv HackedBuildSystem.xcspec `xcode-select-print- path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
複製代碼
  • 配置項目

插件效果



應用場景

clang插件編寫

代碼混淆

APP包瘦身

iOS動態化方案——OCS

開發新的編程語言



推薦書記

相關文章
相關標籤/搜索