LLVM的IR构造教程

675 阅读8分钟

LLVM IR代码类似于三地址代码(一种最常用的中间语言),具有人类可读的表示形式。它是 LLVM 的中间表达形式。构造 IR 的过程,属于前端开发的一部分,下文介绍如何使用 LLVM 的接口,来构造基本的 IR , 并顺便讲解,在SKyEye 动态翻译开发中,常用的 IR 语法和指令。

尽管 SkyEye 在 common/dyncom 中提供了已经封装好的,构造 IR 的接口,但是只有动手实操以后,才能理解这些接口的正确用处,以便更好的掌握它们。

阅读此教程,需要先掌握 IR 的基本语法和 C++ 开发相关的知识。

构造LLVM的基本环境

与其他教程不同,这里会从代码实操的角度,带领大家一步一步掌握 IR 构造的方法。首先我们需要准备一个 LLVM 的基本环境,不同版本的 LLVM,初始化的方法有所不同,这主要的影响是对C++ 标准的支持,LLVM3.0 是比较古老的版本,它还不能支持 C++11 以上的标准。

因此,我们构造 LLVM 基本环境的时候,要以最原始的方法来构造。

头文件介绍

下文给出了构造 IR 和 JIT 环境的主要头文件,这也是我们 SkyEye JIT 环境所需要的全部头文件。重要的部分,已经在代码中给出了注释。

复制#include <iostream>
#include <vector>
#include <string>

#include "llvm/Analysis/Verifier.h"  //检验模块,校验函数
#include "llvm/Module.h" // 一个源文件的抽象
#include "llvm/Target/TargetData.h"
#include "llvm/DerivedTypes.h"
#include "llvm/ExecutionEngine/ExecutionEngine.h"
#include "llvm/ExecutionEngine/JIT.h" // JIT引擎
#include "llvm/ExecutionEngine/Interpreter.h" // JIT解释执行

#include "llvm/LLVMContext.h"  // 公共的数据结构,用来关联模块
#include "llvm/PassManager.h"  // Pass优化
#include "llvm/Analysis/Verifier.h" // 模块检验,用于调试和纠错
#include "llvm/Analysis/Passes.h"
#include "llvm/Target/TargetData.h"
#include "llvm/Transforms/Scalar.h"
#include "llvm/Support/IRBuilder.h"   // 指令生成器
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/ADT/SmallVector.h"  // 更智能的vector

using namespace std;

namespace llvm {
    class BasicBlock;
    class ExecutionEngine;
    class Function;
    class FunctionPassManager;
    class Module;
    class PointerType;
    class StructType;
    class Value;
    class LLVMContext;
}

创建LLVM基本环境

这里我们以 SkyEye 的视角切入,基本可以分为两大部分,一个是LLVM的基本环境初始化,这里包括 LLVM 上下文,模块,IR 构建器;另一个是 JIT 环境和执行引擎初始化。

复制int main(void)
{
    // Create LLVM base environment
    LLVMContext *llvmContext = new LLVMContext();
    Module *module = new Module("func-module", *llvmContext);
    IRBuilder <> irBuilder(*llvmContext);

    ...

    // Initialize LLVM JIT environment
    InitializeNativeTarget();
    InitializeNativeTargetAsmPrinter();
    InitializeNativeTargetAsmParser();
    LLVMLinkInJIT();

    // Create LLVM JIT exec_engine
    string error;
    EngineBuilder builder(module);
    builder.setErrorStr(&error);
    builder.setEngineKind(EngineKind::JIT);
    builder.setOptLevel(CodeGenOpt::Aggressive);
    ExecutionEngine *exec_engine = builder.create();
}

基本的调试手段

在 llvm3.0 中,可以使用dump()来打印构造好的 IR,Value类提供的dump()可以打印自己对应的 IR,module类dump(),可以打印整个模块的 IR,Function类dump()可以打印构造的 jit-function 的 IR。

具体代码如下:

复制    LLVMContext *llvmContext = new LLVMContext();
    Module *module = new Module("ir_global", *llvmContext);

    module->dump();  //print IR

    // Create @func(i32 guest_pc) ;return i32
    vector <Type *> params;
    params.push_back(irBuilder.getInt32Ty());
    FunctionType *fType = FunctionType::get(irBuilder.getInt32Ty(), ArrayRef<Type*>(params), false);
    Function *func = Function::Create(fType, GlobalValue::ExternalLinkage, "func", module);

    func->dump(); //print IR

    Value *tmp = irBuilder.CreateAlloca(irBuilder.getInt32Ty(), 0, "");

    tmp->dump();  //print IR

IR的类型系统

我们知道,汇编语⾔是弱类型的,在操作汇编语⾔的时候,实际上考虑的是⼀些⼆进制串。但是,LLVM IR 却是强类型的,在 LLVM IR 中所有变量都必须有类型。这是因为,我们在使⽤⾼级语⾔编程的时候,往往都会使⽤强类型的语⾔,弱类型的语⾔⽆必要性,也不利于维护。因此,使⽤强类型语⾔,LLVM IR 可以更好地进⾏优化。

