菜单
本页目录

00-计算机硬件速成课 - 你想构建一个语言虚拟机吗?

嗨!这是一系列文章的序章,详细介绍了如何构建一个语言虚拟机(VM)。如果你熟悉寄存器、程序计数器和汇编语言等术语,可以跳过这篇文章。如果不是,继续阅读。请注意,这绝不是全面的,但足以理解我们正在构建的内容。

虚拟机 (VM - Virtual Machine)

虚拟机的概念

虚拟机(Virtual Machine, VM)是一种抽象的计算机系统,它可以模拟真实计算机的运行。虚拟机通常由以下几个部分组成:

  • 指令集:定义了虚拟机可以执行的操作。
  • 寄存器:用于存储数据和指令。
  • 内存:用于存储程序和数据。
  • 执行引擎:负责解释和执行指令。

设计目标

在设计我们的虚拟机时,我们设定了以下目标:

  • 简单性:保持设计简洁,便于理解和实现。
  • 可扩展性:允许未来添加新的功能和优化。
  • 性能:虽然不是首要目标,但仍然要保证虚拟机的执行效率。

实现步骤

以下是构建虚拟机的基本步骤:

  1. 定义指令集:确定虚拟机需要支持的操作。
  2. 设计寄存器:选择合适的寄存器数量和类型。
  3. 实现内存管理:设计内存模型和内存分配策略。
  4. 编写执行引擎:将指令集映射到具体的操作。
  5. 测试和优化:确保虚拟机的稳定性和性能。

什么是语言虚拟机 (language virtual machine)?

你知道你可以输入 python script.py 然后魔法就发生了吗?那就是 Python 虚拟机,或者说语言解释器,它读取你编写的源代码,将其翻译成 Python VM 可以理解的字节码,然后执行它。

我交替使用术语 语言解释器语言 VM。我会尽量保持一致,但我也会尽力抵抗无人看管的果冻甜甜圈。

请确保你安装了 C 编译器!GCC 或 clang 是不错的选择。

注意有些代码故意没有优化,这样我们稍后可以回来学习基准测试和优化 VM。

什么是程序?

像弗利萨一样,一个 程序 有多种形态。当你开始编写一个时,你写的文本看起来像这样:

#include <stdio.h>
int main (void) {
  printf("Hello World!");
  return 0;
}

你的 CPU 对此一无所知。我们必须将这段文本转换成 CPU 可以理解并采取行动的二进制形式。这个过程(或一系列过程)通常被称为 编译,需要更多的步骤。

下一步

所有的处理器都有自己的语言,它们能够理解。这通常被称为汇编代码 (assembly code),并且高度特定于处理器。你的 iPhone 或 Galaxy C4 Boom 版能理解的汇编语言,对你在 NewEgg 上购买的便宜 AMD 处理器来说是不可理解的,而你一点也不后悔那个决定。

你可以直接编写汇编代码,尽管这在现代很少见。这很繁琐且令人烦恼,就像一集《老友记》一样。你的朋友编译器可以接收你的源代码,并为你生成汇编代码。让我们将我们之前的 C 代码示例保存到一个名为 01_c_hello_world.c 的文件中:

#include <stdio.h>
int main (void) {
  printf("Hello World!");
  return 0;
}

编译器

将这个保存在你的磁盘上的某个地方。现在,在终端中运行:

gcc -S 01_c_hello_world.c

你应该在 .c 文件旁边有一个同名但扩展名为 .s 的文件。让我们看看里面是什么……

cat /path/to/01_c_hello_world.s

你应该看到以下版本的汇编代码:

.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 13
.globl _main                   ## -- Begin function main
.p2align 4, 0x90
_main:                                  ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Lcfi0:
.cfi_def_cfa_offset 16
Lcfi1:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Lcfi2:
.cfi_def_cfa_register %rbp
subq $16, %rsp
leaq L_.str(%rip), %rdi
movl $0, -4(%rbp)
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -8(%rbp)          ## 4-byte Spill
movl %ecx, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc
                                      ## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
.asciz "Hello World!"

不要惊慌! 你不需要知道所有这些意味着什么,我们也不会编写这些。这是为了展示汇编语言是什么样子。

汇编器

一旦我们有了汇编代码,我们可以将其转换成一个 对象 (object) 文件。

要看到汇编器在行动,你可以运行:

gcc -c 01_c_hello_world.s -o 01_c_hello_world.o

你现在应该看到一个名为 01_c_hello_world.o 的第三个文件。目录应该看起来像这样:

$ ls
01_c_hello_world.c 01_c_hello_world.o 01_c_hello_world.s

对象文件包含机器代码和元数据。另一个程序,一个 链接器 (linker),然后读取对象文件,链接所需的库,并生成一个可执行文件。

进入 Java JVM、.NET CLR 和其他语言 VM

Java 刚出现时,用于市场推广的一个优势是“一次编写,到处运行”的承诺。也就是说,你编写的 Java 代码可以在任何能够运行 JVM 的硬件平台上运行,无需修改。这意味着人们只需要关心 一个 程序,JVM,在他们的硬件上运行,而 Sun Microsystems(后来的 Oracle)会处理这部分。

其他语言遵循这种模式:.NET CLR、Python、Ruby、Perl 等等。

注意你知道吗,女性是最早的程序员?早期计算机的硬件部分被视为计算机的 男性 部分:转动旋钮,摆弄电路等。编写代码被视为更多的秘书工作。没有她们,我们的世界就不会像今天这样存在。我强烈推荐你阅读以下人物的故事:Ada Lovelace、Grace Hopper 和 Katherine Johnson。

浮士德式的交易

虽然这些 VM 提供了服务(硬件抽象、垃圾回收等),但这一切都有代价:执行速度更慢,资源消耗更高。一般来说,运行在 VM 上的语言比编译到特定硬件上运行的语言执行速度更慢。

注意是的,这里还有很多其他话题,比如 JIT 编译器、本地代码扩展等。我现在要跳过这些。

寄存器

这篇文章最后要介绍的概念是 寄存器 (register)。在 CPU 上,寄存器是存储数据的特殊区域。为了更详细的解释,我将从 Wikipedia 引用:

在计算机架构中,处理器寄存器是计算机的中央处理单元(CPU)可用的快速访问位置。寄存器通常由一小段快速存储组成,尽管一些寄存器具有特定的硬件功能,并且可能是只读或只写的。寄存器通常通过不同于主存储器的机制进行寻址,但在某些情况下,可能会被分配一个内存地址,例如 DEC PDP-10、ICT 1900。

— Wikipedia

当你的 CPU 执行代码将一个变量设置为数字 5 时,那个 5 可能会被加载到某个寄存器中。我们假装是 CPU 的应用程序也将有它可以使用的寄存器。

总结

我们将编写一个假装是 CPU 的应用程序,并执行我们为它编写的程序。当然,这意味着我们还得发明一种语言。但我们会在后面的部分讲到这些。

原文链接及作者信息

成语及典故

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

专有名词及注释

  • 虚拟机(Virtual Machine, VM):一种抽象的计算机系统,可以模拟真实计算机的运行。
  • 指令集(Instruction Set):定义了虚拟机可以执行的操作。
  • 寄存器(Register):用于存储数据和指令。
  • 内存(Memory):用于存储程序和数据。
  • 执行引擎(Execution Engine):负责解释和执行指令。