简单了解LLVM IR基本语法

可以转载,请注明出处!

以下内容是参照官方文档,对一些代码样例做出的解释,想要彻底掌握IR的语法规则,还是需要仔细熟读IR的官方文档。这里只是对IR的入门介绍,等上道之后,建议再去看官方文档,因为工作原因,后面我可能会按照自己的结构划分对官网上的主要内容做一个介绍,搞一个专栏:

1.基本语法介绍

用vim编辑一个c程序代码:

#include <stdio.h>
 
int main()
{
   int a = 10;
   int b = 11;
   return a + b;
}

将c的源码转为LLVM IR,代码如下:

; ModuleID = 'test3.c'
source_filename = "test3.c"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  %b = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 10, i32* %a, align 4
  store i32 11, i32* %b, align 4
  %0 = load i32, i32* %a, align 4
  %1 = load i32, i32* %b, align 4
  %add = add nsw i32 %0, %1
  ret i32 %add
}

attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "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"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 6.0.0 (tags/RELEASE_600/final)"}

根据官方文档的描述:
注解是以 ; 分隔且直到当前行的结尾,所以; Function Attrs: noinline nounwind optnone uwtable这一行是注解;
@代表全局标识符(函数,全局变量);
%代表局部标识符(寄存器名称也就是局部变量,类型)。
所以在llvm IR看来,int main这个函数,或者说他的函数返回值是个全局变量,其内部的a 和b是局部变量。

define i32 @main() #0 {
entry:
...
ret i32 %add
}

上面IR是定义一个函数main,函数的返回值类型是i32;#0是后面attributes 属性组中的属性,暂时先不用管;每个函数的定义都会包含一个基本块(BasicBlock),entry是基本块的开始,ret i32 %add是返回类型为i32,名称为 %add的变量中存放的函数的值,也就是基本块结束;
i32:32位的整数,对应c中的int类型,i后面跟几,这个整数就会占几位(bit),i32的话就是32位,4个字节;i后面的数字可以随意写,可以得知这其实是在设置整数位的长度。

  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  %b = alloca i32, align 4

alloca指令的官方解释大致意思是:用于分配内存堆栈给当前执行的函数,当这个函数返回其调用者时自动释放。这里就是给%retval变量分配一个4byte的内存,待变量不再使用后,将内存释放,有点c语言中声明一个变量的意思,也有点像c中malloc。
align 4:在官方文档中没找到align的解释,我对他的理解是“对齐方式”:若一个结构中含有两个int,一个char,则他应该占用4*3=12字节,虽然char本身只占用一个字节的空间,但由于要向4“对齐”所以即便是数据没有4个字节,也要为其分配4个字节。所以这里的对齐方式为4个字节。

  store i32 0, i32* %retval, align 4
  store i32 10, i32* %a, align 4
  store i32 11, i32* %b, align 4

store指令官方的解释大致是,将数据写入到指定的内存中。所以这里很好理解,将i32类型的整数10存放到变量(寄存器名称)%a对应的内存中,对齐方式是4byte。

  %0 = load i32, i32* %a, align 4
  %1 = load i32, i32* %b, align 4

load指令官方解释大致意思是,读取指定内存中的数据。这里的意思是读取变量%a对应的内存中的数据,将其存放到类型为i32的零时变量%0中,%0的对齐方式为4byte。load后面紧跟的类型是有限制的,必须为frist class type,这个后面熟悉了可以看官方文档。

  %add = add nsw i32 %0, %1

add指令是一个二元运算符,返回它对应的两个操作数的和,操作数也是有要求的,必须为整数或整数值向量,且两个操作数的类型必须要相同。有一个fadd指令,也是求两个操作数的和,只不过对操作数的限制是必须为浮点数或者浮点值向量,且操作数类型相同。add加,sub减,mul乘,div除,rem取余,官网对这一块列举的很详细,也很全面,后面对代码熟悉了可以直接看文档。

