跳转到内容

使用 Swift 创建虚拟机/寄存器虚拟机

来自维基教科书,开放书籍,开放世界

第一个示例将是最简单的体系结构之一,它将包含以下组件

  1. 一组寄存器(我将任意选择拥有 4 个寄存器,编号从 0 到 3)。寄存器充当一组读/写内存单元。
  2. 一个程序,它是一个只读的 VM 指令序列
  3. 一个执行单元来运行程序。执行单元将按顺序读取每个指令并执行它。

寻址模式

[编辑 | 编辑源代码]

在任何汇编语言中,数字都具有多种用途。除了表示标量值外,它们还表示寄存器编号和内存位置。在本例中,我将遵循使用前缀字符来表示数字所扮演的角色的约定。

  • 寄存器编号以字母r开头,例如r0r1r2
  • 立即(标量)值以井号#开头,例如#100#200
  • 内存地址以符号@开头,例如@1000@1004

指令集

[编辑 | 编辑源代码]

设计完 VM 后,有必要设计 VM 的指令集。指令集只是我们希望用此 VM 做(或允许其他人做)的事情的反映。以下是一些可以使用此 VM 做的事情

  • 将一个立即数(常量)加载到一个寄存器中。
  • 对两个寄存器执行算术求和(即,将两个数字相加)。
  • 停止机器。

让我们现在就让它保持简单。在我们拥有一个工作的实现后,我们可以返回并添加更多指令。

首先,让我们使用这三个指令编写一个简短的程序,以了解它们可能是什么样子

loadi r0 #100
loadi r1 #200
add r2 r0 r1
halt

如果 VM 要运行该程序,则以下是对每行发生的事情的描述

  1. 将立即值 100 加载到寄存器r0中。请注意,将值放入寄存器中通常称为加载操作。
  2. 将立即值 200 加载到寄存器r1中。
  3. 将寄存器r0r1的内容相加,并将总和放入寄存器r2中。
  4. 结束程序并停止 VM。

请注意,为了遵循通用约定,我选择在操作数列表中首先放置目标寄存器。

指令码

[编辑 | 编辑源代码]

创建汇编语言后,有必要确定如何将每个指令表示为一个数字。这在汇编语言中的每个指令和指令码集中的每个指令码之间建立了一一对应关系。将程序从汇编语言转换为指令码称为汇编,从指令码转换回汇编语言称为反汇编

此时我们必须做出几个选择

  • 使用什么数字来表示每个汇编语言指令?
  • 如何编码指令操作数?
  • 操作数是指令字的一部分(请记住,我用表示数字),还是单独的字(数字)?

首先,为了回答最后一个问题,由于此 VM 中只有少量指令和寄存器,因此即使(为了简单起见)我要使用 16 位指令字,也不应该很难将所有操作数编码到单个指令字中。因此,用十六进制写的一个 16 位数字有 4 位数字,这让我们可以轻松地访问 4 个信息字段,每个字段包含 16 个变化(0-9 和 A-F)。

  1. 机器字的第一位将是指令编号。这使得我们的 VM 能够拥有最多 16 种不同的指令。按照现代标准来说,这很少,但对于我们的示例虚拟机来说已经足够了。
  2. 接下来的三位数字将用于操作数。这些可以作为三个 1 位操作数、两个 1 位和 2 位操作数或单个 3 位操作数使用。

做出这些决定后,现在让我们建立编码。回想一下,我们有 16 个可用的指令编号。

halt指令将是指令 0,选择 0 作为此指令有一个重要的原因。由于计算机内存中的空闲空间很可能被 0 填充,因此任何失控的程序最终都会遇到一个 0 并尝试执行此指令,立即停止程序。

剩下的两个指令可以任意分配:loadi指令可以是指令 1,add指令可以是指令 2。这是我们当前的指令编码列表

0 = halt
1 = loadi
2 = add

检查第一个程序指令,我们发现现在必须编码寄存器和立即值

loadi r0 #100

有三个十六进制数字可用于操作数,因此我们将这三个中的第一个用作寄存器编号,并将第二个和第三个一起用作立即值。现在我们刚刚确定了loadi指令的完整编码

bits 15-12 = 1
bits 11- 8 = register number
bits  7- 0 = immediate value

此指令的寄存器编号为 0,立即值为 100,十六进制为 64(6 x 16 + 4)。因此,第一个指令的完整 16 位十六进制指令码为 1064。

第二个指令以类似的方式组装。立即值为 200,十六进制为 C8。第二个指令组装为 11C8。

第三个指令是 2201。

最后一个指令是 0。

将这些指令放在一起,我们得到这些 4 个 16 位十六进制数字作为完整的程序

1064
11C8
2201
0000

现在是时候实现这种设计了。这是 swift 的实现

  • 这是一种非常简单、现代且简洁的语言。
  • 它既有低级特性,也有高级特性。

VM 的主要功能将是一个run函数,该函数在循环内按顺序执行以下步骤。循环重复,直到执行halt指令。

  1. 从程序中获取下一条指令。
  2. 将指令解码为其组成部分。
  3. 执行解码后的指令。

让我们开始编写程序。

寄存器

