LLVM IR简介

对于LLVM这样的编译框架来说,IR是非常重要的。LLVM IR(Intermediate Representation,中间表示)连接着编译器前端和编译器后端。IR的设计很大程度体现着LLVM插件化、模块化的设计哲学,LLVM的各种pass其实都是作用在LLVM IR上的。同时IR也是一个编译器组件接口。通常情况下,设计一门新的编程语言只需要完成能够生成LLVM IR的编译器前端即可,然后就可以轻松使用LLVM的各种编译优化、JIT支持、目标代码生成等功能。

LLVM IR的表示形式

LLVM IR有三种形式:

内存中的表示形式,如BasicBlock,Instruction这种cpp类;
bitcode形式,这是一种序列化的二进制表示形式;
LLVM汇编文件形式,这也是一种序列化的表示形式,与bitcode的区别是汇编文件是可读的、字符串的形式。
内存中的IR模型

内存中IR模型其实就是对应LLVM实现中的OO模型,更直白的就是一些cpp的class定义。

Module类,Module可以理解为一个完整的编译单元。一般来说,这个编译单元就是一个源码文件,如一个后缀为cpp的源文件。
Function类,这个类顾名思义就是对应于一个函数单元。Function可以描述两种情况,分别是函数定义和函数声明。
BasicBlock类,这个类表示一个基本代码块,“基本代码块”就是一段没有控制流逻辑的基本流程,相当于程序流程图中的基本过程(矩形表示)。
Instruction类,指令类就是LLVM中定义的基本操作,比如加减乘除这种算数指令、函数调用指令、跳转指令、返回指令等等。
出了上面这几个类之外,还有两个基本类型Value和User。

Value类,Value是一个非常基础的基类,一个继承于Value的子类表示它的结果可以被其他地方使用。
User类,一个继承于User的类表示它会使用一个或多个Value对象。
根据Value与User之间的关系,还可以引申出use-def链和def-use链这两个概念。use-def链是指被某个User使用的Value列表,def-use链是使用某个Value的User列表。实际上,LLVM中还定义了一个Use类,Use就是图中的一个边。

use-def,以函数与函数调用指令之间的关系为例。同一个函数实例可以在多个地方被调用。所以在LLVM中就可以通过以下方式查看一个函数被调用的指令列表。

Function *F = ...;
for (User *U : F->users()) {
  if (Instruction *Inst = dyn_cast(U)) {
    errs() << "F is used in instruction:\n";
    errs() << *Inst << "\n";
  }
}

def-use,以指令和指令操作数为例。一个指令可以有一个或多个操作数。以下代码可以遍历指令的操作数。

Instruction *pi = ...;
for (Use &U : pi->operands()) {
  Value *v = U.get();
  // ...
}

汇编形式的IR

如前文所说,汇编形式的IR是可读的。所以这里用一个简单的例子展示一下汇编形式的IR。首先编写一个简单的c语言函数如下:

// add.cpp
int add(int a, int b) {
    return a + b;
}

使用如下命令可以产生汇编形式的IR:

clang add.cpp -emit-llvm -S -c -o add.ll

具体的汇编IR如下(有精简):


; ModuleID = 'add.cpp'
source_filename = "add.cpp"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"

; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @_Z3addii(i32, i32) #0 {
  %3 = alloca i32, align 4
  %4 = alloca i32, align 4
  store i32 %0, i32* %3, align 4
  store i32 %1, i32* %4, align 4
  %5 = load i32, i32* %3, align 4
  %6 = load i32, i32* %4, align 4
  %7 = add nsw i32 %5, %6
  ret i32 %7
}

从第7~第16行就是add函数的汇编IR,还是具有一定可读性的。即使没有了解LLVM IR的具体指令定义,也大概能对应上源码与汇编IR之间的关系。

LLVM IR的生成

上面非常简单的介绍了LLVM IR的表示形式,这边想介绍一下IR具体的生成方式。LLVM提供了IR的构建接口,语言前端可以调用这些接口实现代码生成。IR的构建其实可以很复杂,但本文的定位仅仅是简介。所以这里会根据一个简单的例子,来介绍一下LLVM IR的构建过程。

