​ 在使用LLVM作为编程语言(尤其是静态语言)后端时,生成IR是一切优化、检查和进一步编译的基础。因此,熟练使用IR Builder尤为重要。

​ 以下笔者将以几段代码为基础,简述LLVM IR Builder中的主要方法和相关类型。

准备工作

​ 为了顺利编译和运行本文中的代码,你需要:

  • 已配置好的llvm环境

  • 确保llvm的头文件位置已被正确包含

  • 确认llvm的相关库已被正确链接

    笔者使用cmake 1.3.5进行构建,CMakeLists.txt如下

    1
    2
    3
    4
    5
    6
    7
    8
    cmake_minimum_required(VERSION 3.5)
    project(TestLLVM)

    include_directories("/usr/include/llvm/") # llvm api (包含 ir builder)
    include_directories("/usr/include/llvm-c/") # llvm runtime api
    link_libraries("/usr/lib/llvm-6.0/lib/libLLVM.so") # llvm 的动态链接库

    set(CMAKE_CXX_STANDARD 14) # 使用 C++14

创建 IR Builder

​ 在正确配置了llvm后,就可以创建和使用IR Builder了,以下是一段(很长的)模板代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <cstdio>
#include "llvm/ADT/APFloat.h" // 所有相关头文件
#include "llvm/ADT/STLExtras.h"
#include "llvm/IR/BasicBlock.h"
#include "llvm/IR/Constants.h"
#include "llvm/IR/DerivedTypes.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/IR/Verifier.h"
#include "llvm/Support/raw_os_ostream.h"

static llvm::LLVMContext context;
static llvm::IRBuilder<> builder(context);
static std::unique_ptr<llvm::Module> global;

int main() {
global = std::make_unique<llvm::Module>("<test>", context);
// write whatever you want
global->print(llvm::outs(), nullptr);
}

​ 头文件多得吓人?没关系,这就是我们需要的所有头文件了。当然,很多情况下,把这些头文件用一个单独的头文件包含确实会让代码显得更简洁。

​ 在上述代码中,我们定义了三个全局变量,意义分别如下:

1
2
3
4
5
6
static llvm::LLVMContext context;
// 当前LLVM的上下文
static llvm::IRBuilder<> builder(context);
// IR Builder, 我们最想要的东西
static std::unique_ptr<llvm::Module> global;
// 我们构造的 llvm 模块,如果你不知道什么是“模块”,建议先去了解一下 llvm 的架构

​ 这三件套是我们构建llvm模块的基础。在这里,“模块”只是一个预留的空指针,我们需要真正向IR Builder“声明”这个模块:

1
2
global = std::make_unique<llvm::Module>("<test>", context);
// 创建一个模块,以`context`为上下文,名字为`<test>`

​ 如果你不熟悉C++的智能指针,上述代码可以看作(其实它们并不等价):

1
global = new llvm::Module("<test>", context);

​ 在“声明”了模块后,<test>模块便实际存在于上下文中了,我们可以用以下代码来显示这个模块的IR Code

1
2
global->print(llvm::outs(), nullptr);
// llvm::outs() 指向 stdout (标准输出流)

​ 此时,编译运行,我们会获得以下输出

1
;ModuleID = '<test>'

​ 模块是空的,所以我们只看到了ModuleID的定义:joy:。但至少,我们已经向生成正经的IR迈出一小步了,不是吗?

创建全局变量(常量)

模块

1
2
3
4
5
6
7
8
9
10
11
12
auto intType = llvm::Type::getInt32Ty(context); 
// 获取int32类型
auto num = llvm::ConstantInt::get( // 创建常量(值)
intType // 类型
, llvm::APInt(32, 233)); // 常量的值, APInt(32, 233) 代表 32 位无符号的 `233`
auto test = new llvm::GlobalVariable( // 创建全局常量
*global.get() // 当前 module
, intType // 类型
, true // 是否为常量 - 如果为false,则是创建变量
, llvm::GlobalVariable::ExternalLinkage // 链接类型
, num // 全局常量对应的值
, "test"); // 全局常量的名称

​ 添加代码后运行程序,会获得以下输出

1
2
3
; ModuleID = '<test>'

@test = constant i32 233

​ 可以看到,模块中增加了对test常量的定义,是类型为32位整型的233值。

​ 同样的,也可以添加字符串等特殊类型的全局变量/常量,只是步骤更为复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
std::string str = "Hello, LLVM !";
// 要定义为全局变量的字符串
std::vector<llvm::Constant *> bytes;

auto byteType = llvm::Type::getInt8Ty(context);
// 获取 int8 类型
auto byteArrayType = llvm::ArrayType::get(byteType, str.size());
// 获取字符串类型(字节数组)

for (auto c : str)
bytes.push_back( // 将每个字符依次定义为int8的常值
llvm::ConstantInt::get(
byteType
, llvm::APInt(8, c)));

