跳转至

后端框架介绍

本篇文档将介绍后端的代码框架。阅读前,你应该浏览下这里的目录结构,对框架的文件布局有总体的印象。

极简框架

本次实验的框架相对简单,你只需要少量的代码阅读就能快速进入代码撰写的环节。

顶层的 Codegen 类只维护了如下成员:

class CodeGen {
    // ...
  private:
    struct { ... } context;             // 类似 lab2 的 context,用于保存翻译过程中的上下文信息,如当前所在函数
    Module *m;                          // 输入的 IR 模块
    std::list<ASMInstruction> output;   // 生成的汇编指令
};

几个上层函数如下:

class CodeGen {
    // ...
  public:
    explicit CodeGen(Module *module) : m(module) {} // 构造函数
    std::string print() const;                      // 将汇编指令格式化输出
    void run();                                     // 后端代码生成的入口函数
}

你需要了解或者实现的是下面一系列函数:

class CodeGen {
    // ...
  private:
    // 栈式分配的变量分配环节,将在函数翻译开始时调用
    void allocate();

    /*=== 以下为助教准备的辅助函数 ===*/
    // 将数据在寄存器和栈帧间搬移。下边的章节将详细介绍
    void load_xxx(...);
    void store_xxx(...);
    // 添加汇编指令
    void append_inst(...);
    // 基本块在汇编程序中的名字
    static std::string label_name(BasicBlock *bb);
    /*=== 以上为助教准备的辅助函数 ===*/

    // 需要补全的部分,进行代码生成的各个环节
    void gen_xxx(...);
};

初始代码已经为你处理好了一些繁琐的细节,如全局变量的定义及初始化、汇编中 section type 的定义等,所以你可以把重点放在栈式分配的实现中。

基本类描述

框架对后端中的指令和寄存器进行了抽象,下面将依次介绍这两个基本类。

指令类

指令类 ASMInstruction 是用来描述一行汇编指令的,在 CodeGen 中以 std::list 形式组织。指令类的代码定义如下:

struct ASMInstruction {
    enum InstType {
        Instruction,    // 汇编指令
        Atrribute,      // 汇编伪指令、描述符等非常规指令
        Label,          // 汇编中的 label
        Comment         // 注释
    } type;             // 用来描述指令的用途,会被下面的 format 函数使用

    std::string content; // 汇编代码,不包含换行符等格式化的信息

    explicit ASMInstruction(std::string s, InstType ty = Instruction); // 构造函数
    std::string format() const; // 根据 type 对 content 进行格式化(如添加缩进、换行符等)
};

举个例子,ASMInstruction("some debug info", ASMInstruction::Comment) 定义了一个指令类实例,其用途是注释, format() 的返回结果是如下字符串:"#some debug info\n"

寄存器类

寄存器分为通用寄存器 Reg 、浮点寄存器 FReg 和条件标志寄存器 CFReg

以下是 FReg 的代码定义,RegCFReg 的定义与之类似:

struct FReg {
    unsigned id; // 0 <= id <= 31

    explicit FReg(unsigned i);
    bool operator==(const FReg &other);

    std::string print() const;  // 根据 id 返回寄存器别名,如 "$fa0" 而不是 "$f0"

    static FReg fa(unsigned i); // 得到寄存器 $faN
    static FReg ft(unsigned i); // 得到寄存器 $ftN
    static FReg fs(unsigned i); // 得到寄存器 $fsN
};

举例:

  • FReg(0) 定义了寄存器 $f0 的实例,print() 的结果是 "$fa0"
  • 为了获得 $ft0 的实例,你可以使用 FReg(8),也可以使用更方便的FReg::ft(0)

辅助函数

在顶层的 CodeGen 类中,助教提供了很多辅助函数,主要是进行数据装载/写回的 load/store 相关和追加指令相关两大类,来帮助你更舒适地完成本次实验。

load/store

阅读过栈式分配介绍后,你将认同:我们生成的汇编中,load、store 的使用会非常频繁。

针对此,我们提供了如下函数,用于方便地提取数据至寄存器和将寄存器数据保存至栈上。

class CodeGen {
    //...
  private:
    // 向寄存器中装载数据
    void load_to_greg(Value *, const Reg &);    // 将 IR 中的 Value 加载到整形寄存器中
    void load_to_freg(Value *, const FReg &);   // 将 IR 中的 Value 加载到浮点寄存器中

    // 将寄存器中的数据保存回栈上
    void store_from_greg(Value *, const Reg &); // 将整形寄存器中的数据保存至 IR 中 Value 对应的栈帧位置
    void store_from_freg(Value *, const FReg &);// 将浮点寄存器中的数据保存至 IR 中 Value 对应的栈帧位置
};

你只需要提供 IR 中的 Value 指针,同时指定目标寄存器,即可方便地完成数据的加载或备份。

事实上,寄存器的另一大数据来源,还有立即数的提取。对于 12bit 能够表示的整形立即数,你可以直接使用 $dest = $zero + imm 的形式,对于比较复杂的大立即数提取及浮点立即数提取,我们提供了一些辅助函数:

class CodeGen{
    // ...
  private:
    // 向寄存器中加载立即数
    void load_large_int32(int32_t, const Reg &);    // 提取 32 bit 的整数
    void load_large_int64(int64_t, const Reg &);    // 提取 64 bit 的整数
    void load_float_imm(float, const FReg &);       // 提取单精度浮点数(32bit)
};

关于这些 API,虽然在这里给出了一些注释,它们看起来很好用,但是仍然不建议你开箱即用,希望你能简单阅读一下源码,理解其中在做什么后,再拿来使用。

append_inst()

CodeGen 中以 std::list 的形式组织 ASMInstructionappend_inst() 接口就是用来添加新的 ASMInstruction

这里有两个版本的append_inst()

  • 你可以直接按照 ASMInstruction 的构造函数添加指令,如:

    append_inst("st.d $ra, $sp, -8", ASMInstruction::Instruction);
    // 第二个参数的默认值即为 ASMInstruction::Instruction,所以下边的代码等价
    append_inst("st.d $ra, $sp, -8");
    
  • 也可以使用二次封装后的版本:

    append_inst("st.d", {"$ra", "$sp", "-8"}, ASMInstruction::Instruction);
    // 最后一个参数的默认值即为 ASMInstruction::Instruction,所以下边的代码等价
    append_inst("st.d", {"$ra", "$sp", "-8"});
    

    这么看还没有什么区别,在 include/codegen/CodeGenUtil.hpp 中,我们定义了一系列宏定义,你可以去看看,使用这些宏定义,结合其他 API,上述调用会变得不太一样:

    append_inst(STORE DOUBLE, {Reg::ra().print(), Reg::sp().print(), "-8"});
    

变量分配

关于变量分配的实验方案,我们已经在栈式分配介绍中有过提及,初始代码已经为你实现,在 src/codegen/CodeGen.cpp 中的 CodeGen::allocate()

你可以设计自己喜欢的 allocate(),但是这不被推荐,因为我们不能保证框架的其他部分(主要是辅助函数)和你的设计兼容。