LLVM IR以两种格式存储在磁盘上:
1.位码(.bc文件)
2.汇编文本(.ll文件)
以sum.c源代码为例
int sum(int a, int b){
return a+b;
}
使用Clang生成位码,命令如下:
$ clang sum.c -emit-llvm -c -o sum.bc
使用Clang生成汇编文本,命令如下:
$ clang sum.c -emit-llvm -S -c -o sum.ll
$ clang -emit-llvm sum.c -S -o sum.ll
还可以汇编上述的LLVM IR汇编文本,创建相应的位码,命令如下:
$ llvm-as sum.ll -o sum.bc
相反,要从位码转换为IR汇编文本,可以使用反汇编程序:
$ llvm-dis sum.bc -o sum.ll
观察LLVM IR汇编码文件sum.ll:
; ModuleID = 'sum.c'
source_filename = "sum.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
; Function Attrs: noinline nounwind optnone uwtable
define dso_local i32 @sum(i32 %0, i32 %1) #0 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
%5 = load i32, i32* %3, align 4
%6 = load i32, i32* %4, align 4
%7 = add nsw i32 %5, %6
ret i32 %7
}
attributes #0 = { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "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" }
!llvm.module.flags = !{!0}
!llvm.ident = !{!1}
!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 10.0.0-4ubuntu1 "}
整个LLVM文件的内容(无论是汇编码还是位码)被定义为一个LLVM模块。模块是LLVM IR顶层数据结构。每个模块包含一系列函数,每个函数由包含一系列指令的一系列基本块组成。模块还包含用于支持该模型的外围实体,如全局变量、目标数据布局、外部函数原型以及数据结构声明。
LLVM局部变量与汇编语言中的寄存器类似,可以用任何以%符号开头的名称命名。因此,
%7 = add nsw i32 %5, %6
这一指令将执行两个局部变量%5和%6的加法,并将这个结果置于新的局部变量%32中。用户可以自由地给这些值命名。
通过这个简短的例子,可以看到LLVM如何表达其基本属性:
- 它使用静态单赋值(SSA)形式。该形式下每个变量都不会被重新赋值,每个变量只有唯一一条定义它的赋值语句。每次使用一个变量都可以立即回溯到负责其定义的唯一指令。使用SSA形式导致“使用定义链”(use-def链,既可以达到使用处的所有定义/赋值语句的集合)的生成变得非常简单。这个简化操作具有巨大的价值。UD链是经典优化(如常量传播和冗余表达式消除)的前提条件,如果LLVM没有使用SSA形式,则需要单独的数据流分析来计算UD链。
- 代码被组织成三地址指令。数据处理指令有两个源操作数,并将结果放在不同的目标操作数中。
- 有无穷多的寄存器。它对局部变量的最大数量没有最大限制。
target datalayout构造包含有关目标机器(target host)中描述的目标三元组(target tripple)的字节顺序和类型大小等信息。有些优化需要知道目标的特定数据布局才能完成正确的代码转化.
其中关于target datalayout的参数基本含义如下:
- target datalayout是通过符号“-”进行分隔的规格列表;
- E/e代表大小端;
- m:<mangling>:名称粉碎的类型:
e:ELF mangling;
l:GOFF mangling;
m:Mips mangling;
o:Mach-O mangling;
x:Windows x86 COFF mangling;
w:Windows COFF mangling;
a:XCOFF mangling;
- p[n]:<size>:<abi>[:<pref>][:<idx>]:n为地址空间编号,size为指针大小,abi为ABI中的指针大小,pref为地址空间n的对齐(默认等于abi),idx为地址计算的index的大小(默认等于size)。
- S<size>:栈自然对齐的的bit数。
-
P<address space>:
默认为0,表示冯诺依曼架构,数据和程序放置到同一个地址空间。
- A<address space>:alloca分配的地址空间,默认为0。
- G<address space>:全局变量放置的地址空间,默认为0。
- i<size>:<abi>[:<pref>]:size为整型的对齐,abi为ABI中的对齐,pref默认等于abi。
- v<size>:<abi>[:<pref>]:size为矢量的对齐,abi为ABI中的对齐,pref默认等于abi。
-
f<size>:<abi>[:<pref>]
:size为浮点的对齐,abi为ABI中的对齐,pref默认等于abi。
-
a:<abi>[:<pref>]
:size为聚合类型的对齐,abi为ABI中的对齐,pref默认等于abi。
上述例子中:
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
我们可以得知它具有小端字节序,采用ELF mangling进行名称粉碎,地址空间270/271的指针小为32bit,地址空间272的指针为大小为64bit,整形64bit对齐,浮点80bit对齐,自然整型为8/16/32/64bit,栈自然对齐为128bit。目标机器是装有linux-gnu的x86_64处理器。
此外,函数声明严格遵循相应的C语法:
define i32 @sum(i32 %a, i32 %b) #0 {
此函数返回i32类型的值,并具有两个i32参数:%a和%b。本地标识符始终需要%前缀,而全局标识符使用@。LLVM支持多种类型,但最重要的类型如下:
- iN形式的任意大小的整数,常见的例子是i32、i64和i128。
- 浮点类型,如32位单精度浮点数float和64位双精度浮点数double。
- 向量类型的格式位<<elements>x<elementtype>>。包含四个i32元素的向量被写为<4 x i32>.
函数声明中的#0记号映射到一组函数属性,这与C/C++函数和方法中使用的属性非常类似。
alloca指令在当前函数的堆栈帧中保留一定的空间。空间的大小由元素类型的大小决定,它遵循特定的对齐方式,例如:
%3 = alloca i32, align 4
%4 = alloca i32, align 4
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
该指令分配一个按4字节对齐的4字节堆栈元素。指向该堆栈元素的指针被存储在本地变量% 3中。
alloca指令通常用于表示本地变量。%0和%1参数通过store指令存储在堆栈地址%3和%4中。这些值通过load指令从相同的内存地址加载回来,并在%7 = add nsw i32 %5, %6加法中使用。最后返回加法结果%7。nsw标识指定此加法操作具有 “no signed wrap",这表示已知指令是无溢出的,从而允许进行一些优化。