2.if语句介绍

c程序代码:

#include <stdio.h>
 
int main()
{
   int a = 10;
   if(a%2 == 0)
	   return 0;
   else 
	   return 1;
}

LLVM IR代码:

define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 10, i32* %a, align 4
  %0 = load i32, i32* %a, align 4
  %rem = srem i32 %0, 2
  %cmp = icmp eq i32 %rem, 0
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  store i32 0, i32* %retval, align 4
  br label %return

if.else:                                          ; preds = %entry
  store i32 1, i32* %retval, align 4
  br label %return

return:                                           ; preds = %if.else, %if.then
  %1 = load i32, i32* %retval, align 4
  ret i32 %1
}

上面代码出现的新的指令主要有三个,icmp、br、label。srem不算新的指令,上一个例子中已经说过了,取余运算。

  %cmp = icmp eq i32 %rem, 0

icmp指令,根据比较规则,比较两个操作数,将比较的结果以布尔值或者布尔值向量(vector of boolean values,暂且就这么叫吧,也不知对不对)返回,且对于操作数的限定是操作数为整数或整数值向量、指针或指针向量。在这里,eq是比较规则,%rem和0是操作数,i32是操作数类型,比较%rem与0的值是否相等,将比较的结果存放到%cmp中。

  br i1 %cmp, label %if.then, label %if.else

br指令有两种形式,分别对应于条件分支和无条件分支。该指令的条件分支在形式上接受一个“i1”值和两个“label”值,用于将控制流传输到当前函数中的不同基本块,上面这条指令是条件分支,有点像c中的三目条件运算符< expression ?Statement :statement>;无条件分支的话就是不用判断,直接跳转到指定的分支,有点像c中goto,比如说这个就是无条件分支br label %return。上面指令的意思是,i1类型的变量%cmp的值如果为真,执行if.then,否则执行if.else。
官网对label的划分与解释是,label是类型系统(type system)中的第一类类型(first class type),所以label并不是一条指令,First class type应该是IR中最重要的类型了,所有的指令产生的值都是first class type值。我这里把label理解成一个代码标签,作为label %if.then这条分支的入口。

总结一下if条件语句:

  • 求出if语句表达式的两个操作数的值(也有可能是一个eg:a > 0);
  • icmp指令开始比较,会产生一个布尔结果值;
  • br指令的条件分支利用上一步产生的值,跳转到相对应的分支入口;
  • 分支执行完再用br指令的无条件分支跳到if的结束分支。

3.While语句介绍

c程序代码:

#include <stdio.h>
 
int main()
{
   int a = 0, b = 1;
   while(a < 5)
   {
	   a++;
	   b *= a;
   }
   return b;
}

LLVM IR代码:

define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %a = alloca i32, align 4
  %b = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 0, i32* %a, align 4
  store i32 1, i32* %b, align 4
  br label %while.cond

while.cond:                                       ; preds = %while.body, %entry
  %0 = load i32, i32* %a, align 4
  %cmp = icmp slt i32 %0, 5
  br i1 %cmp, label %while.body, label %while.end

while.body:                                       ; preds = %while.cond
  %1 = load i32, i32* %a, align 4
  %inc = add nsw i32 %1, 1
  store i32 %inc, i32* %a, align 4
  %2 = load i32, i32* %a, align 4
  %3 = load i32, i32* %b, align 4
  %mul = mul nsw i32 %3, %2
  store i32 %mul, i32* %b, align 4
  br label %while.cond

while.end:                                        ; preds = %while.cond
  %4 = load i32, i32* %b, align 4
  ret i32 %4
}

对比if语句可以发现,while中几乎没有新的指令出现,所以说所谓的while循环,也就是“跳转+分支”这一结构。
While的运行流程是:首先跳到while.cond: 相关变量得到初始值后判断是否满足继续循环条件,若满足,就转到while.body: 进行循环实际操作,一次实际操作运行完后再次跳到while.cond:进行条件判断,如此循环~;若否,则直接跳到 while.end: 终止循环;

