跳轉到內容

使用 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
華夏公益教科書