5

LLVM IR简介

 3 years ago
source link: http://intheworld.win/2020/08/22/introduction-of-llvm-ir/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Post Views: 317

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

llvm_arch.png

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是类似的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK