7. 生成机器码
完成上一节留空的 compile 方法。
首先初始化汇编器。整个 brainfuck 程序编译为一个大函数,函数起始地址就是最开始的偏移。
loops 作为栈,用来存放动态标签,指引跳转。
impl<'io> BfVM<'io> {
#[allow(clippy::fn_to_numeric_cast)]
fn compile(code: &[BfIR]) -> Result<(dynasmrt::ExecutableBuffer, dynasmrt::AssemblyOffset)> {
let mut ops = dynasmrt::x64::Assembler::new()?;
let start = ops.offset();
let mut loops = vec![];
sysv64 调用约定规定 rdi, rsi, rdx, rcx 存放前四个整数参数,rax 存放返回值。
在注释中约定各个寄存器存放的参数,虚拟机调用裸函数时将把参数放入寄存器。
// this: rdi r12
// memory_start: rsi r13
// memory_end: rdx r14
// ptr: rcx r15
程序开始,首先把 rax 压栈。x86-64-psABI 规定参数区域的结尾按16字节对齐。函数开始时返回地址压栈,此时 栈指针+8 是 16 的倍数,因此再把 rax 压栈,使栈指针对齐,以便之后的函数调用,rax 的内容没有意义。
把寄存器中的各参数存入非易失性寄存器,调用其他函数时参数寄存器的值可能丢失。rbp, rbx, r12 ~ r15 是非易失性寄存器。
dynasm!(ops
; push rax
; mov r12, rdi // save this
; mov r13, rsi // save memory_start
; mov r14, rdx // save memory_end
; mov rcx, rsi // ptr = memory_start
);
每个 IR 依次映射到汇编。
指针移动,需要检查算术溢出和数组边界溢出,出错即跳转到全局标签所指的错误处理区域。
use BfIR::*;
for &ir in code {
match ir {
AddPtr(x) => dynasm!(ops
; add rcx, x as i32 // ptr += x
; jc ->overflow // jmp if overflow
; cmp rcx, r14 // ptr - memory_end
; jnb ->overflow // jmp if ptr >= memory_end
),
SubPtr(x) => dynasm!(ops
; sub rcx, x as i32 // ptr -= x
; jc ->overflow // jmp if overflow
; cmp rcx, r13 // ptr - memory_start
; jb ->overflow // jmp if ptr < memory_start
),
单个字节的算术加减,允许溢出。
AddVal(x) => dynasm!(ops
; add BYTE [rcx], x as i8 // *ptr += x
),
SubVal(x) => dynasm!(ops
; sub BYTE [rcx], x as i8 // *ptr -= x
),
IO 操作。首先保存当前数据指针寄存器,将虚拟机函数所需的各参数和函数地址放入寄存器,调用函数。
如果函数返回的不是空指针,说明出错,应该跳转到IO错误处理区域。
最后恢复数据指针寄存器。
GetByte => dynasm!(ops
; mov r15, rcx // save ptr
; mov rdi, r12
; mov rsi, rcx // arg0: this, arg1: ptr
; mov rax, QWORD BfVM::getbyte as _
; call rax // getbyte(this, ptr)
; test rax, rax
; jnz ->io_error // jmp if rax != 0
; mov rcx, r15 // recover ptr
),
PutByte => dynasm!(ops
; mov r15, rcx // save ptr
; mov rdi, r12
; mov rsi, rcx // arg0: this, arg1: ptr
; mov rax, QWORD BfVM::putbyte as _
; call rax // putbyte(this, ptr)
; test rax, rax
; jnz ->io_error // jmp if rax != 0
; mov rcx, r15 // recover ptr
),
跳转指令。利用 dynasm 提供的 api, 创建两个动态标签,分别生成跳转汇编。由于编译到 IR 时已经验证过跳转指令的对应关系,这里的栈可以直接弹出。
Jz => {
let left = ops.new_dynamic_label();
let right = ops.new_dynamic_label();
loops.push((left, right));
dynasm!(ops
; cmp BYTE [rcx], 0
; jz => right // jmp if *ptr == 0
; => left
)
}
Jnz => {
let (left, right) = loops.pop().unwrap();
dynasm!(ops
; cmp BYTE [rcx], 0
; jnz => left // jmp if *ptr != 0
; => right
)
}
}
}
正常退出函数时应该返回空指针,表示没有错误。
溢出时生成一个溢出错误,IO错误时错误指针已经存入 rax,无需处理。
最后退栈,与函数开始时的压栈对应,维持栈平衡。
dynasm!(ops
; xor rax, rax
; jmp >exit
; -> overflow:
; mov rax, QWORD BfVM::overflow_error as _
; call rax
; jmp >exit
; -> io_error:
; exit:
; pop rdx
; ret
);
完成汇编,取出可执行缓冲区,返回。
let code = ops.finalize().unwrap();
Ok((code, start))
}