What & How & Why

这是本文档旧的修订版!


Virtual Machine / Part I

Week 1 notes


VM(Virtual Machine)是 Jack 语言的中间层,其负责将 high-level 语言转化为汇编:

Complication Overview

We can only see a short distance ahead, but we can see plenty there that needs to be done. –Alan Turing

Two tiers compilation

为什么会有这种架构:

  • 计算器的构成不同,导致机器语言不同
  • 直接面向机器语言的编译器是 machine language depended,编译器实现不同。

就应用上来说,这种设计下的高级语言都有跨平台的优势。

Tier I: virtual machine

VM 的理念就是将所有的高级语言翻译为一种抽象化的代码:VM code(bytecode)。在这个层级中,编译器不关心具体的机器语言实现,而是将所有的计算机统一视作虚拟机(Virtual Machine

Tier II: virtual machine implementation

本层级负责具体的,基于计算机类型的实现。具体的工作是将虚拟机中的 bytecode 转化为基于特定机器语言的映射实现。

Jack compilation

  • Jack compiler: Jack 语言 → VM code
  • VM emulator:VMcode to PC & Hack computer

VM Abstraction: the Stack

VM code 有两点需要关注:

  • VM code 需要足够的接近高级语言层,这样编译器的实现会相对简单
  • VM code 也需要足够接近底层,这样 VM implementaion 也会相对简单

Stack machine abstraction

Stack machine abstraction 是一种抽象结构,能够在上述两点要求中找到较为平衡的位置。其包含架构部分和命令部分

Stack 的基础操作

Stack 由一块 RAM 和一个 Stack 组成。 Stack 拥有两种基础操作:

  • push:在最高点之上添加元素
  • pop:移除最高点的元素

Stack 的运算

基于上述两种基础操作,Stack 中定义了一系列算术运算。这些算术运算的基础流程为:

  • 将所有需要 argument 从 stack 中 pop 出来
  • 计算结果
  • 将结果 push 回 stack

比如下面的命令 add ,实质是将 stack 最上面的两层元素进行相加。其流程为:

  • 先将两个元素都 pop 出来
  • 进行加法运算
  • 在 push 到原有的 stack 里


常见的命令:

  • 算术:add / sub / neg
  • 逻辑:eq / gt / lt | and / or / not
  • 常见的命令分几个部分:算术 / 逻辑运算,内存相关运算,Branching & 函数相关运算
  • 这一系列的命令序列是基于高级语言的内容,通过编译器转化而来
  • 运算的算子按 top-most 的顺序来计算:也就是左算子是栈顶值,而右算子是栈顶的下一个值

可以看到一个有趣的现象,Stack 的结构非常利于将复合运算分解为子运算的过程,用序列化的指令表达出来。

VM Abstraction: Memory Segments

为什么需要 memory segment?

我们在编程中通常会根不同作用域的变量打交道。比如下面的 jack 语言:

static inst s1, s2;
function int bar(int x, int y)
{
    var int a, b, c;
    ...
    let c = s1 + y;
    ...
}
如果翻译为 VM code,这几个变量的作用域实际上完全不同。有些是需要在函数运行完毕后销毁的局部变量,而有些则是需要保存下来全局变量:
push s1 // global
push y // argument
add
pop c // local

Memory segments

本课的 VM 设计中提出了 memory segment 这一概念来解决该问题。其原理是将不同类型的变量存储到对应的 memory segment 中。比如上面的例子:


实际上通过 memory segment 对所有的变量进行了一次分组。因此对应的命令实际上可以改写为:

push static 0 // s1
push argument 1 // y
add
pop local 2 // c
实际上,VM 编译器无法识别变量名,在翻译的过程中,所有的变量名都会被解释为 segment + index 的形式。也就是说,stack 的命令对应的就是指定 segment 的某一个部分。因此,push 和 pop 的基础语义可以做进一步的延伸:

  • push:将指定内存区域的内容送到 stack 的栈顶
  • pop:将栈顶的元素移除出 stack,并送入到指定内存区域中

比如下面的例子:

push local 1 // 将 local[1] 的内容推到栈顶
pop local 1 // 将栈顶的内容移除,并存储到 local[1]
本课的 VM 提供了八种类型的 memory segments:
local // 局部变量
argument // 参数变量
this // 类对象
that // 
constant // 常量
static //

  • 注意 constant 后面的数字不是 index,而是对应的常数。 比如:push constant 1 意味着往栈顶加入常数 1
  • 双目运算的算子出栈后不会写回对应的 segment