什么是LLVM
LLVM項目是模塊化、可重用的編譯器以及工具鏈技術(shù)的集合。
美國計算機協(xié)會 (ACM) 將其2012 年軟件系統(tǒng)獎項頒給了LLVM,之前曾經(jīng)獲得此獎項的軟件和技術(shù)包括:Java、Apache、 Mosaic、the World Wide Web、Smalltalk、UNIX、Eclipse等等
創(chuàng)始人:Chris Lattner,亦是Swift之父
備注:有些文章把LLVM當(dāng)做Low Level Virtual Machine(低級虛擬機)的縮寫簡稱,官方描述如下
The name "LLVM" itself is not an acronym; it is the full name of the project. “LLVM”這個名稱本身不是首字母縮略詞; 它是項目的全名。
傳統(tǒng)的編譯器架構(gòu)
傳統(tǒng)編譯器架構(gòu)
Frontend:前端
詞法分析、語法分析、語義分析、生成中間代碼
Optimizer:優(yōu)化器
中間代碼優(yōu)化
Backend:后端
生成機器碼
LLVM架構(gòu)
LLVM架構(gòu)
不同的前端后端使用統(tǒng)一的中間代碼LLVM Intermediate Representation (LLVM IR)
如果需要支持一種新的編程語言,那么只需要實現(xiàn)一個新的前端
如果需要支持一種新的硬件設(shè)備,那么只需要實現(xiàn)一個新的后端
優(yōu)化階段是一個通用的階段,它針對的是統(tǒng)一的LLVM IR,不論是支持新的編程語言,還是支持新的硬件設(shè)備,都不需要對優(yōu)化階段做修改
相比之下,GCC的前端和后端沒分得太開,前端后端耦合在了一起。所以GCC為了支持一門新的語言,或者為了支持一個新的目標(biāo)平臺,就 變得特別困難
LLVM現(xiàn)在被作為實現(xiàn)各種靜態(tài)和運行時編譯語言的通用基礎(chǔ)結(jié)構(gòu)(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
什么是Clang
LLVM項目的一個子項目,基于LLVM架構(gòu)的C/C++/Objective-C編譯器前端。
相比于GCC,Clang具有如下優(yōu)點
編譯速度快:在某些平臺上,Clang的編譯速度顯著的快過GCC(Debug模式下編譯OC速度比GGC快3倍)
占用內(nèi)存小:Clang生成的AST所占用的內(nèi)存是GCC的五分之一左右
模塊化設(shè)計:Clang采用基于庫的模塊化設(shè)計,易于 IDE 集成及其他用途的重用
診斷信息可讀性強:在編譯過程中,Clang 創(chuàng)建并保留了大量詳細(xì)的元數(shù)據(jù) (metadata),有利于調(diào)試和錯誤報告
設(shè)計清晰簡單,容易理解,易于擴展增強
Clang與LLVM關(guān)系
Clang與LLVM
LLVM整體架構(gòu),前端用的是clang,廣義的LLVM是指整個LLVM架構(gòu),一般狹義的LLVM指的是LLVM后端(包含代碼優(yōu)化和目標(biāo)代碼生成)。
源代碼(c/c++)經(jīng)過clang--> 中間代碼(經(jīng)過一系列的優(yōu)化,優(yōu)化用的是Pass) --> 機器碼
OC源文件的編譯過程
這里用Xcode創(chuàng)建一個Test項目,然后cd到main.m的上一路徑。
命令行查看編譯的過程:$ clang -ccc-print-phases main.m
$ clang -ccc-print-phases main.m 0: input,"main.m", objective-c1: preprocessor, {0}, objective-c-cpp-output2: compiler, {1}, ir3: backend, {2}, assembler4: assembler, {3}, object5: linker, {4}, image6:bind-arch,"x86_64", {5}, image
0.找到main.m文件
1.預(yù)處理器,處理include、import、宏定義
2.編譯器編譯,編譯成ir中間代碼
3.后端,生成目標(biāo)代碼
4.匯編
5.鏈接其他動態(tài)庫靜態(tài)庫
6.編譯成適合某個架構(gòu)的代碼
查看preprocessor(預(yù)處理)的結(jié)果:$ clang -E main.m
這個命令敲出,終端就會打印許多信息,大致如下:
# 1"main.m"# 1"<built-in>"1# 1"<built-in>"3# 353"<built-in>"3# 1"<command line>"1# 1"<built-in>"2# 1"main.m"2...intmain(intargc,constchar* argv[]) {@autoreleasepool{NSLog(@"Hello, World!");}return0;}
詞法分析
詞法分析,生成Token: $ clang -fmodules -E -Xclang -dump-tokens main.m
將代碼分成一個個小單元(token)
舉例如下:
voidtest(inta,intb){intc = a + b -3;? }
void'void'[StartOfLine]? Loc=identifier'test'[LeadingSpace] Loc=l_paren'('Loc=int'int'Loc=identifier'a'[LeadingSpace] Loc=comma','Loc=int'int'[LeadingSpace] Loc=identifier'b'[LeadingSpace] Loc=r_paren')'Loc=l_brace'{'Loc=int'int'[StartOfLine] [LeadingSpace]? Loc=identifier'c'[LeadingSpace] Loc=equal'='[LeadingSpace] Loc=identifier'a'[LeadingSpace] Loc=plus'+'[LeadingSpace] Loc=identifier'b'[LeadingSpace] Loc=minus'-'[LeadingSpace] Loc=numeric_constant'3'[LeadingSpace] Loc=semi';'Loc=r_brace'}'[StartOfLine]? Loc=eof''Loc=
可以看出,詞法分析的時候,將上面的代碼拆分一個個token,后面數(shù)字表示某一行的第幾個字符,例如第一個void,表示第18行第一個字符。
語法樹-AST
語法分析,生成語法樹(AST,Abstract Syntax Tree): $ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
通過語法樹,我們能知道這個代碼是做什么的。
還是剛剛的test函數(shù)
生成語法樹如下:
|-FunctionDecl 0x7fa1439f5630 line:18:6test'void (int, int)'| |-ParmVarDecl 0x7fa1439f54b0 col:15 used a'int'| |-ParmVarDecl 0x7fa1439f5528 col:22 used b'int'| `-CompoundStmt 0x7fa142167c88 |? `-DeclStmt 0x7fa142167c70 |? ? `-VarDecl 0x7fa1439f5708 col:9 c'int'cinit|? ? ? `-BinaryOperator 0x7fa142167c48 'int''-'|? ? ? ? |-BinaryOperator 0x7fa142167c00 'int''+'|?| |-ImplicitCastExpr 0x7fa1439f57b8 'int'|? ? ? ? | | `-DeclRefExpr 0x7fa1439f5768 'int'lvalue ParmVar 0x7fa1439f54b0'a''int'|?| `-ImplicitCastExpr 0x7fa1439f57d0 'int'|? ? ? ? |? `-DeclRefExpr 0x7fa1439f5790 'int'lvalue ParmVar 0x7fa1439f5528'b''int'|? ? ? ? `-IntegerLiteral 0x7fa142167c28 'int'3`-
在終端敲出的時候,終端很直觀的幫我們用顏色區(qū)分。我們可以用圖形顯示如下:
test函數(shù)的語法樹
LLVM IR
LLVM IR有3種表示形式(本質(zhì)是等價的)
text:便于閱讀的文本格式,類似于匯編語言,拓展名.ll, $ clang -S -emit-llvm main.m
memory:內(nèi)存格式
bitcode:二進(jìn)制格式,拓展名.bc, $ clang -c -emit-llvm main.m
我們以text形式編譯查看:
; Function Attrs: noinline nounwind optnone ssp uwtabledefinevoid@test(i32, i32)#2 {%3= alloca i32, align4%4= alloca i32, align4%5= alloca i32, align4store i32 %0, i32* %3, align4store i32 %1, i32* %4, align4%6= load i32, i32* %3, align4%7= load i32, i32* %4, align4%8= add nsw i32 %6, %7%9= sub nsw i32 %8,3store i32 %9, i32* %5, align4retvoid}
IR基本語法
注釋以分號 ; 開頭
全局標(biāo)識符以@開頭,局部標(biāo)識符以%開頭
alloca,在當(dāng)前函數(shù)棧幀中分配內(nèi)存
i32,32bit,4個字節(jié)的意思
align,內(nèi)存對齊
store,寫入數(shù)據(jù)
load,讀取數(shù)據(jù)
官方語法參考https://llvm.org/docs/LangRef.html
應(yīng)用與實踐
我們的開發(fā)都是基于源碼開發(fā),所以我們首先要進(jìn)行源碼下載和編譯。
源碼下載
下載LLVM$ git clonehttps://git.llvm.org/git/llvm.git/下載clang$ cd llvm/tools$ git clonehttps://git.llvm.org/git/clang.git/備注:clang是llvm的子項目,但是它們的源碼是分開的,我們需要將clang放在llvm/tools目錄下。
源碼編譯
這里我們在終端敲出的clang是xcode默認(rèn)內(nèi)置clang編譯器,我們自己要進(jìn)行LLVM開發(fā)的話,需要編譯屬于我們自己的clang編譯器
首先安裝cmake和ninja(先安裝brew,https://brew.sh/)$ brew install cmake$ brew install ninjaninja如果安裝失敗,可以直接從github獲取release版放入【/usr/local/bin】中https://github.com/ninja-build/ninja/releases在LLVM源碼同級目錄下新建一個【llvm_build】目錄(最終會在【llvm_build】目錄下生成【build.ninja】$ cd llvm_build$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安裝路徑備注:生成build.ninja,就表示編譯成功,-DCMAKE_INSTALL_PREFIX 表示編譯好的東西放在指定的路徑,-D表示參數(shù)。更多cmake相關(guān)選項,可以參考: https://llvm.org/docs/CMake.html
接下來依次執(zhí)行編譯、安裝指令
$ ninja編譯完畢后, 【llvm_build】目錄大概21.05G(這個真的是好大啊)$ ninja install
然后到這里我們的編譯就完成了。
另一種方式是通過Xcode編譯,生成Xcode項目再進(jìn)行編譯,但是速度很慢(可能需要1個多小時)。
方法如下:
在llvm同級目錄下新建一個【llvm_xcode】目錄
$ cd llvm_xcode
$ cmake -G Xcode ../llvm
應(yīng)用與實踐的參考
libclang、libTooling
官方參考:https://clang.llvm.org/docs/Tooling.html
應(yīng)用:語法樹分析、語言轉(zhuǎn)換等
Clang插件開發(fā)
官方參考
1、https://clang.llvm.org/docs/ClangPlugins.html
2、https://clang.llvm.org/docs/ExternalClangExamples.html
3、https://clang.llvm.org/docs/RAVFrontendAction.html
應(yīng)用:代碼檢查(命名規(guī)范、代碼規(guī)范)等
Pass開發(fā)
官方參考:https://llvm.org/docs/WritingAnLLVMPass.html
應(yīng)用:代碼優(yōu)化、代碼混淆等
開發(fā)新的編程語言
1、https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
2、https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest/