TT Lab
开始
学习 学习路径 课程

编译器 — 从头到尾亲手打造一门小语言

翻成 x86-64 并真正编出来

在 TT Lab 中继续学习

目标

把 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' 를 붙인다(실행 가능한 스택이 필요 없다는 표시)

步骤

  1. 补全 /root/mini/codegen.py 中 Codegen 的 emit、label、push、pop、call、expr(整数、布尔值)、stmt(print、表达式语句)、function 和 new_slot——只靠 main 和输出就让程序跑起来。
  2. 补全 arith——一元、二元、比较,/ % ^ 交给运行时辅助函数。
  3. 补全 lookup、expr_more 和 stmt_more——全局变量与赋值。
  4. 补全 stmt_block——块内局部变量(槽位)与遮蔽。
  5. 补全 expr_control 和 stmt_control——短路求值、if/else、while。
  6. 补全 expr_call 和 stmt_function——函数、参数寄存器、return。这一步的测试程序,在调用的那一刻已压入的格数是偶数。
  7. 确认 call 能保持对齐——评分器会运行在参数中间、在右操作数里进行调用的程序。
  8. 补全模块级的 generate(program) 和 build(src, out_path)。评分器会把固定程序和 25 个随机程序烧制出来,与解释器对照。

参考

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 与运行时一起链接。