目的

实现一个能处理 main 函数和 return 语句的编译器, 同时输出编译后的 RISC-V 汇编。

编译器会将如下的 SysY 程序:

int main() {
  // 摊牌了, 我是注释
  return 0;
}

编译为对应的 RISC-V 汇编:

  .text
  .globl main
main:
  li a0, 0
  ret

或:

  .text
  .globl main
main:
  li t0, 0
  mv a0, t0
  ret

实现

Lv2.1. 处理 Koopa IR

这一节的任务是建立内存形式的 Koopa IR。

libkoopa 中的接口并没有我定义的AST数据类型,所以可以先把 Program Dump 到 string 中,再用koopa_parse_from_string解析这个 string,得到内存形式的 Koopa IR。

Lv2.2. 目标代码生成

这一节的任务是生成 RISC-V 汇编。

可以写一个头文件和cpp文件(ASMGenerator.h,ASMGenerator.cpp)专门用来生成汇编。在ASMGenerator.h中声明需要用到的函数,

class ASMGenerator {
public:
    ASMGenerator(std::ostream &os) : os(os) {}
    void Generate(const koopa_raw_program_t &program);

private:
    // DFS 遍历族
    void Visit(const koopa_raw_program_t &program);
    void Visit(const koopa_raw_slice_t &slice);
    void Visit(const koopa_raw_function_t &func);
    void Visit(const koopa_raw_basic_block_t &bb);
    void Visit(const koopa_raw_value_t &value);

    // 辅助
    void EmitPrologue();
    void EmitEpilogue();

    // 共享状态
    std::ostream &os;
    int frame_size = 0;                  // 对齐后的栈帧大小
    std::string cur_func;                // 当前函数名(已去 @)
};

在ASMGenerator.cpp分别实现这些函数,

EmitPrologue()

EmitPrologue() 生成函数开头的固定汇编代码,负责声明符号 、分配栈帧、保存返回地址 、保存旧的帧指针、设置新的帧指针。

    void ASMGenerator::EmitPrologue() {
    os << "  .text\n";
    os << "  .globl " << cur_func << "\n";
    os << cur_func << ":\n";
    // 栈帧: 保存 ra(4) + s0(4), 对齐到 16 字节
    frame_size = 16;
    os << "  addi sp, sp, -" << frame_size << "\n";
    os << "  sw ra, " << (frame_size - 4) << "(sp)\n";
    os << "  sw s0, " << (frame_size - 8) << "(sp)\n";
    os << "  addi s0, sp, " << frame_size << "\n";
    }
RISC-V 的某些指令(比如加载/存储双字 ld/sd)要求地址按 16 字节对齐,否则触发异常。目前 frame_size 写死为 16,刚好匹配。

EmitEpilogue()

对应 EmitEpilogue()(尾声)做相反的操作:恢复 ra/s0 → 回收栈帧 → ret。

    void ASMGenerator::EmitEpilogue() {
    os << ".L" << cur_func << "_exit:\n";
    os << "  lw ra, " << (frame_size - 4) << "(sp)\n";
    os << "  lw s0, " << (frame_size - 8) << "(sp)\n";
    os << "  addi sp, sp, " << frame_size << "\n";
    os << "  ret\n";
    }

DFS 遍历族

Generate 开启 visit program 的 funcs, Visit(const koopa_raw_slice_t &slice) 遍历 slice 的 buffer 进行下一步的visit,

  • KOOPA_RSIK_FUNCTION 则进行 ASMGenerator::Visit(const koopa_raw_function_t &func)。因为 Koopa IR 和 RISC-V 汇编的语法规则不同,Koopa IR 中,函数名带 @ 前缀:@main 、call @getint是标签名的合法字符, 而RISC-V 汇编(GNU assembler)中,@ 不是标签名的合法字符:.globl @main、 @main:、call @getint , 而汇编器要求标签名只能由字母、数字、.、_ 组成,所以必须去掉 @ 。函数体为空(仅有声明)则跳过,然后是EmitPrologue(),遍历基本块,最后是 EmitEpilogue()。

  • KOOPA_RSIK_BASIC_BLOCK 则进行 ASMGenerator::Visit(const koopa_raw_basic_block_t &bb)。发射基本块标签,然后再遍历指令。

  • KOOPA_RSIK_VALUE 则进行 ASMGenerator::Visit(const koopa_raw_value_t &value)。switch(value->kind.tag) 判断继续执行什么操作,当前只要完成return功能就可以了。

Lv2.3. 测试

make完成后,先执行

/root/build/compiler -riscv hello.c -o hello.s

可以完成对汇编的转化。

再测试

autotest -riscv -s lv1 /root

测试结果: 2