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))
    }