Swift中的LC-3 VM

我遵循了“编写您自己的虚拟机”教程来编写VM。 我决定用Swift编写它,因为我已经有一段时间没有写Swift了,并且错过了编写Swift程序的机会。

可以在此处查看该项目的完整源代码。

在几年前阅读Nand2Tetris书和课程之前,我只写过一个VM,也曾是一个玩具,因此早就忘记了一些细节,这就是为什么这次练习是一个很好的复习。

您会注意到代码没有经过优化,但是可以正常工作,到目前为止已经足够了。
存储器和寄存器均表示为UInt16数组。 我本可以直接在内存中使用指向UInt16值的指针,这可能会更快,更优雅。

这是我在实施此项目时(重新)学习的一些有趣的事情。

标志延伸

LC-3 VM使用无符号的16位整数。 但是,某些指令(例如下面描述的ST(存储)指令)要求对一个无符号的16位整数与另一个少于16位的整数进行算术运算。 ST操作通过将寄存器SR指定的内容存储在通过将程序计数器偏移量(PCOffset)位0到8加到程序计数器(PC)的当前值而指定的存储位置中来进行。 PC与LC8机器中的所有其他寄存器一样,都是16位值,而PCOffset是9位宽。 为了能够通过两个值的加法运算,必须对PCOffset值进行符号扩展。 如果PCOffset的最高有效位为0,我们只需在PCOffset的左侧填充0,直到其长16位。 如果MSB为1,则PCOffset用1填充。

值的符号扩展基本上增加了二进制数的位数,同时保留了其符号(正/负)。

Swift中的溢出添加

与在C语言中在Swift中添加两个UInt16整数(或任何无符号整数)不同,C语言没有自动溢出处理功能。
例如,以下代码将在Swift中给出EXC_BAD_INSTRUCTION错误:

let n: UInt16 = UInt16(UINT16_MAX) print(n + 5) //EXC_BAD_INSTRUCTION 

为了选择无符号整数的溢出行为,必须使用溢出加法(或减法或乘法)运算符&+

 let n: UInt16 = UInt16(UINT16_MAX) print(n &+ 5) //4 

终端模式

终端对stdin的默认行为是对键盘输入进行缓冲和预处理,直到遇到新行\n ,然后才将其传递给正在运行的程序。 此模式称为熟模式或规范模式。
要为VM实现checkKeyBoard()函数,我需要将每个单独的按键传递给我的程序,而无需任何缓冲并等待输入新的换行符。 这要求将终端设置为原始或非规范模式。

我们可以通过使用tcgetattr()读取终端的属性并将其修改为一个结构,然后将该修改后的结构传递到tcsetattr()来设置终端的属性。

到大端

编写此VM可以使您对字节序进行重新整理。 它提醒我,字节序是指整数字的各个字节的顺序(虽然字节序可以指的是单个位,但在实践中极为罕见)。
LC-3 VM程序是大端的。 大多数现代CPU体系结构都使用低位字节序。 为了使VM在小端序计算机上正常工作,我们必须将每个读入的16位字的字节序从大字节序转换为小字节序表示。

内存映射寄存器

我想起了在实施该项目时内存映射寄存器的存在。 顾名思义,这些寄存器在寄存器表中不可用,而是在内存中具有特殊地址。 只需将它们视为常规存储器地址,就可以读取和写入这些寄存器。
在LC-3机器中,键盘状态寄存器(KBSR)和键盘数据寄存器(KBDR)是两个这样的内存映射寄存器。 KBSR指示键盘上的击键是否已注册。 KBDR保存已按下的键的值(在我们的示例中为ASCII码)。 在VM while循环的每次迭代中都会对寄存器进行轮询,以检查击键情况。

编写此VM非常有趣,而且很有教育意义。 如果您有时间,我绝对建议您自己写一个,以加深对一些底层硬件概念的理解。