编译器后端之LLVM

359 阅读6分钟

LLVM中的设计规范


首先LLVM中的库和工具都是通过C++来编写的, 利用了很多的面向对象的特性, 每一个对象都是一个组件或者叫做一个接口模块。

LLVM中经常使用模版, 但是需要小心模版带来的性能消耗, 也就是控制好C++项目的编译时间, 因为C++项目中的典型模版滥用问题会造成比较长的编译时间, 一旦有可能, LLVM会使用模版特化来允许实现快速

尽量使用libLLVMSupport中实现的断点机制,注意调试编译器是非常困难的一件事情, 因为编译器的产物是另外一种程序, 因此可以尽早检测出错误的行为, 就不需要为了确定的程序是否正确而编写一个并不重要的复杂输出。

LLVM代码中会出现另外一种常见的做法是使用智能指针, C++采用析构函数加上delete关键字来进行内存管理, delete关键字对应的语义是, 判断指针是否为空, 在指针不为空的时候调用析构函数并释放之前的内存。

在LLVM中使用轻量级别的字符串引用 如果频繁使用const string&会导致

LLVM 的IR 和 tools

传统的编译器后端往往都是手写的比如C语言, 由于LLVM的出现我们就避免去造轮子, 直接利用LLVM的给我们提供的库去做, 我们可以去看下面这张图片。

Snipaste_2023-10-25_15-23-30.png

我们首先需要做的就是将AST转化成为一个LLVM IR, 为了方便我们直接使用这个S-expression来做为我们的语法, 因为lexer/parser其实是完全没有必要的。

首先我们创建一个文件:

int main(){return 42}
  1. clang++ -S -emit-llvm test.cpp 使用这个命令去生成一个IR
  2. clang++ -o test test.ll 根据IR生成一个test可执行文件
  3. lli这个工具是解释器, 我们可以使用lli test.ll

编译后的IR具有一些元数据, 这些元数据是给编译器看的, 就相当于那个汇编代码的伪指令

define dso_local i32 @main() {                         
  ret i32 42                                                                       
} 

IR的所有函数基本上都是全局的, 除此之外LLVM还有一个llvm-as可以生成这个bitcode, 还有一个反汇编工具llvm-dis可以将bitcode转化成为IR的。

Snipaste_2023-10-25_16-11-56.png

我们在我们的文件中使用了LLVM的头文件, 我们需要在编译的时候引入这些东西, 我们可以写一个脚本

clang++ -o eva-llvm `llvm-config --cxxflags --ldflags --system-libs --libs core`  \
eva-llvm.cpp

LLVM 程序结构

Snipaste_2023-10-25_16-19-05.png

关于上面这张图片可以使用下面这段代码来解释:

llvm::Function* fn
std::unique_ptr<llvm::LLVMContext> ctx;
std::unique_ptr<llvm::Module> module;
std::unique_ptr<llvm::IRBuilder<>> builder;

ctx = std::make_unique<llvm::LLVMContext>();
module = std::make_unique<llvm::Module>("EvaLLVM", *ctx);
builder = std::make_unique<llvm::IRBuilder<>>(*ctx);

需要一个main函数, main函数对应的IR是这样的:

define i32 @main(){
entry:
    ret i32 42
}

对应的CPP代码是

//1.函数名称
//2.函数的类型: FunctionType::get来创建

fn = createFunction(
    "main", 
    llvm::FunctionType::get(
    /*返回值类型*/ builder->getInt32Ty(), 
    /*不接收参数*/ false)); 

//创建一个main函数
llvm::Function* createFunction(const std::string& fnName, llvm::FunctionType* fnType) {
    //在符号表中查找函数
    auto fn = module->getFunction(fnName);
    
    //如果没有在符号表中查找到函数
    if (fn == nullptr) {
        //创建一个函数原型
        fn = createFunctionProto(fnName, fnType);
    }
    
    //创建函数入口块
    createFunctionBlock(fn);
    return fn;
 }
 
llvm::Function* createFunctionProto(const std::string& fnName, llvm::FunctionType* fnType) {
    auto fn = llvm::Function::Create(
        fnType, 
        llvm::Function::ExternalLinkage, 
        fnName, 
        *module);
     
    verifyFunction(*fn);
    return fn;
}


void createFunctionBlock(llvm::Function* fn) {
    //传入块所属的作用域
    auto entry = createBB("entry", fn);
    //一旦我们创建一个块, 我们必须显示告诉IRbuilder
    builder->SetInsertPoint(entry);
}

llvm::BasicBlock* createBB(std::string name, llvm::Function* fn = nullptr){
    return llvm::BasicBlock::Create(*ctx, name, fn);
}

我们首先声明那个标准库中的函数:

auto bytePtrTy = builder->getInt8Ty()->getPointerTo();

//打印函数
module->getOrInsertFunction(
    "printf",
    llvm::FunctionType::get(
      /*return type*/builder->getInt32Ty(),
      /*format args*/bytePtrTy,
      /*vararg*/ true
    ));

上面我们自己构造了一个函数, 但是现在我们想调用一个标准库的函数

