7. 生成机器码

完成上一节留空的 compile 方法。

整个 brainfuck 程序将被编译为一个大函数,在上一节的 run 方法中我们已经指定了该函数的签名。

type RawFn = unsafe extern "sysv64" fn(
    this: *mut BfVM<'_>,
    memory_start: *mut u8,
    memory_end: *const u8,
) -> *mut VMError;

在 compile 方法中,首先初始化汇编器,函数起始地址就是最开始的偏移。

loops 作为栈,用来存放动态标签,指引跳转。

impl BfVM<'_> {
    #[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 存放返回值,这些属于易失性寄存器,在调用子函数时其内容可能丢失。

rbp, rbx, r12 ~ r15 是非易失性寄存器,在调用子函数时不会丢失。如果函数中会占用这些寄存器,也要在开头和结尾相应地保存和恢复它们的内容。

我们在注释里记录函数会用到的所有寄存器。rdi, rsi, rdx 对应函数的三个参数,我们将其保存到 r12, r13, r14 寄存器。函数中需要一个 ptr 变量记录 brainfuck 程序的当前指针,我们使用 r15 寄存器。

        // this:         rdi r12
        // memory_start: rsi r13
        // memory_end:   rdx r14
        // ptr:              r15

汇编函数开始,首先把 rax 压栈。x86-64-psABI 规定参数区域的结尾按16字节对齐。函数开始时返回地址压栈,此时 栈指针+8 是 16 的倍数,因此再把 rax 压栈,使栈指针对齐,以便之后的函数调用,rax 的内容没有意义。

由于函数中用到 r12 ~ r15 非易失性寄存器,将其压栈保存。注意这里压入 4 个 8 字节寄存器,栈指针仍然对齐。

        dynasm!(ops
            ; push rax
            ; push r12
            ; push r13
            ; push r14
            ; push r15
            ; mov r12, rdi   // save this
            ; mov r13, rsi   // save memory_start
            ; mov r14, rdx   // save memory_end
            ; mov r15, rsi   // ptr = memory_start
        );

每个 IR 依次映射到汇编。

指针移动,需要检查算术溢出和数组边界溢出,出错即跳转到全局标签所指的错误处理区域。

        use BfIR::*;
        for &ir in code {
            match ir {
                AddPtr(x) => dynasm!(ops
                    ; add r15, x as i32     // ptr += x
                    ; jc  ->overflow        // jmp if overflow
                    ; cmp r15, r14          // ptr - memory_end
                    ; jnb ->overflow        // jmp if ptr >= memory_end
                ),
                SubPtr(x) => dynasm!(ops
                    ; sub r15, x as i32     // ptr -= x
                    ; jc  ->overflow        // jmp if overflow
                    ; cmp r15, r13          // ptr - memory_start
                    ; jb  ->overflow        // jmp if ptr < memory_start
                ),

单个字节的算术加减,允许溢出。

                AddVal(x) => dynasm!(ops
                    ; add BYTE [r15], x as i8    // *ptr += x
                ),
                SubVal(x) => dynasm!(ops
                    ; sub BYTE [r15], x as i8    // *ptr -= x
                ),

IO 操作。首先保存当前数据指针寄存器,将虚拟机函数所需的各参数和函数地址放入寄存器,调用函数。

如果函数返回的不是空指针,说明出错,应该跳转到IO错误处理区域。

最后恢复数据指针寄存器。

                GetByte => dynasm!(ops
                    ; mov  rdi, r12         // arg0: this
                    ; mov  rsi, r15         // arg1: ptr
                    ; mov  rax, QWORD BfVM::getbyte as _
                    ; call rax              // getbyte(this, ptr)
                    ; test rax, rax
                    ; jnz  ->io_error       // jmp if rax != 0
                ),
                PutByte => dynasm!(ops
                    ; mov  rdi, r12         // arg0: this
                    ; mov  rsi, r15         // arg1: ptr
                    ; mov  rax, QWORD BfVM::putbyte as _
                    ; call rax              // putbyte(this, ptr)
                    ; test rax, rax
                    ; jnz  ->io_error       // jmp if rax != 0
                ),

跳转指令。利用 dynasm 提供的 api, 创建两个动态标签,分别生成跳转汇编。由于编译到 IR 时已经验证过跳转指令的对应关系,这里的栈可以直接弹出。

                Jz => {
                    let left = ops.new_dynamic_label();
                    let right = ops.new_dynamic_label();
                    loops.push((left, right));

                    dynasm!(ops
                        ; cmp BYTE [r15], 0
                        ; jz => right       // jmp if *ptr == 0
                        ; => left
                    )
                }
                Jnz => {
                    let (left, right) = loops.pop().unwrap();
                    dynasm!(ops
                        ; cmp BYTE [r15], 0
                        ; jnz => left       // jmp if *ptr != 0
                        ; => right
                    )
                }
            }
        }

正常退出函数时应该返回空指针,表示没有错误。

溢出时生成一个溢出错误,IO错误时错误指针已经存入 rax,无需处理。

最后退栈,与函数开始时的压栈对应,维持栈平衡。注意退栈时不能破坏 rax 中的返回值。

        dynasm!(ops
            ; xor rax, rax
            ; jmp >exit
            ; -> overflow:
            ; mov rax, QWORD BfVM::overflow_error as _
            ; call rax
            ; jmp >exit
            ; -> io_error:
            ; exit:
            ; pop r15
            ; pop r14
            ; pop r13
            ; pop r12
            ; pop rdx
            ; ret
        );

完成汇编,取出可执行缓冲区,返回。

        let code = ops.finalize().unwrap();

        Ok((code, start))
    }
}