LLVM IR 三部曲之一 --- IR语法

IR基本组成部分

IR主要有以下四部分组成:
Module
Function
BasicBlock
Instruction

他们之间关系:(用图会描述的更加详细,稍后在贴上)
Module -> Function ->BasicBlock ->Instruction

IR中最为复杂的部分就是Instruction,IR中指令繁多,每个Instruction是做什么用、示例代码等在文档上都有详细解释,需要可以查阅文档!

IR整体结构:
IR中的指令介绍:LLVM Instruction

IR语法之变/常量,数组

这部分代码太多,暂时先不贴

不同数据类型运算:

#include 

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命令

; 

基本条件语句多了两个指令+一个数据类型
icmp:比较命令(不同类型会有不同比较命令:icmp、fcmp)icmp Instruction
语法规则如下:

 = icmp   ,    ; yields i1 or :result
//第一参数是:关键字 ,表示比较的规则 ex:大于、小于、大于等于.具体可选择可参考官方文档!
//第二个参数是:类型。表示后面两个值的类型。
//后面两个参数为要做比较的两个值

br:跳转指令
语法规则:

br i1 , label , label  //有条件跳转
br label           ; Unconditional branch //无条件跳转

可以看到br跳转包括无条件跳转有条件跳转 。即br i1br
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

; 

我们可以看到还是运用到了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

; 

可以看到相较于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

; 

可以看到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
  ]

; 

我们可以看到多了一个switch指令。
swicth : switch Instruction
语法规则:

switch  , label  [  , label  ... ]
//para1:int 类型
//para2: 要匹配的值
//para3:要跳转到的标签
//[ ] 中罗列的就是固定结构的:int类型、具体值、对应的标签
跟多更具体可参考官方文档!

switchbr指令的加强版,可以产生多个(不止两个)程序分支;说白了跟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
  ]

; 

综上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。

你可能感兴趣的:(LLVM IR 三部曲之一 --- IR语法)