构建编译器的第一步:我们分两个主要阶段来应对这一挑战,在课本第 10-11 章中,我们将描述编译器的构造,旨在将高级语言的程序转换为中间代码;在第 7-8 章中,我们描述了后续翻译器的构建,旨在将中间代码翻译成目标硬件平台的机器语言。
期末复习,写个blog帮助自己梳理。课本用的是《The Elements of Computing Systems, second edition》
位于此编译器模型核心的中间代码 intermediate code 旨在运行在称为虚拟机或 VM 的抽象计算机上。与将高级程序直接翻译成机器语言的传统编译器相比,这种两层编译模型之所以有意义有几个原因:一是跨平台兼容性,由于可以在许多硬件平台上相对容易地实现虚拟机,因此相同的 VM 代码可以在配备此类 VM 实现的任何设备上运行。这就是 Java 成为为移动设备开发应用程序的主要语言的原因之一,移动设备的特点是有许多不同的处理器/操作系统组合。VM 可以通过使用软件解释器或专用硬件,或通过将 VM 程序翻译成设备的机器语言来在目标设备上实现。后一种实现方式被Java、Scala、C#和Python采用。
这篇blog将介绍典型的 VM 架构和 VM 语言,在概念上分别类似于 Java 虚拟机 (JVM) 和字节码 bytecode。虚拟机将从两个角度呈现。首先,我们描述 VM 的设计目的,从抽象层面入手。接下来,我们将描述虚拟机在 Hack 平台上的实现。我们的实现 implement 需要编写一个称为 VM 转换器的程序,该程序将 VM 代码转换为 Hack 汇编代码。
VM 语言由算术逻辑命令 arithmetic-logical commands 、称为 push 和 pop 的内存访问命令 memory access commands 、分支命令 branching commands 和函数调用和返回命令 function call-and-return commands 组成。
我们将这种语言的讨论和实现分为两部分,每部分都包含在单独的章节和项目中。在本文中,我们需要构建一个基本的 VM 转换器,它需要实现 VM 的算术逻辑和 push/pop 命令。 在下一章中,我们将扩展基本翻译器以处理分支和函数命令。结果将是一个完整的虚拟机实现,它将作为我们之后构建的编译器的后端。本文所讲的虚拟机是stack-base的。
一、The Virtual Machine Paradigm-
二、Stack Machine有效的 VM 语言力求在高级编程语言和各种低级机器语言之间取得操作便捷的平衡。因此,所需的 VM 语言应该满足来自上层和下层的几个要求。首先,语言要有合理的表达能力。我们通过设计一种具有算术逻辑命令、push/pop 命令、分支命令和函数命令的 VM 语言来实现这一点。换句话说,我们要确保一方面高层和VM层之间的翻译差距,另一方面VM层和机器层之间的翻译差距不会很大。满足这些冲突的要求的一种方法,是base the interim VM language on an abstract architecture called a stack machine。
用任何高级编程语言编写的任何程序都可以翻译成堆栈上的一系列操作。
push and pop:堆栈是一个顺序存储空间。堆栈支持各种操作,两个关键操作是push 和pop。push 操作是在堆栈顶部添加一个值,就像在一堆板的顶部添加一个板一样。 pop 操作会移除栈顶值,顶部堆栈元素之前的值会成为新的栈顶元素。
push/pop 逻辑会导致后进先出 (LIFO) 访问逻辑:弹出的值始终是最后一个被压入堆栈的值。事实证明,这种访问逻辑非常适合程序翻译和执行目的。
Stack Arithmetic:考虑通用操作 x op y,其中运算符 op 应用于操作数 x 和 y,例如 7+5、3-8 等。在 stack machine 中,每次 x op y 操作执行如下:首先,将操作数 x 和 y 从堆栈顶部弹出;接下来,计算 x op y 的值;最后,计算出的值被压入栈顶。同样,一元运算 op x 是通过将 x 从栈顶弹出,计算 op x 的值,最后将该值压入栈顶来实现的。例如,这里是如何处理加法和否定:
Virtual Memory Segments:
到目前为止,在我们的堆栈处理示例中,push/pop 命令只是在概念上得到了说明,使用语法 push x 和 pop y,其中 x 和 y 抽象地指代任意内存位置。我们现在开始正式描述我们的 push 和 pop 命令。
high-level programming language 具有符号变量,如 x、y、sum、count 等。如果语言是基于对象的,那么每个这样的变量都可以是类级别的静态变量、对象的实例级别的 field 或方法级别的局部变量或参数变量。在像 Java 的 JVM 这样的虚拟机和我们自己的 VM 模型中,没有符号变量。相反,变量表示为虚拟内存段中的条目,这些条目具有static、this、local 和argument 等名称。例如,如果局部变量 x 和 field y 分别映射到 local 1 和 this 3,那么像 let 这样的高级语句将被编译器翻译为 push this 3 和 pop local 1。总而言之,我们的 VM 模型具有八个内存段,其名称在图 7.4 中列出。
现在,我们观察到 VM 命令以完全相同的方式访问所有虚拟内存段:使用 segment name 后跟非负索引。
-
三、VM Specification, Part I我们的 VM 模型是 stack-based 的:所有 VM 操作都从堆栈中获取操作数,并将其结果存储在堆栈上。只有一种数据类型:有符号的 16 位整数。VM 程序是一系列 VM 命令,分为四类:
Push / pop 命令在堆栈和内存段之间传输数据。算术逻辑命令 Arithmetic-logical commands 执行算术和逻辑运算。分支命令 Branching commands 进行有条件和无条件的分支操作。函数命令 Function commands 进行函数调用和返回操作。
注释和空格:以 // 开头的行被视为注释并被忽略。空行也会被忽略。
在本 blog 中,我们关注算术逻辑和push/pop命令。 在课本下一章中再完成其余命令的规范。(很快就要复习这一章了)
command add、sub、eq、gt、lt、and 和 or 都需要操作数。为了执行它们中的每一个,VM 都需要从堆栈顶部弹出两个值计算,并将结果值压回到堆栈上。其余的 neg 和 not 命令只需要一个操作数并且以相同的方式工作。
-
四、ImplementationVM 翻译器是一个将 VM 命令翻译成机器语言指令的程序。 编写这样的程序需要两个主要任务。 首先,我们必须决定如何在目标平台上表示堆栈和虚拟内存段。其次,我们必须将每个 VM 命令转换成一系列可以在目标平台上执行的低级指令。
假设目标平台是典型的冯诺依曼机器。在这种情况下,我们可以使用 host RAM 中的指定内存块来表示 VM 的堆栈。这个 RAM 块的低端将是一个固定的基地址 base address,它的高端将随着堆栈的增长和收缩而变化。因此,给定一个固定的 stackbase 地址,我们可以通过跟踪单个变量(堆栈指针或 SP)来管理堆栈,它保存紧跟在堆栈顶部值之后的 RAM 条目的地址。 为了初始化堆栈,我们将 SP 设置为 stackbase。
因此,push x的指令可以被表示为 RAM[SP++] = x, 同样的,pop x则可以被表示为 x = RAM[--SP]。
Standard VM Mapping on the Hack Platform, Part IRAM 使用:主机 Hack RAM 由 32K 16 位字组成。 VM 实现应使用此地址空间的顶部,如下所示:
当我们说 segment 的基地址 base address 时,我们指的是 host RAM 中的物理地址。
Memory Segments Mapping:Local, argument, this, that: 这些段的基地址分别存储在寄存器 LCL、ARG、THIS 和 THAT 中。 因此,对虚拟段的第 i 个条目的任何访问(在 VM“push / pop segmentName i”命令中)都应该被转换为访问 RAM 中地址(base+i)的汇编代码,其中 base 是指针 LCL、ARG、THIS 或 THAT 之一。
Pointer 指针:与上述虚拟段不同,指针段正好包含两个值并直接映射到 RAM 位置 3 和 4。回想一下,这些 RAM 位置也分别称为 THIS 和 THAT。 因此,指针段的语义如下。 对指针 0 的任何访问都应导致访问 THIS 指针,对指针 1 的任何访问都应导致访问 THAT 指针。 例如,弹出指针 0 应将 THIS 设置为弹出值,而推送指针 1 应将 THAT 的当前值压入堆栈。
Temp:这个 8-word 的段也是固定的并直接映射到 RAM 位置 5 – 12。考虑到这一点,任何对 temp i 的访问,都应该被转换成访问 RAM 位置 5+i 。
Constant 常量:这个虚拟内存段是真正虚拟的,因为它不占用任何物理 RAM 空间。 相反,VM 实现通过简单地提供常量 i 来处理对常量 i 的任何访问。 例如,命令 push constant 17 应转换为将值 17 压入堆栈的汇编代码。
static 静态:静态变量映射到地址为 16 到 255的 host RAM 中。 VM 转换器可以自动实现这种映射。Hack 汇编器会将这些符号变量映射到 host RAM 上,从地址 16 开始。此约定将导致出现在 VM 程序中的静态变量按照它们在 VM 代码中出现的顺序映射到 16之后的地址上。 例如,假设一个 VM 程序以代码 push constant 100、push constant 200、pop static 5、pop static 2 开头。 上述转换方案将导致 static 5 和 static 2 分别映射到 RAM 地址 16 和 17。
VirtualMachine的实现代码我放这儿了