auto byteArray = llvm::ConstantArray::get( // 创建int8数组
byteArrayType
, bytes);
auto _str = new llvm::GlobalVariable(*global.get(), byteArrayType // 创建全局变量
, false // 这里设置为false,代表变量而非常量
, llvm::GlobalVariable::ExternalLinkage
, byteArray, "str");

​ 添加上述代码后运行程序,获得以下输出:

1
2
3
4
; ModuleID = '<test>'

@test = constant i32 233
@str = global [13 x i8] c"Hello, LLVM !"

​ 通过llc对输出进行编译,可以获得以下汇编(amd64架构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
phosphorus15@ubuntu:~/llvm-test$ ./llir1 | llc
.text
.file "<stdin>"
.type test,@object # @test
.section .rodata,"a",@progbits
.globl test
.align 4
test:
.long 233 # 0xe9
.size test, 4

.type str,@object # @str
.data
.globl str
str:
.ascii "Hello, LLVM !"
.size str, 13
.section ".note.GNU-stack","",@progbits

声明函数

​ 与传统C++类似,llvm的代码段也都是包含在函数内的,声明函数是构造指令与代码逻辑的基础。同时,函数声明也用于显式地链接来自外部库的函数。(如libc中的函数)

​ 以C系最熟悉的main函数为例,我们用以下代码进行声明:

1
2
3
4
5
6
7
8
9
auto intType = llvm::Type::getInt32Ty(context);
auto bytePtrType = llvm::Type::getInt8PtrTy(context);
// 获取int8指针类型
auto byteArrPtrType = byteType->getPointerTo();
// 获取int8双重指针类型

global->getOrInsertFunction("main" // 声明或获取已声明的函数
, intType // 返回值类型
, intType, byteArrPtrType); // 参数类型 (变长参数表)

​ 添加并运行以上代码,获得输出

1
2
3
4
; ModuleID = '<test>'
source_filename = "<test>"

declare i32 @main(i32, i8**)

​ 我们也可以选择以构造函数对象的方式来给模块声明函数

1
2
3
4
5
6
7
8
9
10
11
std::vector<llvm::Type *> printfArgs;
// 参数类型列表
printfArgs.push_back(byteArrType);
auto printfType = llvm::FunctionType::get( // 创建函数类型
intType // 返回值
, printfArgs // 参数
, true); // 是否为不定长参数
llvm::Function::Create(printfType // 通过函数类型来声明函数
, llvm::Function::ExternalWeakLinkage // 弱extern链接
, "printf" // 函数名
, global.get()); // 声明函数的模块

​ 添加并运行以上代码,获得向应输出

1
2
3
4
5
6
; ModuleID = '<test>'
source_filename = "<test>"

declare i32 @main(i32, i8**)

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

​ 模块的函数声明只能有两种链接类型,extern(默认类型)和extern_weak,在printf函数的声明中,我们显式地指定了ExternalWeakLinkage,因此在输出的IR Code中有extern_weak修饰符。

基础指令集: 运算,返回与调用

LLVM 的 “值”

​ 在进入本节前,不妨先看一看LLVM系统中各种“值”的表示方式。

​ 下图是一个取自官方文档的Value类继承结构图。从图中可以看出,不论是常量值立即数值(数值常量),还是全局变量甚至指令,都继承了llvm::User,而llvm::Userllvm::Value的子类。确实,在构建IR的过程中,我们就是以llvm::Value为基指明我们IR所操作的对象的。

​ 这种表示方式带来的好处是显而易见的——在很多熟悉的汇编指令集中,我们都需要给功能相同、但是参数类型不同(如寄存器/立即数/内存)的情形使用特定的指令。而llvm系统中值的继承方式,让我们在构建逻辑正确的前提下,避免区分不同值的“性质”而带来的烦恼。

Value Type Diagram

在函数中构建指令

​ 一段正确的的IR构建示范如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
auto functionBar = llvm::cast<llvm::Function>(
global->getOrInsertFunction("bar", intType, intType, intType, intType));
// 声明函数 int32 bar(int32, int32, int32)
// llvm::cast 将返回的指针转为 llvm::Function 指针
auto blockEntry = llvm::BasicBlock::Create(context, "entry", functionBar);
// 创建一个位于bar函数中的代码块 "entry"
auto functionArgs = functionBar->args();
// 获取函数的参数列表 (可以理解为“实参列表”)

int index = 0;
std::vector<llvm::Value *> argsVector;
for(auto& arg : functionArgs) {
arg.setName(std::string(1, 'a' + (index ++)));
argsVector.push_back(&arg);
}
/*
上面的循环将bar函数的三个参数按顺序存到了本地的std::vector里
并分别命名为 a,b,c (命名只是为了标识,没有实际意义)
此时函数声明为 int32 bar(int32 a, int32 b, int32 c)
*/

builder.SetInsertPoint(blockEntry);
// 设置IR Builder的插入点为"entry"块

auto addtmp = builder.CreateAdd(argsVector[0], argsVector[1], "addtmp");
// 插入 add 指令,以a和b为参数,将运算结果存到 addtmp 中
auto ret = builder.CreateMul(addtmp, argsVector[2], "retValue");
// 插入 mul 指令,以addtmp和c为参数,将运算结果存到 retValue 中
builder.CreateRet(ret);
// 插入 ret 指令,把retValue作为返回值

llvm::verifyFunction(*functionBar, &llvm::errs());
// 验证函数构建是否存在错误

​ 以上代码会获得输出:

1
2
3
4
5
6
7
8
9
; ModuleID = '<test>'
source_filename = "<test>"

define i32 @bar(i32 %a, i32 %b, i32 %c) {
entry:
%addtmp = add i32 %a, %b
%retValue = mul i32 %addtmp, %c
ret i32 %retValue
}

​ 把这段IR Code交给llc生成汇编,可以看到生成的关键代码(x86架构),验证了生成的IR Code符合我们的预期:

1
2
3
4
5
6
7
8
9
10
11
bar:                                    # @bar
.cfi_startproc
# %bb.0: # %entry
movl 4(%esp), %eax
addl 8(%esp), %eax
imull 12(%esp), %eax
retl
.Lfunc_end0:
.size bar, .Lfunc_end0-bar
.cfi_endproc
# -- End function

​ 在每次函数构建完成后,利用llvm::verifyFunction验证构建的IR Code是否正确十分重要,llvm并不强制你进行验证,但及时进行验证总是一种好习惯。

​ 在上述代码中,如果去掉builder.CreateRet(ret);一句(即不创建返回语句),在运行程序时,会看到一句来自llvm::verifyFunction的信息,警告你函数并未正确终止。

1
2
Basic Block in function 'bar' does not have terminator!
label %entry

​ 这也是llvm系统便利性的体现——在IR表示下,系统有着完整且可扩展的验证/优化/编译套件,而且大部分都是可选的模块化组件,相关支持十分完善。

今天成功把llvm开发环境从虚拟机迁移到wsl了,很开心www

构建函数调用

​ 构建函数调用通过IR Builder中的CreateCall实现,标准的调用方式十分浅显。在上文已经定义了bar函数的基础上,以下通过一小段代码概括

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
auto functionBaz = llvm::cast<llvm::Function>(
global->getOrInsertFunction("baz", intType));
// 创建函数 baz, 返回值为int32
auto blockEntryBaz = llvm::BasicBlock::Create(context, "entry", functionBaz);
// 创建 entry 代码块

std::vector<llvm::Value *> barArgs;
for (int i = 0; i < 3; i++)
barArgs.push_back(llvm::ConstantInt::get(intType, llvm::APInt(32, 1 << i)));
/*
初始化调用参数列表为三个int32
值分别为1, 2, 4
*/

builder.SetInsertPoint(blockEntryBaz);
// 设置IR Builder的插入点为"entry"块
auto calltmp = builder.CreateCall( // 插入调用指令
functionBar // 调用的目标函数
, barArgs // 给函数提供的参数
, "calltmp"); // 返回值的存储变量
builder.CreateRet(calltmp);
// 插入 ret 指令,把 calltmp 作为返回值

​ 这里我们首先创建了一个baz函数,并在baz函数内用三个常量(立即数)调用了bar函数。生成的对应IR Code如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; ModuleID = '<test>'
source_filename = "<test>"

define i32 @bar(i32 %a, i32 %b, i32 %c) {
entry:
%addtmp = add i32 %a, %b
%retValue = mul i32 %addtmp, %c
ret i32 %retValue
}

define i32 @baz() {
entry:
%calltmp = call i32 @bar(i32 1, i32 2, i32 4)
ret i32 %calltmp
}

​ 本章节旨在大致介绍llvm的函数和指令的构建方式,如要了解更多类型的指令,可以自行翻阅文档,笔者(可能)也会在后文中补充记录一些基础指令。

SSA与控制流

LLVM中的SSA

​ 读者在上面的代码中可能已经注意到,与很多编程语言的风格不同,LLVM IR中并未出现任何变量复用的情况。一个变量在被赋值之后,就再没有被二次赋值过。实际上,如果你尝试在IR Builder中对变量进行二次赋值,实际生成的IR中也会对二次赋值的变量进行重命名。这就要提到LLVMSSA 性质

​ SSA (Static single assignment form), 是指在IR中,所有变量严格仅被赋值一次,且保证先声明后赋值的行为,这种性质使得IR能够更高效地优化,也是LLVM中模块化优化过程(Pass)的保障。

控制流 - 无条件跳转

​ 当提到一款编程语言, 控制流分支跳转就不可避免地被提及。在llvm中,一个所对应的标识符就是当前分支的标签。下面代码构建了一个死循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto functionBar = llvm::cast<llvm::Function>(
global->getOrInsertFunction("bar", llvm::Type::getVoidTy(context)));
// 创建函数 void bar()

auto blockEntry = llvm::BasicBlock::Create(context, "entry", functionBar);
// 创建一个 entry 块
builder.SetInsertPoint(blockEntry);
auto blockLoop = llvm::BasicBlock::Create(context, "loop", functionBar);
// 创建一个 loop 块
builder.CreateBr(blockLoop);
// 插入分支跳转语句
builder.SetInsertPoint(blockLoop);
builder.CreateBr(blockLoop);
// 插入分支跳转语句

llvm::verifyFunction(*functionBar, & llvm::errs());

​ 上面的代码可能不是很清晰,没关系,来分析一下输出的IR Code(见#号注释)

1
2
3
4
5
6
7
8
9
10
; ModuleID = '<test>'
source_filename = "<test>"

define void @bar() {
entry: # `entry`块
br label %loop # 跳转到 loop 分支

loop: # `loop` 块
br label %loop # 跳转到 loop 分支 (形成死循环)
}

​ 通过观察IR,我们至少可以明白以下几点:

  1. br的作用就是进行无条件跳转
  2. 每一个“块”其实就是一个包含代码的跳转标签
  3. llvm IR中,除第一个入口块外,每一个块都必须显式地被其它块跳转到,不然会被判定为死块(即使是顺序连在一起也不行,如果删掉entry块里的跳转语句,IR验证时会报错)

控制流 - 布尔值与逻辑运算

​ 在llvm中,布尔值有专门的类型i1表示,作为一个比特的单位,i1的值只能是0(对应假)或者1(对应真)。通过特定指令,可以完成一系列比较/逻辑运算:

1
2
3
4
5
6
7
8
9
builder.CreateFCmpOEQ(lvalue, rvalue, "cmpResult");
// 比较 lvalue 和 rvalue 是否相等, 结果在cmpResult中
// FCmp 代表进行浮点比较
// cmpResult 的类型为 i1

builder.CreateICmpSGT(ilvalue, irvalue, "icmpResult");
// 比较 iLvalue 是否大于 iRvalue , 结果在icmpResult中
// ICmp 代表进行浮点比较 - SGT 是有符号, UGT 是无符号
// icmpResult 的类型为 i1

​ 较完全的比较函数清单如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FCMP_OEQ   =  1,  ///< 0 0 0 1    True if ordered and equal
FCMP_OGT = 2, ///< 0 0 1 0 True if ordered and greater than
FCMP_OGE = 3, ///< 0 0 1 1 True if ordered and greater than or equal
FCMP_OLT = 4, ///< 0 1 0 0 True if ordered and less than
FCMP_OLE = 5, ///< 0 1 0 1 True if ordered and less than or equal
FCMP_ONE = 6, ///< 0 1 1 0 True if ordered and operands are unequal
FCMP_ORD = 7, ///< 0 1 1 1 True if ordered (no nans)
FCMP_UNO = 8, ///< 1 0 0 0 True if unordered: isnan(X) | isnan(Y)
FCMP_UEQ = 9, ///< 1 0 0 1 True if unordered or equal
FCMP_UGT = 10, ///< 1 0 1 0 True if unordered or greater than
FCMP_UGE = 11, ///< 1 0 1 1 True if unordered, greater than, or equal
FCMP_ULT = 12, ///< 1 1 0 0 True if unordered or less than
FCMP_ULE = 13, ///< 1 1 0 1 True if unordered, less than, or equal
FCMP_UNE = 14, ///< 1 1 1 0 True if unordered or not equal
ICMP_EQ = 32, ///< equal
ICMP_NE = 33, ///< not equal
ICMP_UGT = 34, ///< unsigned greater than
ICMP_UGE = 35, ///< unsigned greater or equal
ICMP_ULT = 36, ///< unsigned less than
ICMP_ULE = 37, ///< unsigned less or equal
ICMP_SGT = 38, ///< signed greater than
ICMP_SGE = 39, ///< signed greater or equal
ICMP_SLT = 40, ///< signed less than
ICMP_SLE = 41, ///< signed less or equal

​ 逻辑运算与位运算都是通过同一套方法生成,分别是:

1
2
3
4
// 以下参数用简记, lhs 是左值, rhs 是右值, ret是存储返回值的变量
builder.CreateAnd(lhs, rhs, ret);
builder.CreateOr(lhs, rhs, ret);
builder.CreateXor(lsh, rhs, ret);

控制流 - 条件跳转

​ 条件跳转真正引入了分支与循环