菜单
本页目录

04-跳转操作 - 你想构建一个语言虚拟机吗?

随意跳转 (Jump Around)

上次我们离开勇敢的教程追随者时,我们有了一个可以进行加、减、乘、除运算的简单虚拟机。这一切都很好,但我们需要更多的功能而不仅仅是这些。在本节中,我们将添加一些与跳转相关的指令。

跳转类型

有几种非常常见的跳转类型:

  1. 绝对跳转 (Absolute)
  2. 相对前进跳转 (JMPF - Relative Forward)
  3. 相对后退跳转 (JMPB - Relative Backward)

绝对跳转

这些将程序计数器更改为参数指定的任何值,而不考虑其他任何内容。在最简单的形式中,这条指令通常看起来像这样:

...<指令>...
JMP 100
...<指令>...

每当程序计数器遇到 JMP 指令时,程序计数器就设置为 100,并从那里继续执行。这种方法的一个问题是我们不能跳转到超出指令中使用的位数所能表示的最大数值。由于我们有一个 32 位的指令,其中 8 位被 JMP 指令占用,这给了我们 2^24,或 16,777,216。

一个替代方案是有一个跳转指令,它从寄存器中读取目标。这允许跳转到 2^32 个地址。这将是我们将要实现的第一个 JMP 版本。

相对跳转

这些相对于当前程序计数器向前或向后跳转特定数量的指令。这些对于实现循环非常有用。

跳转到标签

我们还没有太多讨论标签。将其视为用名称标记特定字节,并不需要担心要跳转到哪个数字,你可以直接跳转到该标签。当我们编写汇编器 (assembler) 时,我们将更深入地讨论这一点。

实现绝对跳转

让我们先做这个。首先,打开 instruction.rs 文件并添加操作码。我们将称之为 JMP。

#[derive(PartialEq)]
pub enum Opcode {
    LOAD,
    ADD,
    SUB,
    MUL,
    DIV,
    HLT,
    JMP,
    IGL
}

现在我们需要在 vm.rs 中的执行函数中添加一些代码:

Opcode::JMP => {
    let target = self.registers[self.next_8_bits() as usize];
    self.pc = target as usize;
},

使用这个的一个示例可能看起来像这样:

LOAD $0 #0
JMP $0

这将导致无限循环。糟糕。

测试

让我们在 vm.rs 中为这条指令添加一个测试:

#[test]
fn test_jmp_opcode() {
    let mut test_vm = get_test_vm();
    test_vm.registers[0] = 1;
    test_vm.program = vec![7, 0, 0, 0];
    test_vm.run_once();
    assert_eq!(test_vm.pc, 1);
}
重要如果你已经更改了 instruction.rs 中每个操作码关联的数字,那么 vec![7, 0, 0, 0]; 将不起作用。将 7 替换为你的 JMP 操作码的整数。

我们获取测试虚拟机,设置寄存器 0 为持有 JMP 目标(字节 4),制作一个小程序,运行一条指令,并确保程序计数器如预期那样改变。

实现相对跳转

JMP 指令从虚拟机的角度更改程序计数器;相对跳转指令相对于当前指令更改程序计数器。

我们可以用两种方式实现这一点。一种可能的语法是:

  • JMP -5:向后跳转 5 个位置。

另一个是:

  • JMPB 5: 向后跳转 5 个位置。

如果我们在指令中隐含方向,那么我们当然需要两个指令。如果我们想用一个操作码,那么我们不得不接受汇编器中的 - 符号,并在我们的字节码中表达它。现在,两个指令最容易,所以我们选择这个。

我们的两个新指令将是 JMPFJMPB,分别用于向前跳转和向后跳转。它们每个都是 1 个参数 指令,该参数是存储要移动的字节数的寄存器编号。

向前看,我不会详细介绍添加新操作码的重复部分。这里是步骤的简短列表:
1. 在 instruction.rs 中的 enum 中添加新的 Opcode
2. 在 instruction.rs 中的 From<u8> 实现中添加新的 Opcode
3. 在 vm.rs execute_instruction 函数的匹配分支中添加执行指令所需的代码
4. 在 vm.rs 中添加测试

两个指令的完整代码都在 GitLab 仓库中。我将在这里展示 JMPF 的功能代码:

Opcode::JMPF => {
    let value = self.registers[self.next_8_bits() as usize]
    self.pc += value;
}

然后让我们编写一个测试:

#[test]
fn test_jmpf_opcode() {
    let mut test_vm = get_test_vm();
    test_vm.registers[0] = 2;
    test_vm.program = vec![8, 0, 0, 0, 6, 0, 0, 0];
    test_vm.run_once();
    assert_eq!(test_vm.pc, 4);
}

完成

现在只需实现 JMPB,我们就有了一系列良好的跳转操作。接下来,我们将添加一些等式测试指令!

原文链接及作者信息

成语及典故

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

专有名词及注释

  • 绝对跳转(Absolute Jumps):将程序计数器更改为参数指定的任何值,而不考虑其他任何内容。
  • 相对跳转(Relative Jumps):相对于当前程序计数器向前或向后跳转特定数量的指令。
  • 汇编语言(Assembly Language):一种低级编程语言,用于编写机器语言指令,通常用于硬件级编程。
  • Rust 枚举(Rust Enums):Rust 语言中的一种类型,用于表示一组相关的值。