https://zhuanlan.zhihu.com/p/66793637
https://zhuanlan.zhihu.com/p/66909226
內容概要
- 什么是LLVM IR?如何得到IR?
- LLVM編譯的流程,IR文件之間的鏈接簡介
- C++ name mangling的用途,“extern C"作用的極簡介紹
- IR文件的布局
- IR中函數定義的結構,什么是BB,什么是CFG
- IR是一個強類型語言,如何用工具檢查IR的合法性
- 如何理解Language reference
- 常見的terminator instruction介紹
- 如何利用工具得到函數的CFG
- 什么是SSA?SSA的好處和問題,以及如何解決這個問題
參考文獻
- what is tail reursion
- make clang compile to ll
- -cc1的含義
- clang和clang++的區別
- what is a linkage unit?
- LLVM LanguageRef
- extern "C"的作用
- what is name mangling
- what is static single assignment?
- what is reaching definition?
1. 什么是LLVM IR?
- LLVM IR 是 LLVM Intermediate Representation,它是一種 low-level languange,是一個像RISC的指令集。
- 然而可以很表達high-level的ideas,就是說high-level languange可以很干凈地map到LLVM IR
- 這使得我們可以高效地進行代碼優化
2. 如何得到IR?
我們先以尾遞歸的形式實現一個階乘,再在main函數中調用中這個階乘
// factorial.c int factorial(int val, int total) { if(val==1) return total; return factorial(val-1, val * total); }
// main.cpp extern "C" int factorial(int); int main(int argc, char** argv) { return factorial(2, 1) * 7 == 42; }
注:這里的extern "C"是必要的,為了支持C++的函數重載和作用域的可見性的規則,編譯器會對函數進行name mangling, 如果不加extern "C",下文中生成的main.ll文件中factorial的函數名會被mangling成類似_Z9factoriali的樣子,鏈接器便找不到要鏈接的函數。
LLVM IR有兩種等價的格式,一種是.bc(Bitcode)文件,另一種是.ll文件,.ll文件是Human-readable的格式。 我們可以使用下面的命令得到這兩種格式的IR文件
$ clang -S -emit-llvm factorial.c # factorial.ll
$ clang -c -emit-llvm factorial.c # factorial.bc
我們可以利用grep命令查看clang參數的含義
$ clang --help | grep -w -- -[Sc]
-c Only run preprocess, compile, and assemble steps
-S Only run preprocess and compilation steps
既然兩種格式等價,自然就可以相互轉換
$ llvm-as factorial.ll # factorial.bc
$ llvm-dis factorial.bc # factorial.ll
對於cpp文件,只需將clang命令換成clang++即可。
$ clang++ -S -emit-llvm main.cpp # main.ll
$ clang++ -c -emit-llvm main.cpp # main.bc
3. IR文件之間的鏈接以及將IR轉為Target machine code
上圖顯示了llvm編譯代碼的一個pipleline, 其利用不同高級語言對應的前端(這里C/C++的前端都是clang)將其transform成LLVM IR,進行優化,鏈接后,再傳給不同target的后端transform成target-specific的二進制代碼。IR是LLVM的power所在,我們看下面這條command:
$ llvm-link factorial.bc main.bc -o linked.bc # lined.bc
llvm-link將兩個IR文件鏈接起來了,值得注意的是factorial.bc是C轉成的IR,而 main.bc是C++轉成的IR,也就是說到了IR這個level,高級語言之間的差異消失了!它們之間可以相互鏈接(這里只是演示了C和C++的,其他語言的也可以鏈接)。
我們進一步可以將鏈接得到的IR轉成target相關的code
llc --march=x86-64 linked.bc # linked.s
下圖展示了完整的build過程
4. IR文件的布局
4.1 Target information
我們以linked.ll為例進行解析,文件的開頭是
; ModuleID = 'linked.bc' source_filename = "llvm-link" target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128" target triple = "x86_64-unknown-linux-gnu"
;后面的注釋指明了module的標識,source_filename是表明這個module是從什么文件編譯得到的(如果你打開main.ll會發現這里的值是main.cpp),如果該modules是通過鏈接得到的,這里的值就會是llvm-link。
Target information的主要結構如下:
4.2 函數定義的主要結構
我們看一下函數factorial的定義
; Function Attrs: noinline nounwind optnone uwtable define dso_local i32 @factorial(i32 %val, i32 %total) #0 { entry: %retval = alloca i32, align 4 %val.addr = alloca i32, align 4 %total.addr = alloca i32, align 4 store i32 %val, i32* %val.addr, align 4 store i32 %total, i32* %total.addr, align 4 %0 = load i32, i32* %val.addr, align 4 %cmp = icmp eq i32 %0, 1 br i1 %cmp, label %if.then, label %if.end if.then: ; preds = %entry %1 = load i32, i32* %total.addr, align 4 store i32 %1, i32* %retval, align 4 br label %return if.end: ; preds = %entry %2 = load i32, i32* %val.addr, align 4 %sub = sub nsw i32 %2, 1 %3 = load i32, i32* %val.addr, align 4 %4 = load i32, i32* %total.addr, align 4 %mul = mul nsw i32 %3, %4 %call = call i32 @factorial(i32 %sub, i32 %mul) store i32 %call, i32* %retval, align 4 br label %return return: ; preds = %if.end, %if.then %5 = load i32, i32* %retval, align 4 ret i32 %5 }
前面已經提到,;表示單行注釋的開始。define dso_local i32 @factorial(i32 %val) #0表明開始定義一個函數,其中第一個i32是返回值類型,對應C語言中的int;%factorial是函數名;第二個i32是形參類型,%val是形參名。llvm中的標識符分為兩種類型:全局的和局部的。全局的標識符包括函數名和全局變量,會加一個@前綴,局部的標識符會加一個%前綴。一般地,可用標識符對應的正則表達式為[%@][-a-zA-Z$._][-a-zA-Z$._0-9]*。
dso_local是一個Runtime Preemption說明符,表明該函數會在同一個鏈接單元(即該函數所在的文件以及包含的頭文件)內解析符號。#0指出了該函數的attribute group。在文件的下面,你會找到類似這樣的代碼
attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
因為attribute group可能很包含很多attribute且復用到多個函數,所以我們IR使用attribute group ID(即#0)的形式指明函數的attribute,這樣既簡潔又清晰。
在一對花括號里的就是函數體,函數體是由一系列basic blocks(BB)組成的,這些BB形成了函數的控制流圖(Control Flow Graph, CFG)。每個BB都有一個label,label使得該BB有一個符號表的入口點,在函數factorial中,這些BB的label就是entry、if.then、if.end,BB總是以terminator instruction(e.g. ret、br、callbr)結尾的。
5. IR是一個強類型語言
看一下函數main的定義
; Function Attrs: noinline norecurse optnone uwtable define dso_local i32 @main(i32 %argc, i8** %argv) #1 { entry: %retval = alloca i32, align 4 %argc.addr = alloca i32, align 4 %argv.addr = alloca i8**, align 8 store i32 0, i32* %retval, align 4 store i32 %argc, i32* %argc.addr, align 4 store i8** %argv, i8*** %argv.addr, align 8 %call = call i32 @factorial(i32 2, i32 1) %mul = mul nsw i32 %call, 7 %cmp = icmp eq i32 %mul, 42 %conv = zext i1 %cmp to i32 ret i32 %conv }
LLVM的IR是一個強類型語言,每一條指令都顯式地指出了實參的類型,例如mul nsw i32 %call, 7表明要將兩個i32的數值相乘,icmp eq i32 %mul, 42表明要將兩個i32的數據類型進行相等比較(這里%mul是一個變量,而mul是一條指令,可以看出IR加前綴的好處)。此外,我們還很容易推斷出返回值的類型,比如i32的數相乘的返回值就是i32類型,比較兩個數值的相等關系的返回值就是i1類型。
強類型不但使得IR很human readable,也使得在優化IR時不需要考慮隱式類型轉換的影響。在main函數的結尾,zext i1 %cmp to i32將%cmp從1位整數擴展成了32位的整數(即做了一個類型提升)。如果我們把最后兩行用以下代碼替代
ret i32 %cmp
那么這段IR就變成illegal的,檢查IR是否合法可以使用opt -verify <filename>命令
$ opt -verify linked.ll
opt: linked.ll:45:11: error: '%cmp' defined with type 'i1' but expected 'i32'
ret i32 %cmp
6. LangRef is your friend
在函數main的定義中,我們可以看到這樣一條IR
%call = call i32 @factorial(i32 2)
對照着相應的C++代碼我們很容易可以猜出每個符號的含義,但是每條指令可以有很多的變體,當我們不確定符號的含義的時候,LangRef為我們提供了參考
<result> = [tail | musttail | notail ] call [fast-math flags] [cconv] [ret attrs] [addrspace(<num>)] <ty>|<fnty> <fnptrval>(<function args>) [fn attrs] [ operand bundles ]
[]包圍的表示可選參數(可以不寫),<>包圍的表示必選參數,選項用|分格開,表示只能寫其中一個。
6. 常見的terminator instruction介紹
6.1 ret
語法
ret <type> <value> ; Return a value from a non-void function ret void ; Return from void function
概述
ret用來將控制流從callee返回給caller
Example
ret i32 5 ; Return an integer value of 5 ret void ; Return from a void function ret { i32, i8 } { i32 4, i8 2 } ; Return a struct of values 4 and 2
6.2 br
語法
br i1 <cond>, label <iftrue>, label <iffalse> br label <dest> ; Unconditional branch
概述
br用來將控制流轉交給當前函數中的另一個BB。
Example
Test: %cond = icmp eq i32 %a, %b br i1 %cond, label %IfEqual, label %IfUnequal IfEqual: ret i32 1 IfUnequal: ret i32 0
6.3 switch
語法
switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]
概述
switch根據一個整型變量的值,將控制流交給不同的BB。
Example
; Emulate a conditional br instruction %Val = zext i1 %value to i32 switch i32 %Val, label %truedest [ i32 0, label %falsedest ] ; Emulate an unconditional br instruction switch i32 0, label %dest [ ] ; Implement a jump table: switch i32 %val, label %otherwise [ i32 0, label %onzero i32 1, label %onone i32 2, label %ontwo ]
6.4 unreachable
語法
unreachable
概述
unreachable告訴optimizer控制流時到不了這塊代碼,就是說這塊代碼是dead code。
Example
在展示unreachable的用法的之前,我們先看一下undef的用法。undef表示一個未定義的值,只要是常量可以出現的位置,都可以使用undef。(此Example標題下的代碼為偽代碼)
%A = or %X, undef %B = and %X, undef
or指令和and指令分別是執行按位或和按位與的操作,由於undef的值是未定義的,因此編譯器可以隨意假設它的值來對代碼進行優化,譬如說假設undef的值都是0
%A = %X %B = 0
可以假設undef的值是-1
%A = -1 %B = %X
也可以假設undef的兩處值是不同的,譬如第一處是0,第二處是-1
%A = -1 %B = 0
為什么undef的值可以不同呢?這是因為undef對應的值是沒有確定的生存期的,當我們需要一個undef的值的時候,編譯器會從可用的寄存器中隨意取一個值拿過來,因此並不能保證其值隨時間變化具有一致性。下面我們可以看unreachable的例子了
%A = sdiv undef, %X %B = sdiv %X, undef
sdiv指令是用來進行整數/向量的除法運算的,編譯器可以假設undef的值是0,因為一個數除以0是未定義行為,因此編譯器可以認為其是dead code,將其優化成
%A = 0 unreachable
6. 控制流圖(Control Flow Graph)
既然函數體是由一系列basic blocks(BB)組成的,並且BB形成了函數的控制流圖,每個BB都有唯一的label,那么我們就可以label之間的跳轉關系來表示整個函數的控制流圖,llvm提供了opt -analyze -dot-cfg-only <filename>命令來幫助我們生成
$ opt -analyze -dot-cfg-only factorial.ll
$ vim .factorial.dot
digraph "CFG for 'factorial' function" {
label="CFG for 'factorial' function";
Node0x207ced0 [shape=record,label="{entry|{<s0>T|<s1>F}}"];
Node0x207ced0:s0 -> Node0x207d7e0;
Node0x207ced0:s1 -> Node0x207d8b0;
Node0x207d7e0 [shape=record,label="{if.then}"];
Node0x207d7e0 -> Node0x207da90;
Node0x207d8b0 [shape=record,label="{if.end}"];
Node0x207d8b0 -> Node0x207da90;
Node0x207da90 [shape=record,label="{return}"];
}
把它畫成圖就非常清晰了
7. IR是靜態單一賦值的(Static Single Assignment)
在IR中,每個變量都在使用前都必須先定義,且每個變量只能被賦值一次(如果套用C++的術語,就是說每個變量只能被初始化,不能被賦值),所以我們稱IR是靜態單一賦值的。舉個例子的,假如你想返回a*b+c的值,你覺得可能可以這么寫
%0 = mul i32 %a, %b %0 = add i32 %0, %c ret i32 %0
但是這里%0被賦值了兩次,是不合法的,我們需要把它修改成這樣
%0 = mul i32 %a, %b %1 = add i32 %0, %c ret i32 %1
7.1 SSA的好處
SSA可以簡化編譯器的優化過程,譬如說,考慮這段代碼
d1: y := 1
d2: y := 2
d3: x := y
我們很容易可以看出第一次對y賦值是不必要的,在對x賦值時使用的y的值時第二次賦值的結果,但是編譯器必須要經過一個定義可達性(Reaching definition)分析才能做出判斷。編譯器是怎么分析呢?首先我們先介紹幾個概念:
變量x的定義是指一個會給x賦值或可能給x賦值的語句,譬如d1就是對y的一個定義
當一個變量x有新的定義后 ,舊的的定義會被新的定義kill掉,譬如d2就kill掉了d1。
一個定義d到達點p是指存在一條d到p路徑,在這條路徑上,d沒有被kill掉
t1是t2的reaching definition是指存在一條t1到t2路徑,沿着這條路徑走就可以得到t1要賦值的變量的值,而不需要額外的信息。
按照上面的代碼寫法,編譯器是很難判斷d3的reaching definition的。因為d3的reaching definition可能是d1,也可能是d2,要搞清楚d1和d2誰kill了誰很麻煩。但是,如果我們的代碼是SSA的,則代碼就會長成這樣
d1: y1 := 1
d2: y2 := 2
d3: x := y2
編譯發現x是由y2賦值得到,而y2被賦值了2,且x和y2都只能被賦值一次,顯然得到x的值的路徑就是唯一確定的,d2就是d3的reaching definition。
7.3 SSA帶來的問題
假設你想用IR寫一個用循環實現的factorial函數
int factorial(int val) { int temp = 1; for (int i = 2; i <= val; ++i) temp *= i; return temp; }
按照C語言的思路,我們可能大概想這樣寫
然而跑opt -verify <filename>命令我們就會發現%temp和%i被多次賦值了,這不合法。但是如果我們把第二處的%temp和%i換掉,改成這樣
那返回值就會永遠是1。
7.4 phi指令來救場
語法
<result> = phi <ty> [<val0>, <label0>], [<val1>, <label1>] …
概述
根據前一個執行的是哪一個BB來選擇一個變量的值。
有了phi指令,我們就可以把代碼改成這樣
這樣的話,每個變量就只被賦值一次,並且實現了循環遞增的效果。
7.5 alloca指令來救場
語法
<result> = alloca [inalloca] <type> [, <ty> <NumElements>] [, align <alignment>] [, addrspace(<num>)]
概述
在當前執行的函數的棧幀上分配內存並返回一個指向這片內存的指針,當函數返回時內存會被自動釋放(一般是改變棧指針)。
有了alloca指令,我們也可以通過使用指針的方式間接多次對變量賦值來騙過SSA檢查
內容概要
- IR的全局變量
- IR中的Aggregate Types
getelementptr指令的使用
參考文獻
1. 全局變量
IR中的全局變量定義了一塊在編譯期分配的內存區域,其類型是一個指針,跟指令alloca的返回值用法一樣。我們看一下一段使用全局變量簡單的C代碼對應的IR是什么樣子
// a.c static const int a=0; const int b=1; const int c=1; int d=a+1;
; a.ll @b = dso_local constant i32 1, align 4 @c = dso_local constant i32 1, align 4 @d = dso_local global i32 1, align 4
前面已經講過dso_local是一個Runtime Preemption,表明該變量會在同一個鏈接單元內解析符號,align 4表示4字節對齊。global和constant關鍵字都可以用來定義一個全局變量,全局變量名必須有@前綴,因為全局變量會參與鏈接,所以除去前綴外,其名字會跟你用C語言定義時的相同。
因為我們定義變量a時使用了C語言的static關鍵字,也就是說a是local to file的,不參與鏈接,因此我們可以在生成的IR中可以看到,其被優化掉了。
// b.c extern const int b; extern const int c; extern const int d; int f() { return b*c+d; }
; b.ll @b = external dso_local constant i32, align 4 @c = external dso_local constant i32, align 4 @d = external dso_local constant i32, align 4 define dso_local i32 @f() #0 { entry: %0 = load i32, i32* @b, align 4 %1 = load i32, i32* @c, align 4 %mul = mul nsw i32 %0, %1 %2 = load i32, i32* @d, align 4 %add = add nsw i32 %mul, %2 ret i32 %add }
從函數f的IR可以看到,全局變量其實是一個指針,在使用其時需要load指令(賦值時需要store指令)。那gloal和constant有什么區別呢?constant相比global,多賦予了全局變量一個const屬性(對應C++的底層const的概念,表示指針指向的對象是一個常量)。
跟C/C++類似,IR中可以在定義全局變量時使用global,而在聲明全局變量時使用constant,表示該變量在本文件內不改變其值。
我們可以使用opt -S --globalopt <filename>命令對全局變量進行優化
$ opt -S --globalopt a.ll -o a-opt.ll
@b = dso_local local_unnamed_addr constant i32 1, align 4
@c = dso_local local_unnamed_addr constant i32 1, align 4
@d = dso_local local_unnamed_addr global i32 1, align 4
可以看到優化過,全局變量前多了local_unnamed_addr的attribute, 該屬性表明在這個module內,這個變量的地址是不重要的,只要關心它的值就好。有什么作用呢?譬如說這里b和c都是常量且等於1,又有local_unnamed_addr屬性,編譯器就可以把b和c合並成一個變量。
2. Aggregate Types
這里我們使用英文Aggregate Types主要是想跟C++的Aggregate Class區分開。IR的Aggregate Types包括數組和結構體。
2.1 數組
語法
[<elementnumber> x <elementtype>]
概述
跟C++的模板類template<class T, std::size_t N > class array類似,數組元素在內存中是連續分布的,元素個數必須是編譯器常量,未被提供初始值的元素會被零初始化,只是下標的使用方式有點區別。
Example
@array = global [17 x i8] ; 17個i8都是0 %array2 = alloca [17 x i8] [i8 1, i8 2] ; 前兩個是1、2,其余是0 %array3 = alloca [3 x [4 x i32]] ; 3行4列的i32數組 @array4 = global [2 x [3 x [4 x i16]]] ; 2x3x4的i16數組
2.2 結構體
語法
%T1 = type { <type list> } ; Identified normal struct type %T2 = type <{ <type list> }> ; Identified packed struct type
概述
與C語言中的struct相同,不過IR提供了兩種版本,normal版元素之間是由padding的,packed版沒有。
Example
%struct1 = type { i32, i32, i32 } ; 一個i32的triple %struct2 = type { float, i32 (i32) * } ; 一個pair,第一個元素是float,第二個元素是一個函數指針,該函數有一個i32的形參,返回一個i32 %struct3 = type <{ i8, i32 }> ; 一個packed的pair,大小為5字節
2.3 getelementptr指令(GEP)
我們可以使用 getelementptr指令來獲得指向數組的元素和指向結構體成員的指針。
語法
<result> = getelementptr <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}* <result> = getelementptr inbounds <ty>, <ty>* <ptrval>{, [inrange] <ty> <idx>}*
概述
第一個ty是第一個索引使用的基本類型,第二個ty表示其后的基址ptrval的類型,inbounds和inrange關鍵字的含義這里不講,有興趣可以去LangRef查閱。 <ty> <idx>是第一組索引的類型和值,<ty> <idx>可以出現多次,其后出現的就是第二組、第三組等等索引的類型和值。要注意索引的類型和索引使用的基本類型是不一樣的,索引的類型一般為i32或i64,而索引使用的基本類型確定的是增加索引值時指針的偏移量。
GEP的幾個要點
理解第一個索引
- 第一個索引不會改變返回的指針的類型,也就是說
ptrval前面的<ty>*對應什么類型,返回就是什么類型 - 第一個索引的偏移量的是由第一個索引的值和第一個
ty指定的基本類型共同確定的。
下面看個例子
上圖中第一個索引所使用的基本類型是[6 x i8],值是1,所以返回的值相對基址@a_gv前進了6個字節。由於只有一個索引,所以返回的指針也是[6 x i8]*類型。
理解后面的索引
- 后面的索引是在 Aggregate Types內進行索引
- 每增加一個索引,就會使得該索引使用的基本類型和返回的指針的類型去掉一層
下面看個例子
我們看%elem_ptr = getelementptr [6 x i8], [6 x i8]* @a_gv, i32 0, i32 0這一句,第一個索引值是0,使用的基本類型[6 x i8], 因此其使返回的指針先前進0 x 6 個字節,也就是不前進,第二個索引的值是1,使用的基本類型就是i8([6 x i8]去掉左邊的6),因此其使返回的指針前進一個字節,返回的指針類型為i8*([6 x i8]*去掉左邊的6)。
GEP如何作用於結構體
只有一個索引情況下,GEP作用於結構體與作用於數組的規則相同,%new_ptr = getelementptr %MyStruct*, %MyStruct* @a_gv, i32 1使得%new_ptr相對@a_gv偏移一個結構體%MyStruct的大小。
在有兩個索引的情況下,第二個索引對返回指針的影響跟結構體的成員類型有關。譬如說在上圖中,第二個索引值是1,那么返回的指針就會偏移到第二個成員,也就是偏移1個字節,由於第二個成員是i32類型,因此返回的指針是i32*。
如果結構體的本身也有Aggregate Type的成員,就會出現超過兩個索引的情況。第三個索引將會進入這個Aggregate Type成員進行索引。譬如說上圖中的第二個索引是2,指針先指向第三個成員,第三個成員是個數組。再看第三個索引是0,因此指針就指向該成員的第一個元素,指針類型也變成了i32*。
注:GEP作用於結構體時,其索引一定要是常量。GEP指令只是返回一個偏移后的指針,並沒有訪問內存。