[编辑 | 编辑源代码]

第一个也是最简单的要实现的部分是 4 个寄存器的集合。

let NUM_REGS = 4
var regs = [Int](repeating: 0, count: NUM_REGS)

此 VM 中的寄存器是带符号整数,其宽度取决于您的计算机和操作系统,分别是 16 位、32 位或 64 位。对于本例,确切的大小无关紧要。

程序指令

[编辑 | 编辑源代码]

完全组装的程序可以很容易地存储在整数数组中。

let prog = [0x1064, 0x11c8, 0x2201, 0x0000]

fetch函数从程序中检索下一个字。为了做到这一点,我们必须引入一个称为程序计数器(有时也称为指令指针)的变量。在从函数返回指令字之前,必须递增程序计数器,使其引用程序数组中的下一条指令。

var pc = 0  // program counter

func fetch() -> Int {
    let instruction = prog[pc]
    pc += 1
    return instruction
}

decode 函数会完全解码每条指令。它会确定每条指令的三个操作数寄存器和立即值,即使指令没有使用该部分。这实际上使函数更简单。指令编号和操作数存储在变量中。

var instrNum = 0
// operands:
var reg1     = 0
var reg2     = 0
var reg3     = 0
var imm      = 0

// decode a word
func decode(instr:Int) -> Void {
    instrNum = (instr & 0xF000) >> 12
    reg1     = (instr & 0xF00 ) >>  8
    reg2     = (instr & 0xF0  ) >>  4
    reg3     = (instr & 0xF   )
    imm      = (instr & 0xFF  )
}

execute 函数实际上执行指令。首先需要一个 running 标志,halt 指令可以将其设置为 0。

// the VM runs until this flag becomes 0
var running = 1

现在是 eval 函数。它只是一个 switch 级联。此函数使用 decode 函数将值存储到的指令代码和操作数变量。注意,在每个指令 case 中,我都会显示该指令。这使在程序运行时更容易跟踪程序。

// evaluate the last decoded instruction
func eval() -> Void {
    switch instrNum {
    case 0:
        // halt
        print("halt")
        running = 0
    case 1:
        // loadi
        print("loadi r\(reg1) #\(imm)")
        regs[reg1] = imm;
    case 2:
        // add
        print("add r\(reg1) r\(reg2) r\(reg3)");
        regs[reg1] = regs[reg2] + regs[reg3]
    default:
        print("oh-oh");
    }
}

run 函数非常简单:获取,然后解码,然后执行。

func run() -> Void {
    while running != 0 {
        let instr = fetch()
        decode(instr: instr)
        eval()
    }
}

入口点

[编辑 | 编辑源代码]

入口点是 run 函数

run()

完整程序源代码

[编辑 | 编辑源代码]

为了方便起见,我已经将完整的程序源代码包含在一个代码块中。请注意,我添加了 formatRegistershowRegs 函数,以便查看指令之间所有寄存器的值。这使我能够验证虚拟机和汇编程序是否正常工作。

let NUM_REGS = 4
var regs = [Int](repeating: 0, count: NUM_REGS)

let prog = [0x1064, 0x11c8, 0x2201, 0x0000]

var pc = 0  // program counter

func fetch() -> Int {
    let instruction = prog[pc]
    pc += 1
    return instruction
}

var instrNum = 0
// operands:
var reg1     = 0
var reg2     = 0
var reg3     = 0
var imm      = 0

// decode a word
func decode(instr:Int) -> Void {
    instrNum = (instr & 0xF000) >> 12
    reg1     = (instr & 0xF00 ) >>  8
    reg2     = (instr & 0xF0  ) >>  4
    reg3     = (instr & 0xF   )
    imm      = (instr & 0xFF  )
}

// the VM runs until this flag becomes 0
var running = 1

// evaluate the last decoded instruction
func eval() -> Void {
    switch instrNum {
    case 0:
        // halt
        print("halt")
        running = 0
    case 1:
        // loadi
        print("loadi r\(reg1) #\(imm)")
        regs[reg1] = imm;
    case 2:
        // add
        print("add r\(reg1) r\(reg2) r\(reg3)");
        regs[reg1] = regs[reg2] + regs[reg3]
    default:
        print("credit-card #");
    }
}

func formatRegister(reg:Int) -> String {
    let hex = String(reg, radix: 16, uppercase: true)
    let paddingCharacter = "0" as Character
    let padding = String(repeating: paddingCharacter, count: 4 - hex.count)
    return padding + hex
}

func showRegs() -> Void {
    print("regs = ", terminator:"")
    print(regs.map(formatRegister).joined(separator: " "))
}

func run() -> Void {
    while running != 0 {
        showRegs()
        let instr = fetch()
        decode(instr: instr)
        eval()
    }
    showRegs()
}

run()

示例运行

[编辑 | 编辑源代码]

以下是程序的输出

regs = 0000 0000 0000 0000 
loadi r0 #100
regs = 0064 0000 0000 0000 
loadi r1 #200
regs = 0064 00C8 0000 0000 
add r2 r0 r1
regs = 0064 00C8 012C 0000 
halt
regs = 0064 00C8 012C 0000
华夏公益教科书