//创建一个字符串指针
auto str = builder->CreateGlobalStringPtr("Hello, World");
//获取IR中的函数
auto printfFn = module->getFunction("printf");
//函数参数
std::vector<llvm::Value*> args{str};
//在IR中生成
builder->CreateCall(printfFn, args);

在LLVM中关于变量的那些事情:

Snipaste_2023-11-02_22-19-39.png

首先我们创建一个全局的作用域

llvm::GlobalVariable* createGlobalVar(const std::string& name, llvm::Constant* init)
{   
    module->getOrInsertGlobal(name, init->getType());
    //从模块中获得全局变量
    auto variable = module->getNamedGlobal(name);
    //设置IR的大小
    variable->setAlignment(llvm::MaybeAlign(4));
    variable->setConstant(false);
    //设置IR的初始值
    variable->setInitializer(init);
    return variable;
}

但是只有一个全局作用域的时候, 后面的变量会覆盖前面的变量:

@VERSION = global i32 getelementptr inbounds

我们可以自已设置一个环境:

void setupGlobalEnviroment()
{   
    //创建global
    std::map<std::string, llvm::Value*> globalObject{
        {"Version", builder->getInt32(42)},
    };  

    std::map<std::string, llvm::Value*> globalRec{};

    for (auto& entry: gloablObject)
    {   
       globalRec[entry.first] =
           createGloablVar(entry.first, (llvm::Constant*)entry.second);
    }   

    GlobalEnv = std::make_shared<Enviroment>(globalRec, nullptr);
} 

LLVM中的活动记录


前面我们定义了一种数据结构Environment, 这种数据结构方便我们去存储我们的变量, 我们现在创建的东西是方便去管理我们的环境。

Snipaste_2023-11-04_14-39-43.png

/*
局部变量的声明格式
(var a 1)
(var (name i32) 32)
*/


//首先是获取声明
auto varNameDecl = exp.list[1];
auto varName = extractVarName(varNameDecl);
//获取初始化的值
auto init = gen(exp.list[2], env);
//获取变量的类型
auto varTy = extractVarType(varNameDecl);
//将变量类型, 变量的名称, 变量的环境所绑定
auto varBinding = allocVar(varName, varTy, env);
return builder->CreateStore(init, varBinding);

然后就是函数的具体细节

//x -> x
//(x number) -> x
std::string extractVarName(const Exp& exp)
{
    return exp.type == ExpType::LIST ? exp.list[0].string : exp.string;
}

//x -> i32
//(x number) -> number
llvm::Type* extractVarType(const Exp& exp)
{
    return exp.type == ExpType::LIST ? 
        getTypeFromString(exp.list[1].string) : builder->getInt32Ty();
}

然后给这些局部变量分配地址:

llvm::Value* allocVar(const std::string& name, llvm::Type* type_, Env env)
{   
    varsBuilder->SetInsertPoint(&fn->getEntryBlock());
    auto varAlloc = varsBuilder->CreateAlloca(type_, 0, name.c_str());
    //add to the environment
    env->define(name, varAlloc);

    return varAlloc;
} 

从生成的代码上去看, 是SSA的, 在编译器的设计中, 静态单赋值形式是中间表示IR的特性, 每个变量被赋值一次, 在原始的IR中, 已经存在的变量可被分割成为许多不同的版本。

%x = alloca i32, align 4
store i32 42, i32* %x, align 4
%x1 = alloca i32, align 4

下面我们将引入if块, 我们知道if语句根据条件传递到不同的块, 如果条件为真的话, 我们会传递到一个块, 如果条件为假的话我们会传递到另外一个块

llvm中的if语句

llvm-if-block.png

创建三个block

auto thenBlock = createBB("then", fn);
auto elseBlock = createBB("else");
//这个叫做结果的合并块
auto ifEndBlock = createBB("ifend");

创建两个分支,因为需要执行代码之后才知道执行哪一个分支, 所有我们需要两个都创建, 但是我们可以做优化

//condation branch
builder->CreateCondBr(cond, thenBlock, elseBlock);

//then branch
builder->SetInsertPoint(thenBlock);
auto thenRes = gen(exp.list[2], env);
builder->CreateBr(ifEndBlock);


//else branch
builder->SetInsertPoint(elseBlock);
auto elseRes = gen(exp.list[3], env);
builder->CreateBr(ifEndBlock);

对应到IR就是:

br i1 %tmpcmp, label %then label %else

同时由于LLVM是采用整个的静态单点赋值, 所以应该不会在同一个块中出现两个相同的变量, 所以你不可以使用下面这样的语句

var x;

if (isDigit(x)) {
    x = 2;
    //llvm会释放掉x
} else {
   x = "hello";
   //llvm会释放掉x
}
console.log(x);

我们会使用llvm中的phi指令:

auto phi = builder->CreatePHI(thenRes->getType(), 2, "tmpif");
phi->addIncoming(thenRes, thenBlock);
phi->addIncoming(elseRes, elseBlock);

如果碰到if语句的嵌套, 往往又会出现一些问题, 所以我们需要解决一下嵌套问题, 我们可以添加一个块列表