菜单
本页目录

02-基础操作码 - 你想构建一个语言虚拟机吗?

引言

当我们上次离开我们勇敢的读者(就是你)时,他们已经到了编写操作码的地步。让我们从那里继续。

什么是操作码 (opcode)?

一个介于 0 和某个上限之间的整数。因为我们使用 8 位来表示一个操作码,我们可以有 255 个操作码。为了在代码中表示它们,我们将使用枚举,因为 Rust 枚举是最棒的。在你的源代码目录中,创建一个名为 instruction.rs 的新文件。

枚举

instruction.rs 中,放入以下代码:

#[derive(Debug, PartialEq)]
pub enum Opcode {
  HLT,
  IGL
}

指令 (Instructions)

记得我们的指令是 32 位吗?让我们创建一个结构体来表示一个完整的指令:

#[derive(Debug, PartialEq)]
pub struct Instruction {
  opcode: Opcode
}

我们稍后会添加更多字段,但目前这样就足够了。我们还需要添加一个实现块:

impl Instruction {
  pub fn new(opcode: Opcode) -> Instruction {
    Instruction {
      opcode: opcode
    }
  }
}

更多测试

instruction.rs 中,如果你还没有,添加一个测试模块:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_hlt() {
        let opcode = Opcode::HLT;
        assert_eq!(opcode, Opcode::HLT);
    }

    #[test]
    fn test_create_instruction() {
      let instruction = Instruction::new(Opcode::HLT);
      assert_eq!(instruction.opcode, Opcode::HLT);
    }
}

是的,这些测试有点简单,但我习惯于在项目早期尽快编写测试。

重要如果你还没有!不要忘记在 main.rs 文件中添加 pub mod instruction

执行循环

我们有了一个操作码和一个指令,还有一个虚拟机。我们下一个关键组件是将执行操作码的函数。前往 vm.rs,我们将在那里进行一些更改。

首先,我们需要向 VM 结构体添加一个向量来存储我们的程序字节码:

#[derive(Debug)]
pub struct VM {
    registers: [i32; 32],
    pc: usize,
    program: Vec<u8>
}
不,我们不是存储一个指令向量,而是存储一个字节向量。指令结构体将在我们稍后编写汇编器时有用。

其次,我们的实现块需要改变:

impl VM {
    pub fn new() -> VM {
        VM {
            registers: [0; 32],
            program: vec![],
            pc: 0,
        }
    }
}

你会注意到我们还添加了一个名为 pc 的字段。这是我们的 程序计数器,将跟踪正在执行的字节。

循环

是时候了!我们可以让我们的虚拟机真正地做些事情了!我的意思是,它将会停止运行,但无论如何,这也算是一个进步!(I mean, it will stop, but still!) 让我们为我们的虚拟机实现添加一个功能:

pub fn run(&mut self) {
    loop {
        // 如果我们的程序计数器超出了程序本身的长度,就出了问题
        if self.pc >= self.program.len() {
            break;
        }
        match self.decode_opcode() {
            Opcode::HLT => {
                println!("遇到 HLT");
                return;
            },
            _ => {
              println!("发现无法识别的操作码!终止!");
              return;
            }
        }
    }
}
重要主执行循环通常被认为是语言解释器中最性能关键的部分。这是一个相当简单的实现,没有优化。我们以后会对此进行大量测试和工作。它涉及诸如 CPU 分支预测之类的主题,这些主题值得另写一篇文章。

上面的代码目前还不能工作。我们把整个程序存储为一个字节向量;我们的虚拟机没有办法知道 HLT 操作码的数字是 0。看到名为 decode_opcode 的函数调用了吗?那将是一个 u8 并将其转换为操作码。它看起来像这样:

fn decode_opcode(&mut self) -> Opcode {
    let opcode = Opcode::from(self.program[self.pc]);
    self.pc += 1;
    return opcode;
}

把它添加到我们 VM 的实现中。注意我们如何使用 Opcode::from?这是一个 Rust Trait。我们需要告诉我们的程序如何将一个字节转换为特定的操作码,我们可以通过为我们的枚举实现这个 trait 来做到这一点。在 instruction.rs 中放入这个:

impl From<u8> for Opcode {
    fn from(v: u8) -> Self {
        match v {
            0 => return Opcode::HLT,
            _ => return Opcode::IGL
        }
    }
}

我们实际上定义了两个匹配分支:一个用于 HLT,另一个用于所有其他数字。如果虚拟机遇到我们没有计划作为操作码的数字,它将返回 IGL(非法的缩写 (short for Illegal))操作码,虚拟机将出现错误并停止。

最后需要指出的是,在 decode_opcode 函数中,我们将程序计数器(pc)递增了 1。我们这样做是因为一旦解码了操作码,我们就希望将计数器移动到下一个字节。

在本节结束时,猜猜我们应该添加什么?没错,测试!从上一节开始,我们有一个基本的测试,我将在下面重新粘贴,并添加两个更多:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_create_vm() {
        let test_vm = VM::new();
        assert_eq!(test_vm.registers[0], 0)
    }

    #[test]
    fn test_opcode_hlt() {
      let mut test_vm = VM::new();
      let test_bytes = vec![0,0,0,0];
      test_vm.program = test_bytes;
      test_vm.run();
      assert_eq!(test_vm.pc, 1);
    }

    #[test]
    fn test_opcode_igl() {
      let mut test_vm = VM::new();
      let test_bytes = vec![200,0,0,0];
      test_vm.program = test_bytes;
      test_vm.run();
      assert_eq!(test_vm.pc, 1);
    }
}

对于这个测试,我们可以手动创建一个 4 个字节的向量并运行循环,并检查 pc 是否递增。以后,我们将希望添加一个函数来允许执行一次迭代,以防止失败的测试无限循环。

原文链接及作者信息

成语及典故

  • 成语 & 典故:A Faustian Bargain: 浮士德式的交易 (A Faustian Bargain),这个概念源于德国关于浮士德的传说,浮士德为了追求知识和权力,与魔鬼签订了契约,以自己的灵魂作为交换。此外,这个词也可以用来形容人们为了获取某项服务或产品,可能需要牺牲一定的隐私权或其他权利的情形。

专有名词及注释

  • 操作码(Opcode):介于 0 和某个上限之间的整数,用于表示不同的操作。
  • 程序计数器(Program Counter, PC):用于跟踪正在执行的字节。
  • 汇编语言(Assembly Language):一种低级编程语言,用于编写机器语言指令,通常用于硬件级编程。
  • Rust 枚举(Rust Enums):Rust 语言中的一种类型,用于表示一组相关的值。