基本的数据类型

LLVM IR 中比较基本的数据类型包括:

  • 空类型( void )
  • 整型( iN )
  • 浮点型( float 、 double 等)

空类型⼀般是作为不返回值的函数的返回类型,没有特别的含义,就代表「什么都没有」。

整型是指 i1 , i8 , i16 , i32 , i64 这类的数据类型。这⾥ iN 的 N 可以是任意正整数,可以是 i3 , i1942652 。但最常⽤,最符合常理的就是 i1 以及8的整数倍。 i1 有两个值:truefalse。也就是说,下⾯的代码可以正确编译:

复制%boolean_variable = alloca i1
store i1 true, i1* %boolean_variable

对于⼤于1位的整型,也就是如 i8 , i16 等类型,我们可以直接⽤数字字面量赋值:

复制%integer_variable = alloca i32
store i32 128, i32* %integer_variable
store i32 -128, i32* %integer_variable

注意,这里的数字,其实是 const 类型,我们在代码中构造IR的时候,是不能直接给数值的,而是要先构造一个 const 类型的常量,然后赋值给具体的 Value。具体构造方法,我们下文再谈。

现在用 C++ 构造一个int a = 10;,代码如下:

复制    //int a = 10;
    module->getOrInsertGlobal("a", irBuilder.getInt32Ty());
    GlobalVariable *a = module->getNamedGlobal("a");
    a->setInitializer(irBuilder.getInt32(10));

这里我们默认a变量是一个全局变量,给它赋值10,我们可以通过setInitializer()实现。生成的 IR 如下:

复制@a = global i32 10

指针类型

将基本的数据类型后加上⼀个星号就变成了指针类型i8* , i16* , float*等。我们之前提到,LLVM IR 中的全局变量和栈上分配的变量都是指针,所以其类型都是指针类型。

在⾼级语⾔中,直接操作裸指针的机会都⽐较少,除⾮在性能极其敏感的场景下。这是因为,裸指针极其危险,稍有不慎就会出现段错误等致命错误,所以我们使⽤指针时应该慎之⼜慎。

这里给出int *p = &a;的IR构造代码:

复制    // int *p = &a;
    GlobalVariable *p = static_cast<GlobalVariable *>( //第二种写法
        module->getOrInsertGlobal("p", PointerType::getInt32PtrTy(*llvmContext))
    );
    p->setInitializer(a);

生成的IR如下:

复制@p = global i32* @a

聚合类型

比起指针类型⽽⾔,更重要的是聚合类型。我们在 C 语⾔中常⻅的聚合类型有数组和结构体,LLVM IR 也为我们提供了相应的⽀持。

数组

数组类型很简单,我们要声明⼀个类似 C 语⾔中的int a[4],只需要:

复制%a = alloca [4 x i32]

也就是说,C语⾔中的 int[4] 类型在LLVM IR中可以写成 [4 x i32] 。注意,这⾥⾯是个 x 不是 * 。

我们也可以使⽤类似地语法进⾏初始化:

复制@global_array = global [4 x i32] [i32 0, i32 1, i32 2, i32 3]

特别地,我们知道,字符串在底层可以看作字符组成的数组,所以LLVM IR为我们提供了语法糖:

复制@global_string = global [12 x i8] c"Hello world\00"

在字符串中,转义字符必须以\xy的形式出现,其中xy是这个转义字符的ASCII码。⽐如说,字符串的结尾,C语⾔中的\0,在LLVM IR中就表现为\00

这里给出数组的IR构造代码:

复制    // double d[2] = {1.1, 2.2, 3.3};
    ArrayType *arrayType = ArrayType::get(irBuilder.getDoubleTy(), 2);
    module->getOrInsertGlobal("d", arrayType);
    GlobalVariable *d = module->getNamedGlobal("d");

    vector <Constant *> array_elems;
    Constant *const_tmp;

    // d[0]
    const_tmp = ConstantFP::get(irBuilder.getDoubleTy(), 1.1);
    array_elems.push_back(const_tmp);

    // d[1]
    const_tmp = ConstantFP::get(irBuilder.getDoubleTy(), 2.2);
    array_elems.push_back(const_tmp);

    // d[2]
    const_tmp = ConstantFP::get(irBuilder.getDoubleTy(), 3.3);
    array_elems.push_back(const_tmp);

    d->setInitializer(ConstantArray::get(arrayType, array_elems));

生成的IR如下:

复制@d = global [2 x double] [double 1.100000e+00, double 2.200000e+00, double 3.300000e+00]

结构体

结构体的类型也相对⽐较简单,在C语⾔中的结构体:

复制struct MyStruct {
    int x;
    char y;
};

在LLVM IR中就成了:

复制%MyStruct = type {
    i32,
    i8
}

初始化一个结构体也很简单:

