菜单
本页目录

01-构建语言虚拟机概述及简单虚拟机 - 你想构建一个语言虚拟机吗?

引言

嗨!这是关于用 Rust 构建编程语言的 VM/解释器系列文章的第一篇。这是我几个月来一直在琢磨的事情,所以现在把它们变成了一个教程系列。

为什么?

这个话题一直是计算机科学中让我感兴趣的领域。去年我开始编写 Elixir 和 Erlang 代码时,我对 BEAM 虚拟机印象深刻。不仅仅是因为它执行代码的能力,还因为它作为应用程序的命令和控制平台的能力,内置的集群功能等等。我来自基础设施背景,我的一个宠物理论是,我们依赖像 Docker 这样的工具的很多东西,可以通过语言虚拟机更好地提供。

灵感和致谢

在我研究这个项目时,我大量参考了一些网站、书籍和项目:

  1. Thorsten Ball 有一本很棒的书,《用 Go 编写解释器》,它带你逐步编写一个树遍历解释器。它全面、写得好,是这个话题的一个很好的介绍。

  2. BEAM 虚拟机 (https://en.wikipedia.org/wiki/BEAM_(Erlang_virtual_machine))

  3. Lua 虚拟机 (http://files.catwell.info/misc/mirror/lua-5.2-bytecode-vm-dirk-laurie/lua52vm.html) 和 (http://luaforge.net/docman/83/98/ANoFrillsIntroToLua51VMInstructions.pdf)

  4. Wren 虚拟机 (https://github.com/munificent/wrenhttp://wren.io)

  5. Dis 虚拟机 (http://www.vitanuova.com/inferno/papers/dis.html)

代码

目标

我们希望我们的虚拟机能够:

  1. 与现代实现相比具有合理的性能。我们将以 Python 作为比较。

  2. 容错性。

  3. 作为运行应用程序的命令和控制平台。

  4. 在不同物理服务器上运行的虚拟机的集群。

类型

解释器分为三大类:

  1. 树遍历 (Tree-Walking)

  2. 基于栈的 (Stack-Based)

  3. 基于寄存器的 (Register-Based)

重要如果你想要复习一下 CPU 是什么,汇编语言等,可以查看 https://blog.subnetzero.io/post/building-language-vm-part-00/

树遍历解释器

树遍历解释器将源代码转换为类似树的数据结构,从树的根部开始访问每个节点,一边执行操作。这些通常是人们编写的第一个解释器。

优点

  • 简单

  • 允许更大的灵活性来进一步转换代码,因为它已经在一个数据结构中

缺点

  • 与其他类型相比较慢

基于栈的解释器

这是最常见的。JVM 是一个,Python VM 是另一个。这些虚拟机将操作和结果存储在栈上。当虚拟机执行一个指令时,它会从栈上弹出最近的值,执行操作,并将结果推回栈上。

优点

  • 比基于寄存器的虚拟机更容易编码

  • 性能不错

缺点

  • 不映射到真实硬件;CPU 使用寄存器。

  • 大多数任务需要更多的指令

基于寄存器的解释器

这是最后一种类型,也是最不常见的。例子包括 BEAM 虚拟机(有时称为 Erlang 虚拟机)、Lua 虚拟机和 Dalvik 虚拟机(在 Android 上运行 Java 代码的东西)。

优点

  • 基于寄存器的虚拟机更接近实际硬件的工作方式

  • 性能更好

缺点

  • 编码更复杂

  • 编写编译器时,我们必须担心寄存器的分配和释放

所以我们要编码……哪一个?

……一个基于寄存器的虚拟机!

为什么?它们对我来说更有趣。互联网上有很多关于如何实现树遍历或基于栈的虚拟机的教程。基于寄存器的虚拟机的教程要少得多。

别担心,这会很有趣!=)

汇编

从原始的 0 和 1 抽象出来的下一步是语言。这里是计算机语言的一个大致的层次结构/时间线:

  1. 二进制 (binary)

  2. 汇编 (assembly)

  3. FORTRAN、C 和后来的语言,如 Rust

    • 当它们刚出现时被称为“高级语言”,我们现在通常称它们为低级语言。
  4. Perl、Bash、Python、Ruby 和后来的语言,如 Go

    • 这些是“非常高级别的语言”

    • 是的,Go 也可以归入 C 级语言部分

  5. SQL 和其他 DSL(领域特定语言)

    • 还有 Excel 宏和其他噩梦般的恐怖

在完成这个项目的过程中,我们将创建一种汇编语言和汇编器,这样我们就可以不用十六进制编辑器来编写程序了。之后,我们将研究一种更高级别的语言,它可以编译成我们的汇编语言,然后是我们的字节码。

如果你有一些汇编语言的经验,要知道我们将编写的是一种 RISC 风格的,受 MIPS 启发的汇编,因为它看起来最简单。

汇编指令的结构

我们的虚拟 CPU 将能够一次处理 32 位数据,执行它,然后去获取另外一组 32 位数据。在非常一般的水平上,这就是所有硬件处理器所做的:

  1. 读取下一个指令

  2. 执行指令

  3. 重复

分解很难

我们可以将我们的 32 位指令分解成更小的块。这让我们可以在一行中放入多个参数:

LOAD $0 #500

像下面这样是不必要的,没有必要换行:

LOAD
$0
#500
我们现在要处理的最小单位是 8 位,所以是单个字节。稍后的增强将让我们使用更奇特的位数。

指令

所以,快速回顾一下术语。32 位的一组是一个 指令。这 32 位中的前 8 位将包含我们的 操作码。剩下的位将专门用于 操作数。有了这个设计,一个 操作码 可以有 0、1、2 或 3 个 操作数

下表显示了我们将要使用的所有排列组合:

操作码 (8 位)
操作码 (8 位)操作数 1 (24 位)
操作码 (8 位)操作数 1 (8 位)操作数 2 (16 位)
操作码 (8 位)操作数 1 (8 位)操作数 2 (8 位)操作数 3 (8 位)

这显示了我们可以使用 32 位指令的所有方式。HLT 指令(或 操作码(opcode)),它为停止执行,没有操作数,所以我们使用 1 个 8 位操作码。剩余的 24 位将被清零。在整个教程中,我将以十六进制 (hex) 显示字节码,如下所示:

HLT Example:

0x06 00 00 00

HLT 只是数字 6,我们不需要操作数,所以这是我们的虚拟机可以执行的一个完整指令。其他一切都只是在这上面构建。

开始项目

让我们通过 cargo 创建一个新的 Rust 项目。本教程的其余部分假设你已经安装了最新版本的 Rust,并且 cargo 可执行文件在你的 PATH 上。如果没有,请访问 https://rustup.rs/ 进行设置。

cargo new iridium --bin

跳到新创建的 iridium 目录,并在 src/ 目录下创建一个名为 vm.rs 的新文件:

cd iridium
touch src/vm.rs

你可以在你喜欢的任何编辑器中打开 vm.rs(当然应该是 vim)。

在代码中

好的,让我们开始编码吧!记住,我们所做的只是编写一个模拟硬件 CPU 功能的软件。所以让我们从一个简单的结构开始。

首先,让我们给我们的虚拟机一些寄存器。我们可以用一个数组来表示寄存器:

pub struct VM {
    registers: [i32; 32]
}

为什么使用数组而不是向量?因为我们在编译时就知道我们需要的数量。

然后在结构体的实现块中:

impl VM {
    pub fn new() -> VM {
        VM {
            registers: [0; 32]
        }
    }
}

这将在我们创建虚拟机时将所有寄存器初始化为零。

测试

在我们进一步之前,让我们编写一些测试。毕竟,我们不想成为那些从不编写测试的 那些 开发人员,对吧?

src/vm.rs 的末尾,放入以下内容:

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

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

接下来,在 src/main.rs 的顶部添加以下内容:

pub mod vm;

(你也可以清空 main 函数的默认打印 Hello World!

你现在应该能够在主目录中运行 cargo test 并看到一个测试运行并通过。 耶!我们在路上!在下一节中,我们将做我们的第一个操作码!

原文链接及作者信息

成语及典故

  • 成语 & 典故:A Faustian Bargain: A Faustian Bargain (浮士德式的交易)。这个概念源自德国关于浮士德的传说,浮士德与魔鬼交易他的灵魂以换取知识和力量。此外,这个术语还可以描述人们可能不得不牺牲某些权利或隐私以获得服务或产品的情况。

专有名词及注释

  • 树遍历解释器(Tree-walking Interpreter):一种解释器,它将源代码转换为树状数据结构,从树的根部开始访问每个节点,一边执行操作。
  • 基于栈的解释器(Stack Based Interpreters):最常见的解释器类型,如 JVM 和 Python VM,使用栈来存储操作和结果。
  • 基于寄存器的解释器(Register Based Interpreters):较少见的解释器类型,更接近硬件的工作方式,如 BEAM VM、Lua VM 和 Dalvik VM。
  • 汇编语言(Assembly Language):一种低级编程语言,用于编写机器语言指令,通常用于硬件级编程。
  • RISC 风格(RISC-style):精简指令集计算机风格,以简单、统一的指令集为特点,如 MIPS 架构。