翻成 x86-64 并真正编出来
目标
把 mini 程序翻译成 x86-64 汇编(GNU as,AT&T 语法),交给 gcc 烧制并运行。表达式用 %rax 和栈来翻译,函数按 System V 调用约定来翻译,并且即使在计算过程中的调用里,也要把栈对齐到 16 字节。
为什么重要
原生代码没有 VM 的循环,由 CPU 直接读取,但作为代价,VM 原来代劳的决定——值的位置、栈帧、参数传递——都要由编译器来做,而且要想与 C 库互相调用,就必须遵守 ABI 的约定。违反了这些约定的代码,通常大多数时候运行得很好,只在某些输入上崩溃。
规则
받는 부분집합 함수는 맨 바깥에서만 선언하고 이름으로 직접 부른다 · 인자 6개까지 · 매개변수와 반환값은 정수
(클로저·함수 값·실행 중 타입 오류·스택 넘침은 이 백엔드가 다루지 않는다)
값의 자리 모든 식의 값은 %rax. bool 은 0/1. 두 항: 왼쪽 → push %rax → 오른쪽 → mov %rax, %rcx → pop %rax → 연산
정수 리터럴은 movabs(32비트를 넘는 즉시값) · 비교는 cmp %rcx, %rax → setl 등 → movzbq %al, %rax
! 는 xor $1, %rax · && 와 || 는 cmp $0 과 je/jne 로 오른쪽을 건너뛴다
이름의 자리 main 맨 위의 let → .data 의 mini_g_<이름>: .quad 0, 읽기·쓰기는 mini_g_<이름>(%rip)
그 밖의 let 과 매개변수 → 프레임 슬롯 k 는 -8*(k+1)(%rbp). 슬롯은 함수마다 0 부터 하나씩(재사용 없음)
함수 mini_f_<이름>. 들어오면 push %rbp · mov %rsp, %rbp · sub $(슬롯 수×8 을 16의 배수로 올림), %rsp
인자를 %rdi %rsi %rdx %rcx %r8 %r9 에서 자기 슬롯으로 옮긴다 · return: 값을 %rax 에 → leave · ret
return 없이 끝나면 mov $0, %rax · leave · ret. main 은 .globl main, 맨 위 문장들을 차례로 실행
호출 인자를 왼쪽부터 계산해 push, 다 되면 거꾸로 pop 해 인자 레지스터에 싣고 call mini_f_<이름>
정렬 call 직전 %rsp 는 16의 배수. 지금 올려 둔 칸 수가 홀수면 sub $8, %rsp · call · add $8, %rsp
런타임 mini_print_int(v) · mini_print_bool(v) · mini_div/mini_mod/mini_pow(a, b, 줄, 칸)
(/opt/fixtures/mini/runtime.c — 부를 때마다 정렬을 확인하고, 0 나누기 등은 인터프리터와 같은 글자로 끝낸다)
끝 '.section .note.GNU-stack,"",@progbits' 를 붙인다(실행 가능한 스택이 필요 없다는 표시)
步骤
- 补全
/root/mini/codegen.py中Codegen的emit、label、push、pop、call、expr(整数、布尔值)、stmt(print、表达式语句)、function和new_slot——只靠 main 和输出就让程序跑起来。 - 补全
arith——一元、二元、比较,/ % ^ 交给运行时辅助函数。 - 补全
lookup、expr_more和stmt_more——全局变量与赋值。 - 补全
stmt_block——块内局部变量(槽位)与遮蔽。 - 补全
expr_control和stmt_control——短路求值、if/else、while。 - 补全
expr_call和stmt_function——函数、参数寄存器、return。这一步的测试程序,在调用的那一刻已压入的格数是偶数。 - 确认
call能保持对齐——评分器会运行在参数中间、在右操作数里进行调用的程序。 - 补全模块级的
generate(program)和build(src, out_path)。评分器会把固定程序和 25 个随机程序烧制出来,与解释器对照。
参考
python3 /root/mini/mini.py asm 파일.mini(占位符为文件名)会生成汇编,python3 /root/mini/mini.py build 파일.mini -o prog && ./prog(占位符为文件名)会生成可执行文件。- 在第 1–7 步,评分器用
Codegen().function("main", [], 문장들, main=True)(占位符为语句列表)做出 main,再接上done和data烧制。generate在第 8 步补全。 - 常见错误:用 32 位 mov 装载 64 位立即数,sub 的操作数顺序(AT&T 写作
sub 원본, 대상,占位符依次为源操作数和目标操作数),把参数寄存器装反,return 时漏掉 leave,栈帧大小没有取成 16 的倍数,在计算过程中的 call 里忘了对齐。 - 会话从 60 分钟开始,可以用+时间延长,结束后
/root/mini会消失。
main 与输出
function 先生成函数体、得知用了多少个槽位之后,再把序言加到前面。整数用 movabs $值, %rax,布尔值用 mov $1(或 0), %rax。print 把值移到 %rdi,并按类型调用 mini_print_int 或 mini_print_bool。
算术与运行时辅助函数
按左边 → push → 右边 → mov %rax, %rcx → pop 的顺序,让 %rax 中是左边,%rcx 中是右边。AT&T 的 sub %rcx, %rax 就是 rax = rax - rcx。/ % ^ 把两个值装进 %rdi %rsi,把行和列装进 %rdx %rcx,然后调用辅助函数。
全局变量放在 .data 中
scopes 为空时(main 的最上层),let 就是全局的。在 data 里追加一行 'mini_g_名字: .quad 0',用 mov %rax, mini_g_名字(%rip) 写入值。读取时,从 lookup 返回的地址 mov。要把布尔变量打印成 true/false,还需要一并记住类型。
块内局部变量与栈帧
每个块都往 scopes 里压入一个空字典,结束时丢弃。局部 let 用 new_slot 取得槽位,写入 -8*(槽位+1)(%rbp)。lookup 从内层字典开始看。栈帧大小要把槽位数×8 向上取整到 16 的倍数,函数体开头的对齐才是对的。
标号与跳转
if 是条件 → cmp $0, %rax → je 另一边 → then → jmp 结尾 → 另一边: → else → 结尾:。while 在 起点: 每次都重新计算条件。&& 在左边为 0(je)时跳到结尾,把 %rax 里的 0 直接作为结果。标号要做成 .L1、.L2 这样互不重复。
函数与调用约定
fn 先把名字记到 functions 里(为了递归),再用 function('mini_f_名字', 形参, 函数体) 来生成。形参在序言之后,从 %rdi %rsi … 转存到槽位。调用时先依次 push 参数,再倒过来 pop 装进寄存器,然后 call。
计算过程中的调用与 16 字节对齐
push 和 pop 在计数 pushed,所以在 call 之前看这个数是不是奇数。如果是奇数,就用 sub $8, %rsp 对齐后再调用,并用 add $8, %rsp 恢复。1 + f(2) 在左边的 1 已压入的状态下调用 f,所以会碰到这种情况。
烧制整个程序
generate 用一个 Codegen,把 main 做成 function(…, main=True),并依次接上 .text、做好的各个函数、.data 和 GNU-stack 标记。build 在解析和语义分析之后,把汇编写入临时文件,用 gcc -O0 -fno-omit-frame-pointer 与运行时一起链接。