复制@global_structure = global %MyStruct { i32 1, i8 0 }
; or
@global_structure = global { i32, i8 } { i32 1, i8 0 }

值得注意的是,⽆论是数组还是结构体,其作为全局变量或栈上变量,依然是指针,也就是
说,@global_array的类型是[4 x i32]*@global_structure的类型是%MyStruct*也就是{ i32, i8 }*

下面给出结构体的IR构造代码:

复制    // struct s{ int a, float b, double d, int *p};
    StructType *structType = StructType::create(*llvmContext, "s");
    vector <Type * > struct_elems;
    vector <Constant *> struct_elems_init;
    struct_elems.push_back(irBuilder.getInt32Ty());
    struct_elems.push_back(irBuilder.getFloatTy());
    struct_elems.push_back(irBuilder.getDoubleTy());
    struct_elems.push_back(PointerType::getInt32PtrTy(*llvmContext));
    structType->setBody(struct_elems, true);

    // sa = {1, 1.1, 2.2, };
    module->getOrInsertGlobal("sa", structType);
    GlobalVariable *sa = module->getNamedGlobal("sa");
    struct_elems_init.push_back(irBuilder.getInt32(1));
    struct_elems_init.push_back(ConstantFP::get(irBuilder.getDoubleTy(), 1.1));
    struct_elems_init.push_back(ConstantFP::get(irBuilder.getDoubleTy(), 2.2));
    struct_elems_init.push_back(ConstantPointerNull::get(PointerType::getInt32PtrTy(*llvmContext)));
    sa->setInitializer(ConstantStruct::get(structType, struct_elems_init));

生成的IR如下:

复制sa = global %s <{ i32 1, double 1.100000e+00, double 2.200000e+00, i32* null }>

getelementptr

⾸先,我们要讲的是对聚合类型的指针进⾏操作。⼀个最全⾯的例⼦,⽤C语⾔来说,就是:

复制struct MyStruct {
    int x;
    int y;
};
struct MyStruct my_structs[4];

我们有⼀个⻓度为4的MyStruct类型的数组my_structs,我们需要的是my_structs[2].y这个数。

我们先直接看结论,⽤ LLVM IR 来表示为:

复制%MyStruct = type {
    i32,
    i32
}
%my_structs = alloca [4 x %MyStruct]
%1 = getelementptr [4 x %MyStruct], [4 x %MyStruct]* %my_structs, i64 2,
i32 1 ; %1 is pointer to my_structs[2].y
%2 = load i32, i32* %1 ; %2 is value of my_structs[2].y

核⼼就在于 getelementptr 这个指令。这个指令的前两个参数很显然,第⼀个是这个聚合类型的类型,第⼆个则是这个聚合类型对象的指针,也就是我们的 my_structs 。第三个参数,则是指明在数组中的第⼏个元素,第四个,则是指明在结构体中的第⼏个字段(LLVM IR中结构体的字段不是按名称,而是按下标索引来区分)。用人话来说, %1 就是 my_structs 数组第2个元素的第1个字段的地址。

这看上去似乎很好理解,但是,下⾯的例⼦就似乎有些特殊了:

复制%MyStruct = type {
    i32,
    i32
}
%my_struct = alloca %MyStruct
%1 = getelementptr %MyStruct, %MyStruct* %my_struct, i64 0, i32 1 ; %1 is
pointer to my_struct.y

如果想根据结构体的指针获取结构体的字段, getelementptr 的第三个参数还需要⼀个 i64 0 。这⾥就是指数组的第⼀个元素,想象⼀下我们有⼀个C语⾔代码:

复制struct MyStruct {
    int x;
    int y;
};
struct MyStruct my_struct;
struct MyStruct* my_struct_ptr = &my_struct;
int *y_ptr = my_struct_ptr[0].y;

这⾥的my_struct_ptr[0]就代表了我们getelementptr的第三个参数,这万万不可省略。
此外,getelementptr还可以接多个参数,类似于级联调⽤。我们有C程序:

复制struct MyStruct {
    int x;
    int y[5];
};
struct MyStruct my_structs[4];

那么如果我们想获得my_structs[2].y[3]的地址,只需要:

复制%MyStruct = type {
    i32,
    [5 x i32]
}
%my_structs = alloca [4 x %MyStruct]
%1 = getelementptr [4 x %MyStruct], [4 x %MyStruct]* %my_structs, i64 2,
i32 1, i64 3

用C++构造 getelementptr 指令的代码如下:

复制    Value *idx = irBuilder.CreateLoad(index, "idx");
    llvm::SmallVector<llvm::Value *, 2> IdxList;
    IdxList.push_back(llvm::ConstantInt::get(irBuilder.getInt32Ty(), 0));
    IdxList.push_back(idx);
    Value *label_entry = irBuilder.CreateInBoundsGEP(label_array, IdxList);
    //Value *label_entry = GetElementPtrInst::CreateInBounds(label_array, IdxList, "label_entry", bb);

其中label_array 是数组 Value, IdxList 是索引。