4.switch语句

对应C的伪代码:

int main (){
   char grade = 'B';
   int score;
   switch(grade)
   {
   case 'A' :
      score = 4;
      break;
   case 'B' :
	  score = 3;
	  break;
   case 'C' :
       score = 2;
      break;
   case 'D' :
       score = 1;
      break;
   default :
       score = 0;
   }
   printf("your score: %d\n", score );
   return 0;
}

IR代码:

@.str = private unnamed_addr constant [16 x i8] c"your score: %d\0A\00", align 1

define i32 @main(){
entry:
  %grade = alloca i8, align 1
  %score = alloca i32, align 4
  store i8 66, i8* %grade, align 1
  %0 = load i8, i8* %grade, align 1
  %conv = sext i8 %0 to i32
  switch i32 %conv, label %sw.default [
    i32 65, label %sw.a
    i32 66, label %sw.b
    i32 67, label %sw.c
    i32 68, label %sw.d
  ]

sw.a: 
  store i32 4, i32* %score, align 4
  br label %sw.end

sw.b: 
  store i32 3, i32* %score, align 4
  br label %sw.end

sw.c: 
  store i32 2, i32* %score, align 4
  br label %sw.end

sw.d:
  store i32 1, i32* %score, align 4
  br label %sw.end

sw.default: 
  store i32 0, i32* %score, align 4
  br label %sw.end

sw.end:
  %1 = load i32, i32* %score, align 4
  %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str, i32 0, i32 0), i32 %1)
  ret i32 0
}

declare i32 @printf(i8*, ...)

这里新出来了switch 指令,switch指令是一个终端指令,同br、ret一样,用在一个基本快的结束。也没啥难度,就是选一个与%conv值相等的基本快标签跳进去。

5.对指针的操作

对应C的伪代码:

int main(){
	int i = 10;
	int* pi = &i;
	printf("i的值为:%d",i);
	printf("*pi的值为:%d",*pi);
	printf("&i的地址值为:",%d);
	printf("pi的地址值为:",%d);
}

IR代码:

@.str = private unnamed_addr constant [16 x i8] c"i\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.1 = private unnamed_addr constant [18 x i8] c"*pi\E7\9A\84\E5\80\BC\E4\B8\BA\EF\BC\9A%d\00", align 1
@.str.2 = private unnamed_addr constant [23 x i8] c"&i\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1
@.str.3 = private unnamed_addr constant [23 x i8] c"pi\E7\9A\84\E5\9C\B0\E5\9D\80\E5\80\BC\E4\B8\BA\EF\BC\9A%p\00", align 1

define i32 @main(){
entry:
  %i = alloca i32, align 4
  %pi = alloca i32*, align 8
  store i32 10, i32* %i, align 4
  store i32* %i, i32** %pi, align 8
  
  %0 = load i32, i32* %i, align 4
  %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([16 x i8], [16 x i8]* @.str, i32 0, i32 0), i32 %0)
  %1 = load i32, i32* %i, align 4
  %call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([18 x i8], [18 x i8]* @.str.1, i32 0, i32 0), i32 %1)
  
  %call2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.2, i32 0, i32 0), i32* %i)
  %2 = load i32*, i32** %pi, align 8
  %call3 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str.3, i32 0, i32 0), i32* %2)
  ret i32 0
}

declare i32 @printf(i8*, ...)

对指针的操作就是指针的指针,开辟一块指针类型的内存,里面放个指针%pi = alloca i32*, align 8,如果你是从头看下来的,相信这里不会有啥问题。需要注意的是alloca 指令产生的是一个指针,所以才有上一句所说的指针的指针。这些内容属于C的知识,如果不理解的话百度一下,很简单的。

6.对数组的操作

对应C的伪代码:

