IR基本组成部分
IR主要有以下四部分组成:
Module
Function
BasicBlock
Instruction
他们之间关系:(用图会描述的更加详细,稍后在贴上)
Module -> Function ->BasicBlock ->Instruction
IR中最为复杂的部分就是Instruction,IR中指令繁多,每个Instruction是做什么用、示例代码等在文档上都有详细解释,需要可以查阅文档!
IR整体结构:
IR中的指令介绍:LLVM Instruction
IR语法之变/常量,数组
这部分代码太多,暂时先不贴
不同数据类型运算:
#include <iostream>
double dou() {
double a,b;
return a+b;
}
int main() {
int c,d;
return c+d;
}
上述代码转换为IR后内容如下:
; ModuleID = 'haoyu.cpp'
source_filename = "haoyu.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline nounwind optnone ssp uwtable
define double @_Z3douv() #0 {
%1 = alloca double, align 8
%2 = alloca double, align 8
%3 = load double, double* %1, align 8
%4 = load double, double* %2, align 8
%5 = fadd double %3, %4 //add前加f,表示浮点数相加
ret double %5
}
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #1 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
%4 = load i32, i32* %2, align 4
%5 = load i32, i32* %3, align 4 //将%3变量的指向的值加载到%5中
%6 = add nsw i32 %4, %5
ret i32 %6
}
//省略一部分不相关内容
关于上述IR中相关命令的解释:
alloca
是开辟内存空间指令
load
是加载指令,即读出内容
store
是写入指令。
这之后是运算命令:
Add
是加
Sub
是减
Mul
是乘
Div
是除
Rems
是求余
运算命令前头:
加
f
的是浮点运算;加
u
的是返回无符号整型值(unsigned integer);加
s
返回的是有符号的;
ret i32 %6
表示返回加的结果,如果是void
型的函数,就ret void
;
基本条件语句:
int main() {
int a,b,c;
a=78;
b=66;
c=33;
if( a > b) {
c=1;
}
else {
c=2;
}
//没有添加返回值,llvm会默认添加一个返回值,默认值为0
}
; ModuleID = 'haoyu.cpp'
source_filename = "haoyu.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4 //返回值
%2 = alloca i32, align 4 //a
%3 = alloca i32, align 4 //b
%4 = alloca i32, align 4 //c
store i32 0, i32* %1, align 4 //赋值操作
store i32 78, i32* %2, align 4
store i32 66, i32* %3, align 4
store i32 33, i32* %4, align 4
%5 = load i32, i32* %2, align 4
%6 = load i32, i32* %3, align 4
%7 = icmp sgt i32 %5, %6 //icmp
br i1 %7, label %8, label %9 //br命令
; <label>:8: ; preds = %0
store i32 1, i32* %4, align 4
br label %10 //无条件跳转指令
; <label>:9: ; preds = %0
store i32 2, i32* %4, align 4
br label %10 //无条件跳转指令
; <label>:10: ; preds = %9, %8
%11 = load i32, i32* %1, align 4
ret i32 %11
}
基本条件语句多了两个指令+一个数据类型
icmp
:比较命令(不同类型会有不同比较命令:icmp、fcmp)icmp Instruction
语法规则如下:
<result> = icmp <cond> <ty> <op1>, <op2> ; yields i1 or <N x i1>:result
//第一参数是:关键字 ,表示比较的规则 ex:大于、小于、大于等于.具体可选择可参考官方文档!
//第二个参数是:类型。表示后面两个值的类型。
//后面两个参数为要做比较的两个值
br
:跳转指令
语法规则:
br i1 <cond>, label <iftrue>, label <iffalse> //有条件跳转
br label <dest> ; Unconditional branch //无条件跳转
可以看到br
跳转包括无条件跳转
和有条件跳转
。即br i1
和br
。
cond
表示跳转条件:
第一个lable表示如果条件为true
要跳转到哪一个基本块的标签(用来标记该基本块的入口)
第二个label表示如果比较条件false
要跳转的基本块。
以上面的示例为例:
br i1 %7, label %8, label %9
如果局部变量%7的值为真,则跳转到标签为label %8的基本块执行,否则跳转到标签为label %9的基本块执行。
至于无条件跳转br指令就很容易理解了,直接跳转至标签为dest的基本块执行。
br label %10
label
:标签
严格的讲它也是一种数据类型(type),但它可以标识入口,相当于代码标签;
我们再来看一个if-else if-else 结构的IR:
int main() {
int i = 0;
int b = 0;
if(i>0) {
b = 5;
}
else if ( i == 0 ) {
b = 10;
}
else if( i < 0 ) {
b = 200;
}
return 0;
}
生成IR如下:
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 0, i32* %2, align 4
store i32 0, i32* %3, align 4
%4 = load i32, i32* %2, align 4
%5 = icmp sgt i32 %4, 0
br i1 %5, label %6, label %7
; <label>:6: ; preds = %0
store i32 5, i32* %3, align 4
br label %17
; <label>:7: ; preds = %0
%8 = load i32, i32* %2, align 4
%9 = icmp eq i32 %8, 0 //做一次是否相等的判断
br i1 %9, label %10, label %11
; <label>:10: ; preds = %7
store i32 10, i32* %3, align 4
br label %16
; <label>:11: ; preds = %7
%12 = load i32, i32* %2, align 4
%13 = icmp slt i32 %12, 0 //做一次小于判断,判断是否小于0
br i1 %13, label %14, label %15
; <label>:14: ; preds = %11
store i32 200, i32* %3, align 4
br label %15
; <label>:15: ; preds = %14, %11
br label %16
; <label>:16: ; preds = %15, %10
br label %17
; <label>:17: ; preds = %16, %6
ret i32 0
}
我们可以看到还是运用到了icmp、br、label 命令和标签无其他特殊内容
While循环
int main() {
int a = 10,i = 20;
while (i < 10) {
i=i+1;
a=a*2;
}
return 0;
}
生成的IR如下:
; ModuleID = 'haoyu.cpp'
source_filename = "haoyu.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4 //a
%3 = alloca i32, align 4 //i
store i32 0, i32* %1, align 4
store i32 10, i32* %2, align 4
store i32 20, i32* %3, align 4
br label %4
; <label>:4: ; preds = %7, %0 这里是一个if判断
%5 = load i32, i32* %3, align 4
%6 = icmp slt i32 %5, 10 //判断i是否小于10
br i1 %6, label %7, label %12
; <label>:7: ; preds = %4 这里是一个普通的分支循环
%8 = load i32, i32* %3, align 4
%9 = add nsw i32 %8, 1 // i + 1 操作
store i32 %9, i32* %3, align 4
%10 = load i32, i32* %2, align 4
%11 = mul nsw i32 %10, 2 //a * 2 操作
store i32 %11, i32* %2, align 4
br label %4 //跳转到while开始的地方,构成循环调用
; <label>:12: ; preds = %4
ret i32 0
}
可以看到相较于if,while在IR中的实现几乎没有用到新的指令,可以说,所谓的循环语句while == if + 分支循环;
for循环
int main() {
int a = 10, i = 20;
for ( i = 0; i < 10; i++ ) {
a = a *2;
}
return 0;
}
生成的IR如下:
; ModuleID = 'haoyu.cpp'
source_filename = "haoyu.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 10, i32* %2, align 4 //a
store i32 20, i32* %3, align 4 //i
store i32 0, i32* %3, align 4
br label %4
; <label>:4: ; preds = %10, %0
%5 = load i32, i32* %3, align 4
%6 = icmp slt i32 %5, 10 //判断i是否小于10
br i1 %6, label %7, label %13
; <label>:7: ; preds = %4
%8 = load i32, i32* %2, align 4
%9 = mul nsw i32 %8, 2 // a *2 操作
store i32 %9, i32* %2, align 4 //将* 2后的值重新赋值到a上
br label %10
; <label>:10: ; preds = %7
%11 = load i32, i32* %3, align 4
%12 = add nsw i32 %11, 1 //对 i 进行 ++ 操作
store i32 %12, i32* %3, align 4
br label %4 // i++完成后回到上一步
; <label>:13: ; preds = %4
ret i32 0
}
可以看到for循环同样也没有什么新的指令出现;它一样是条件判断+分支循环,只不过比while更高级的地方在于:它把用于判断是否继续循环的条件单独放进一个basicblock中,也因此比while循环多了一个basicBlock。
switch操作:
int main() {
int a = 5, b = 20;
switch(a)
{
case 0:
{
b = 1;
}
case 1:
{
b = 2;
}
case 5:
{
b = 3;
}
}
return 0;
}
生成的IR如下:
; ModuleID = 'haoyu.cpp'
source_filename = "haoyu.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
; Function Attrs: noinline norecurse nounwind optnone ssp uwtable
define i32 @main() #0 {
%1 = alloca i32, align 4
%2 = alloca i32, align 4
%3 = alloca i32, align 4
store i32 0, i32* %1, align 4
store i32 5, i32* %2, align 4 //a
store i32 20, i32* %3, align 4 //b
%4 = load i32, i32* %2, align 4 //读取a的值用于switch判断
switch i32 %4, label %8 [ //switch指令
i32 0, label %5
i32 1, label %6
i32 5, label %7
]
; <label>:5: ; preds = %0
store i32 1, i32* %3, align 4
br label %6 //没有添加break,执行完label 5后,会继续向下执行 label 6
; <label>:6: ; preds = %0, %5
store i32 2, i32* %3, align 4
br label %7
; <label>:7: ; preds = %0, %6
store i32 3, i32* %3, align 4
br label %8
; <label>:8: ; preds = %7, %0
ret i32 0
}
我们可以看到多了一个switch指令。
swicth
: switch Instruction
语法规则:
switch <intty> <value>, label <defaultdest> [ <intty> <val>, label <dest> ... ]
//para1:int 类型
//para2: 要匹配的值
//para3:要跳转到的标签
//[ ] 中罗列的就是固定结构的:int类型、具体值、对应的标签
跟多更具体可参考官方文档!
switch
是br
指令的加强版,可以产生多个(不止两个)程序分支;说白了跟c语言的switch
机制差不多。
另外我们可以看到switch的各个分支不是运行一个就完事了的,而是自上而下顺序依次运行的,如果你的条件变量的值触发了第N个程序分支,那么运行完第N个程序分支后switch会继续运行N+1,知道执行完所有label。
switch i32 %4, label %8 [ //switch指令
i32 0, label %5
i32 1, label %6
i32 5, label %7
]
; <label>:5: ; preds = %0
store i32 1, i32* %3, align 4
br label %6 //没有添加break,执行完label 5后,会继续向下执行 label 6
; <label>:6: ; preds = %0, %5
store i32 2, i32* %3, align 4
br label %7
//添加了break后的执行如下:
; <label>:5: ; preds = %0
store i32 1, i32* %3, align 4
br label %8 //直接跳到最后面的label 8上执行,不会继续向下执行。
综上IR中除了switch
指令外,没有什么新命令出现。是不是觉得很简单。
OC文件编译出的IR
上述都是C++代码编译的结果,那么我们OC语法编译出来的会有什么不同吗?其实本质上无太大不同,只是OC由于复杂的继承关系以及xitp0ng类库的调用,会使编译出来的结果全局、局部变量很多,另外很多隐式的函数也会出现在我们的IR中。
先来看一下生成IR的命令:
clang -fobjc-arc -emit-llvm HaoyuViewController.m -S -c -o haoyu.ll -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk
-emiit-llvm
:触发LLVM生成IR
-S
:生成刻度的汇编IR
-fobjc-arc
:指定使用ARC方式
-isysroot
:指定依赖的系统类库
部分代码示例:(文件内容太多,不全部展示)
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
%struct._objc_cache = type opaque
%struct._class_t = type { %struct._class_t*, %struct._class_t*, %struct._objc_cache*, i8* (i8*, i8*)**, %struct._class_ro_t* }
...
//默认继承的函数也会被添加到IR中
; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[UIViewController setView:]"(%0*, i8*, %2*) #0 {
%4 = alloca %0*, align 8
%5 = alloca i8*, align 8
%6 = alloca %2*, align 8
store %0* %0, %0** %4, align 8
store i8* %1, i8** %5, align 8
store %2* %2, %2** %6, align 8
%7 = load %2*, %2** %6, align 8
%8 = load %0*, %0** %4, align 8
%9 = load i64, i64* @"OBJC_IVAR_$_UIViewController._view", align 8, !invariant.load !10
%10 = bitcast %0* %8 to i8*
%11 = getelementptr inbounds i8, i8* %10, i64 %9
%12 = bitcast i8* %11 to %2**
%13 = bitcast %2** %12 to i8**
%14 = bitcast %2* %7 to i8*
call void @llvm.objc.storeStrong(i8** %13, i8* %14) #1
ret void
}
; Function Attrs: noinline optnone ssp uwtable
define internal %2* @"\01-[UIViewController viewIfLoaded]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
%8 = getelementptr inbounds i8, i8* %7, i64 %6
%9 = bitcast i8* %8 to %2**
%10 = load %2*, %2** %9, align 8
ret %2* %10
}
...
//运行时函数也会被加入带这里面来
; Function Attrs: noinline optnone ssp uwtable
define internal void @"\01-[UIViewController .cxx_destruct]"(%0*, i8*) #0 {
%3 = alloca %0*, align 8
...
}
综上基本上常见的的语法我们都覆盖到了,还有一些更为具体的细节,我们可以参考这个LLVM中文文档
可作为参考,更深入的了解LLVM。