数据表示

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)、privateinternal

寄存器内的数据和栈上的数据

大多数对数据的操作,如加减乘除、比大小等,都需要操作的是寄存器内的数据。那么,我们为什么需要把数据放在栈上呢?主要有两个原因:

寄存器

LLVM IR引入了虚拟寄存器的概念。在LLVM IR中,一个函数的局部变量可以是寄存器或者栈上的变量。对于寄存器而言,我们只需要像普通的赋值语句一样操作,但需要注意名字必须以%开头:

%local_variable = add i32, 1, 2

粗略地理解LLVM IR对寄存器的使用:

当不需要操作地址并且寄存器数量足够时,我们可以直接使用寄存器。而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

如果要操作这些值,必须使用loadstore这两个命令。如果我们要获取@global_variable的值,就需要

%1 = load i32, i32* @global_variable

这个指令的意思是,把一个i32*类型的指针@global_variablei32类型的值赋给虚拟寄存器%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而降低性能。