int main(){
    char str[30];
    char c;
    int i;
    for(c=65,i=0; c<=90; c++,i++){
        str[i] = c;
    }
    printf("%s\n", str);
    return 0;
}

IR代码:

@.str = private unnamed_addr constant [4 x i8] c"%s\0A\00", align 1

define i32 @main() {
entry:
  %str = alloca [30 x i8], align 16
  %c = alloca i8, align 1
  %i = alloca i32, align 4
  store i8 65, i8* %c, align 1
  store i32 0, i32* %i, align 4
  br label %for.cond

for.cond:
  %0 = load i8, i8* %c, align 1
  %sext = sext i8 %0 to i32
  %cmp = icmp sle i32 %sext, 90
  br i1 %cmp, label %for.body, label %for.end

for.body:
  %1 = load i8, i8* %c, align 1
  %2 = load i32, i32* %i, align 4
  %array = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 %2
  store i8 %1, i8* %array, align 1
  %add = add i8 %1, 1
  store i8 %add, i8* %c, align 1
  %3 = load i32, i32* %i, align 4
  %add2 = add nsw i32 %3, 1
  store i32 %add2, i32* %i, align 4
  br label %for.cond

for.end:
  %arraydecay = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 0
  %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i32 0, i32 0), i8* %arraydecay)
  ret i32 0
}

declare i32 @printf(i8*, ...)

数组新增了两个不同的地方:%str = alloca [30 x i8], align 16%arraydecay = getelementptr inbounds [30 x i8], [30 x i8]* %str, i32 0, i32 0
第一条分配内存的指令alloca [30 x i8]同first class类型一样理解,就是分配了30个i8类型的连续空间。
第二条是一个很重要的指令,首次看可能不是特别好理解,直接看指令介绍,的“内存访问和寻址”的“getelementptr指令”介绍。

7.对结构体的操作

对应C的伪代码:

struct Grade{
	int number;
}
	
struct Stu{
	char *name;
	int age;
	char group;
	float score;
	struct Grade grade1;
};
	
int main(){
    struct Stu stu1;
    stu1.name = "Tom";
    stu1.age = 18;
    stu1.group = 'A';
    stu1.score = 136.5;
	stu1.grade1.number = 4;
	
    printf("%s,%d,%c,%.1f, %d\n", stu1.name,stu1.age, stu1.group, stu1.score, stu1.grade1.number);
    return 0;
}

IR代码:

%struct.grade = type{ i32 }
%struct.Stu = type { i8*, i32, i8, float, %struct.grade}

@str.name = private unnamed_addr constant [4 x i8] c"Tom\00", align 1
@str.print = private unnamed_addr constant [25 x i8] c"%s\EF\BC\8C%d\EF\BC\8C%c\EF\BC\8C%.1f, %d\0A\00", align 1

define i32 @main(){
entry:
  %stu1 = alloca %struct.Stu, align 8

  %name = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 0
  store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @str.name, i32 0, i32 0), i8** %name, align 8
  %age = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 1
  store i32 18, i32* %age, align 8
  %group = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 2
  store i8 65, i8* %group, align 4
  %score = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 3
  store float 136.5, float* %score, align 8
  %grade = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 4, i32 0
  store i32 4, i32* %grade, align 4
  %0 = load i8*, i8** %name, align 8
  %1 = load i32, i32* %age, align 8
  %2 = load i8, i8* %group, align 4
  %sext = sext i8 %2 to i32
  %3 = load float, float* %score, align 8
  %4 = load i32, i32* %grade, align 4
  %fpext = fpext float %3 to double
  %call = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([25 x i8], [25 x i8]* @str.print, i32 0, i32 0), i8* %0, i32 %1, i32 %sext, double %fpext, i32 %4)
  ret i32 0
}

declare i32 @printf(i8*, ...)

