章节目录

汇编:为 copy 增加翻译规则

本节阅读量:

第一章的汇编生成器已经会做这些事:

1
2
3
4
给每条 IR 操作的 dst 分配栈槽
在函数开头一次性留出栈空间
把 add 翻译成 movq、addq、movq
把最终结果放进 %rax

第二章继续使用这套流程。汇编生成器只需要认识上一节新增的 copy 操作。

copy 怎样生成汇编

代码在:

1
code/02_let/src/compile/assembly.cpp

IR:

1
x0 = 40

表示把操作数 40 保存到内部名字 x0。它只需要两条指令:

1
2
movq $40, %rax
movq %rax, -8(%rbp)

对应的新增分支是:

1
2
3
4
case OpKind::copy:
    out_ += "    movq " + operand_text(op.lhs) + ", %rax\n";
    out_ += "    movq %rax, " + stack_slot(op.dst) + "\n";
    return;

这里仍然使用第一章已有的两个辅助函数:

1
2
operand_text(op.lhs)  把整数或 IR 名字写成汇编操作数
stack_slot(op.dst)    找到结果对应的栈槽

因此,如果 lhs 是另一个 IR 名字,copy 也不需要特殊处理。例如:

1
x1 = x0

可以生成:

1
2
movq -8(%rbp), %rax
movq %rax, -16(%rbp)

这里借 %rax 中转是有实际原因的:x0 和 x1 都已经位于内存栈槽,普通 movq 不能直接完成一次内存到内存的复制,必须先读进寄存器,再写到另一个内存位置。

对于 x0 = 40,x86-64 其实允许进一步选择更短的写法。当前 emitter 仍统一使用“先放进 %rax,再保存到目标栈槽”的两步模板,让整数、临时结果和其他 IR 名字都走同一条清楚的生成规则;这里暂时不做指令数量优化。

完整例子

源码:

1
(let x 40 (+ x 2))

IR:

1
2
3
x0 = 40
t.0 = x0 + 2
return t.0

其中只有第一条使用新的 copy 分支;第二条 add 和最后的返回都沿用第一章:

1
2
3
4
5
6
movq $40, %rax
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
addq $2, %rax
movq %rax, -16(%rbp)
movq -16(%rbp), %rax

这段 IR 有两个 dst,所以栈槽分配是:

1
2
x0  -> -8(%rbp)
t.0 -> -16(%rbp)

两个栈槽一共 16 字节,不需要继续向上填充。加入第一章的栈帧开头和结尾后,Linux 或 WSL 上的完整输出是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
.globl main
main:
    pushq %rbp
    movq %rsp, %rbp
    subq $16, %rsp
    movq $40, %rax
    movq %rax, -8(%rbp)
    movq -8(%rbp), %rax
    addq $2, %rax
    movq %rax, -16(%rbp)
    movq -16(%rbp), %rax
    movq %rbp, %rsp
    popq %rbp
    retq

从源码到位置的关系现在完整接上了:

1
源码绑定 x -> IR 名字 x0 -> 栈槽 -8(%rbp) -> 运行时保存 40

Shadowing 也不需要新的汇编规则。lowerer 已经把两个同名变量变成 x0 和 x1,原有的栈槽分配会自然地给它们不同的位置:

1
2
3
x0  -> -8(%rbp)    外层 x
x1  -> -16(%rbp)   内层 x
t.0 -> -24(%rbp)   最终加法结果

三个栈槽需要 24 字节,生成器向上对齐后会在函数开头分配 32 字节:

1
subq $32, %rsp

布局是:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
较高地址

    8(%rbp)   返回地址
    0(%rbp)   调用者原来的 %rbp       <- %rbp
   -8(%rbp)   x0:外层 x
  -16(%rbp)   x1:内层 x
  -24(%rbp)   t.0:最终加法结果
  -32(%rbp)   对齐留下的空间          <- %rsp

较低地址

离开内层 let 时,lowerer 环境不再把源码名字 x 指向 x1,但 -16(%rbp) 不会立刻释放。当前生成器在函数入口一次分配整个固定栈帧,到函数返回时才整体收回;词法作用域结束和运行时栈槽回收是两件不同的事。

到这里,第二章新增的编译路径就接通了:

1
let 绑定 -> copy IR -> copy 汇编分支

手动验证

1
2
3
4
5
6
cd code/02_let
make
./mini compile examples/let.lang -o out.s
cc out.s -o out
./out
echo $?

退出码应该是:

1
42

2.5 IR:把用户名字变成内部名字

上一节

2.7 本章总结

下一节