//  Created by InTheWorld on 2020/8/22.

#include "llvm/ADT/STLExtras.h"
#include "llvm/IR/Argument.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/Instructions.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/IR/Module.h"
#include "llvm/IR/Type.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/TargetSelect.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/ToolOutputFile.h"
#include "llvm/Bitcode/BitcodeWriter.h"
#include 
#include 
#include 
#include 

using namespace llvm;
int main() {
    LLVMContext Context;
    // Create some module to put our function into it.
    std::unique_ptr Owner = std::make_unique("test", Context);
    Module *M = Owner.get();
    
    // Create the add1 function entry and insert this entry into module M.  The
    // function will have a return type of "int" and take an argument of "int".
    Function *Add1F =
        Function::Create(FunctionType::get(Type::getInt32Ty(Context),
                                             {Type::getInt32Ty(Context)}, false),
                           Function::ExternalLinkage, "add1", M);

    // Add a basic block to the function. As before, it automatically inserts
    // because of the last argument.
    BasicBlock *BB = BasicBlock::Create(Context, "EntryBlock", Add1F);

    // Create a basic block builder with default parameters.  The builder will
    // automatically append instructions to the basic block `BB'.
    IRBuilder<> builder(BB);
    // Get pointers to the constant `1'.
    Value *One = builder.getInt32(1);
    // Get pointers to the integer argument of the add1 function...
    assert(Add1F->arg_begin() != Add1F->arg_end()); // Make sure there's an arg
    Argument *ArgX = &*Add1F->arg_begin();          // Get the arg
    ArgX->setName("AnArg");            // Give it a nice symbolic name for fun.

    // Create the add instruction, inserting it into the end of BB.
    Value *Add = builder.CreateAdd(One, ArgX);
    // Create the return instruction and add it to the basic block
    builder.CreateRet(Add);
    // Now, function add1 is ready.
    // Now we're going to create function `foo', which returns an int and takes no
    // arguments.
    Function *FooF =
        Function::Create(FunctionType::get(Type::getInt32Ty(Context), {}, false),
                           Function::ExternalLinkage, "foo", M);
    // Add a basic block to the FooF function.
    BB = BasicBlock::Create(Context, "EntryBlock", FooF);
    // Tell the basic block builder to attach itself to the new basic block
    builder.SetInsertPoint(BB);
    // Get pointer to the constant `10'.
    Value *Ten = builder.getInt32(10);
    // Pass Ten to the call to Add1F
    CallInst *Add1CallRes = builder.CreateCall(Add1F, Ten);
    Add1CallRes->setTailCall(true);
    // Create the return instruction and add it to the basic block.
    builder.CreateRet(Add1CallRes);

    outs() << "We just constructed this LLVM module:\n\n" << *M;
    outs().flush();
    std::error_code error_code;
    std::unique_ptr Out(new ToolOutputFile(
        "./test.bc", error_code, sys::fs::F_None));
    
    if (errorCodeToError(error_code)) {
        errs() << errorCodeToError(error_code) << '\n'; return -1; } //write bitcode WriteBitcodeToFile(*M, Out->os());
    Out->keep(); // Declare success
    return 0;
}

这段代码是我基于LLVM项目中关于JIT功能的一段sample code改过来的。这段代码的目标很明确就是构建一个IR Module,这个Module由以下两个函数构成:

int add1(int x) {
 return x+1;
}

int foo() {
 return add1(10);
}

理解这段代码的方式就是把自己当做一个编译器前端,看着这两个函数。然后根据语义,把它们翻译成LLVM IR。41~56行是在构建add1的IR,60~73行是在构建foo的IR。

这段程序的最后会把IR的汇编形式打印出来,也会保存一份到test.bc这个文件里。其中控制台输出如下:

We just constructed this LLVM module:

; ModuleID = 'test'
source_filename = "test"

define i32 @add1(i32 %AnArg) {
EntryBlock:
  %0 = add i32 1, %AnArg
  ret i32 %0
}

define i32 @foo() {
EntryBlock:
  %0 = tail call i32 @add1(i32 10)
  ret i32 %0
}

是不是感觉很眼熟?与前面clang生成的IR是类似的。

发表评论