IR中对于结构体的使用同C中很相似,都是先声明一个类型,然后在使用这个类型利用alloca指令开辟内存。第1节基本语法介绍中也说过,%代表局部标识符(寄存器名称也就是局部变量,类型),其中类型就是这里的结构体。

%struct.grade = type{ i32 }
%struct.Stu = type { i8*, i32, i8, float, %struct.grade}

结构体这一块需要注意的是这一条指令,总共有三个索引,%grade = getelementptr inbounds %struct.Stu, %struct.Stu* %stu1, i32 0, i32 4, i32 0,还是直接看指令介绍,的“内存访问和寻址”的“getelementptr指令”介绍,里面都有。

8.引用内置函数

对应C的伪代码:

int main (){
   printf("值 8.0 ^ 3 = %lf\n", pow(8.0, 3));
   printf("值 3.05 ^ 1.98 = %lf", llvm.pow.f32(3.05, 1.98));
   return 0;
}

IR代码:

@.str = private unnamed_addr constant [19 x i8] c"\E5\80\BC 8.0 ^ 3 = %lf\0A\00", align 1
@.str.1 = private unnamed_addr constant [22 x i8] c"\E5\80\BC 3.05 ^ 1.98 = %lf\00", align 1

declare i32 @printf(i8*, ...)
declare double @pow(double, double)
declare double  @llvm.pow.f32(double, double)

define i32 @main(){
entry:
  %pow1 = call double @pow(double 8.0, double 3.0)
  call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([19 x i8], [19 x i8]* @.str, i32 0, i32 0), double %pow1)
  %pow2 = call double @llvm.pow.f32(double 3.05, double 1.98)
  call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([22 x i8], [22 x i8]* @.str.1, i32 0, i32 0), double %pow2)
  ret i32 0
}

9.引用外部函数

对应C的伪代码:

int func(int a) {
 a = a*2;
 return a;
}

----------------------------------------

#include<stdio.h>

extern int func(int a);

int main() {
 int num = 5;
 num = func(num);
 printf("number is %d\n", num);
 return num;
}

IR代码:

define i32 @func(i32 %a){
entry:
  %a.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  %0 = load i32, i32* %a.addr, align 4
  %mul = mul nsw i32 %0, 2
  store i32 %mul, i32* %a.addr, align 4
  %1 = load i32, i32* %a.addr, align 4
  ret i32 %1
}

-----------------------------------------------------

@.str = private unnamed_addr constant [14 x i8] c"number is %d\0A\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %num = alloca i32, align 4
  store i32 0, i32* %retval, align 4
  store i32 5, i32* %num, align 4
  %0 = load i32, i32* %num, align 4
  %call = call i32 @func(i32 %0)
  store i32 %call, i32* %num, align 4
  %1 = load i32, i32* %num, align 4
  %call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %1)
  %2 = load i32, i32* %num, align 4
  ret i32 %2
}

declare i32 @func(i32)

declare i32 @printf(i8*, ...)

链接后的IR:

@.str = private unnamed_addr constant [14 x i8] c"number is %d\0A\00", align 1

define i32 @func(i32 %a){
entry:
  %a.addr = alloca i32, align 4
  store i32 %a, i32* %a.addr, align 4
  %0 = load i32, i32* %a.addr, align 4
  %mul = mul nsw i32 %0, 2
  store i32 %mul, i32* %a.addr, align 4
  %1 = load i32, i32* %a.addr, align 4
  ret i32 %1
}

define i32 @main(){
entry:
  %num = alloca i32, align 4
  store i32 5, i32* %num, align 4
  %0 = load i32, i32* %num, align 4
  %call = call i32 @func(i32 %0)
  store i32 %call, i32* %num, align 4
  %1 = load i32, i32* %num, align 4
  %call1 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str, i32 0, i32 0), i32 %1)
  %2 = load i32, i32* %num, align 4
  ret i32 %2
}

declare i32 @printf(i8*, ...)
  • 44
    点赞
  • 105
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yelvens

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值