数据表示
llvm-ir-tutorial/LLVM IR入门指南(3)——数据表示.md at master · Evian-Zhang/llvm-ir-tutorial · GitHub
汇编语言的两个核心
- 操作什么数据
- 怎么操作
这篇内容的核心是如何表示一个数据
; 数据区数据
%global_variable = global i32 0
; 寄存器数据
%local_variable = add i32, 1, 2
; 栈数据
%stack_variable = alloca i32
汇编层次的数据表示
+------------------------------+
| stack_data |
| heap_pointer | <------------- stack
+------------------------------+
| |
| | <------------- available memory space
| |
+------------------------------+
| data pointed by heap_pointer | <------------- heap
+------------------------------|
| global_data | <------------- .DATA section
+------------------------------+
由于堆中的数据不能独立存在,一定需要一个位于其他位置的引用。所以内存中的数据有两类,再加上寄存器中的数据,一共有三种地方存储数据
- 寄存器中的数据
- 栈上的数据
- 数据区里的数据
LLVM IR中的数据表示
LLVM IR中,我们需要表示的数据也是以上三种。
数据区里的数据
数据区指可执行文件中的.DATA分区。不同的平台有不同的特性,比如ELF有.rodata分区存储只读数据
LLVM的策略是,让我们尽可能细致地定义一个全局变量,比如说注明其是否只读等,然后依据各个平台,如果平台的可执行程序格式支持相应的特性,就可以进行优化。
; 定义了一个i32全局变量@global_variable, 初始化为0
@global_variable = global i32 0
; 定义了一个i32的全局常量,初始化为0
@global_constant = constant i32 0
符号表
我们有如下LLVM IR
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"
@global_variable = global i32 0
define i32 @main() {
ret i32 0
}
编译成可执行文件
clang global_variable_test.ll -o global_variable_test
用nm
命令查看符号表, 可以看到global_variable
0000000000404000 d _GLOBAL_OFFSET_TABLE_
000000000040402c B global_variable
w __gmon_start__
0000000000402004 r __GNU_EH_FRAME_HDR
0000000000401000 T _init
链接类型
对于链接类型,我们常用的主要有什么都不加(默认为external
)、private
和internal
。
private
--> 变量名称不会出现再符号表中internal
--> 变量以局部符号的身份出现(全局变量的局部符号,可以理解成C中的static
关键词)
寄存器内的数据和栈上的数据
大多数对数据的操作,如加减乘除、比大小等,都需要操作的是寄存器内的数据。那么,我们为什么需要把数据放在栈上呢?主要有两个原因:
- 寄存器数量不够 (register spilling)
- 需要操作内存地址
寄存器
LLVM IR引入了虚拟寄存器的概念。在LLVM IR中,一个函数的局部变量可以是寄存器或者栈上的变量。对于寄存器而言,我们只需要像普通的赋值语句一样操作,但需要注意名字必须以%
开头:
%local_variable = add i32, 1, 2
粗略地理解LLVM IR对寄存器的使用:
- 当所需寄存器数量较少时,直接使用caller-saved register,即不需要保留的寄存器
- 当caller-saved register不够时,将callee-saved register原本的值压栈,将callee-saved register
- 当寄存器用光以后,就把多的虚拟寄存器的值压栈
栈
当不需要操作地址并且寄存器数量足够时,我们可以直接使用寄存器。而LLVM IR的策略保证了我们可以使用无数的虚拟寄存器。那么,在需要操作地址以及需要可变变量(之后会提到为什么)时,我们就需要使用栈。
LLVM IR对栈的使用十分简单,直接使用alloca
指令即可。如:
%local_variable = alloca i32
就可以声明一个在栈上的变量了。
全局变量和栈上变量皆指针
全局变量和栈上变量都是指向他们那个类型的指针
如果上述@global_variable
和%local_variable
都是i32*
类型的指针,指向i32所处的内存区域
所以,我们不能这样:
%1 = add i32 1, @global_variable ; wrong!
因为@global_variable
只是一个指针。
load & store
如果要操作这些值,必须使用load
和store
这两个命令。如果我们要获取@global_variable
的值,就需要
%1 = load i32, i32* @global_variable
这个指令的意思是,把一个i32*
类型的指针@global_variable
的i32
类型的值赋给虚拟寄存器%1
,然后我们就能
%2 = add i32 1, %1
类似地,如果我们要将值存储到全局变量或栈上变量里,会需要store
命令:
store i32 1, i32* @global_variable
这个代表将i32
类型的值1
赋给i32*
类型的全局变量@global_variable
所指的内存区域中。
SSA
LLVM IR是一个严格遵守SSA(Static Single Assignment)策略的语言。SSA的要求很简单:每个变量只被赋值一次。也就是说,你不能
%1 = add i32 1, 2
%1 = add i32 3, 4
对%1
同时赋值两次是不被允许的。
那么,我们应该怎样实现可变变量呢?很简单,把可变变量放到全局变量或者栈内变量里,虚拟寄存器只存储不可变的变量。比如说,我想实现上面的功能,把两次运算结果储存到同一个变量内:
%stack_variable = alloca i32
%1 = add i32 1, 2
store i32 %1, i32* %stack_variable
%2 = add i32 3, 4
store i32 %2, i32* %stack_variable
我们同样遵守了SSA,而且也满足了可变变量的需求。此外,虽然LLVM IR上看上去很复杂,LLVM后端也会帮我们优化到比较简单的形式,不会因为SSA而降低性能。