用 Lua 實現一個微型虛擬機-基本篇

用 Lua 實現一個微型虛擬機-基本篇

目錄

介紹

在網上看到一篇文章 使用 C 語言實現一個虛擬機, 這裏是他的代碼 Github示例代碼, 以爲挺有意思, 做者用不多的一些代碼實現了一個可運行的虛擬機, 因此打算嘗試用 Lua 實現一樣指令集的虛擬機, 同時也仿照此文寫一篇文章, 本文中大量參考引用了這位做者的文章和代碼, 在此表示感謝.html

準備工做:

  • 一個 Lua 環境
  • 文本編輯器
  • 基礎編程知識

爲何要寫這個虛擬機?

緣由是: 頗有趣, 想象一下, 作一個很是小, 可是卻具有基本功能的虛擬機是多麼有趣啊!git

指令集

談到虛擬機就不可避免要提到指令集, 爲簡單起見, 咱們這裏使用跟上述那篇文章同樣的指令集, 硬件假設也同樣:github

  • 寄存器: 本虛擬機有那麼幾個寄存器: A,B,C,D,E,F, 這些也同樣設定爲通用寄存器, 能夠用來存儲任何東西.
  • 程序: 本虛擬機使用的程序將會是一個只讀指令序列.
  • 堆棧: 本虛擬機是一個基於堆棧的虛擬機, 咱們能夠對這個堆棧進行壓入/彈出值的操做.

這樣基於堆棧的虛擬機的實現要比基於寄存器的虛擬機的實現簡單得多.編程

示例指令集以下:數組

PSH 5       ; pushes 5 to the stack
PSH 10      ; pushes 10 to the stack
ADD         ; pops two values on top of the stack, adds them pushes to stack
POP         ; pops the value on the stack, will also print it for debugging
SET A 0     ; sets register A to 0
HLT         ; stop the program

注意,POP 指令將會彈出堆棧最頂層的內容, 而後把堆棧指針, 這裏爲了方便觀察, 咱們會設置一條打印命令,這樣咱們就可以看到 ADD 指令工做了。我還加入了一個 SET 指令,主要是讓你理解寄存器是能夠訪問和寫入的。你也能夠本身實現像 MOV A B(將A的值移動到B)這樣的指令。HTL 指令是爲了告訴咱們程序已經運行結束。網絡

說明: 原文的 C語言版 在對堆棧的處理上不太準確, 沒有把 stack 的棧頂元素 "彈出", 在 POPADD 後, stack 中依然保留着應該彈出的數據,,數據結構

虛擬機工做原理

這裏也是本文的核心內容, 實際上虛擬機很簡單, 遵循這樣的模式:框架

  • 讀取: 首先,咱們從指令集合或代碼中讀取下一條指令
  • 解碼: 而後將指令解碼
  • 執行: 執行解碼後的指令

爲聚焦於真正的核心, 咱們如今簡化一下這個處理步驟, 暫時忽略虛擬機的編碼部分, 由於比較典型的虛擬機會把一條指令(包括操做碼和操做數)打包成一個數字, 而後再解碼這個數字, 所以, 典型的虛擬機是能夠讀入真實的機器碼並執行的.編輯器

項目文件結構

正式開始編程以前, 咱們須要先設置好咱們的項目. 我是在 OSX 上寫這個虛擬機的, 由於 Lua 的跨平臺特性, 因此你也能夠在 WindowsLinux 上無障礙地運行這個虛擬機.ide

首先, 咱們須要一個 Lua 運行環境(我使用Lua5.3.2), 能夠從官網下載對應於你的操做系統的版本. 其次咱們要新建一個項目文件夾, 由於我打算最終把這個項目分享到 github 上, 因此用這個目錄 ~/GitHub/miniVM, 以下:

Air:GitHub admin$ cd ~/GitHub/miniVM/
Air:miniVM admin$

如上,咱們先 cd 進入 ~/GitHub/miniVM,或者任何你想放置的位置,而後新建一個 lua 文件 miniVM.lua。 由於如今項目很簡單, 因此暫時只有這一個代碼文件。

運行也很簡單, 咱們的虛擬機程序是 miniVM.lua, 只須要執行:

lua miniVM.lua

機器指令集

如今開始爲虛擬機準備要執行的代碼了. 首先, 咱們須要定義虛擬機使用的機器指令集.

指令集數據結構設計

咱們須要用一種數據結構來模擬虛擬機中的指令集.

C語言版

C語言版 中, 做者用枚舉類型來定義機器指令集, 由於機器指令基本上都是一些從 0n 的數字, 咱們就像在編輯一個彙編文件, 使用相似 PSH 之類的助記符, 再翻譯成對應的機器指令.

假設助記符 PSH 對應的機器指令是 0, 也就是把 PSH, 5 翻譯爲 0, 5, 可是這樣咱們讀起來會比較費勁, 由於在 C 中, 以枚舉形式寫的代碼更具可讀性, 因此 C語言版 做者選擇了使用枚舉來設計機器指令集, 以下:

typedef enum {
   PSH,
   ADD,
   POP,
   SET,
   HLT
} InstructionSet;

Lua版的其餘方案

看看咱們的 Lua 版本如何選擇數據結構, 衆所周知 Lua 只有一種基本數據結構: table, 所以咱們若是想使用枚舉這種數據結構. 就須要寫出 Lua 版的枚舉來, 在網絡上搜到這兩篇文檔:

第一篇是直接用 Lua 使用 C 定義的枚舉, 代碼比較多, 就不在這裏列了, 不符合咱們這個項目對於簡單性的要求.

第二篇是用Luatable 模擬實現了一個枚舉, 代碼比較短, 列在下面.

function CreateEnumTable(tbl, index)   
    local enumtbl = {}   
    local enumindex = index or 0   
    for i, v in ipairs(tbl) do   
        enumtbl[v] = enumindex + i   
    end   
    return enumtbl   
end  

local BonusStatusType = CreateEnumTable({"NOT_COMPLETE", "COMPLETE", "HAS_TAKE"},-1)

不過這種實現對咱們來講也不太適合, 一方面寫起來比較繁瑣, 另外一方面代碼也不太易讀, 因此須要設計本身的枚舉類型.

最終使用的Lua版

如今的方案是直接選擇用一個 table 來表示, 以下:

InstructionSet = {"PSH","ADD","POP","SET","HLT"}

這樣的實現目前看來最簡單, 可讀性也很不錯, 不過缺少擴展性, 咱們暫時就用這種方案.

測試程序數據結構設計

如今須要一段用來測試的程序代碼了, 假設是這樣一段程序: 把 56 相加, 把結果打印出來.

C語言版 中, 做者使用了一個整型數組來表示該段測試程序, , 以下:

const int program[] = {
    PSH, 5,
    PSH, 6,
    ADD,
    POP,
    HLT
};

注意: PSH 是前面 C語言版 定義的枚舉值, 是一個整數 0, 其餘相似.

咱們的 Lua 版暫時使用最簡單的結構:表, 以下:

program = {
    "PSH", "5",
    "PSH", "6",
    "ADD",
    "POP",
    "HLT"
}

這段代碼具體來講, 就是把 56 分別前後壓入堆棧, 調用 ADD 指令, 它會將棧頂的兩個值彈出, 相加後再把結果壓回棧頂, 而後咱們用 POP 指令把這個結果彈出, 最後 HLT 終止程序.

很好, 咱們有了一個完整的測試程序. 如今, 咱們描述了虛擬機的讀取, 解碼, 求值 的詳細過程. 可是實際上咱們並無作任何解碼操做, 由於咱們這裏提供的就是原始的機器指令. 也就是說, 咱們後續只須要關注 讀取求值 兩個操做. 咱們將其簡化爲 fetcheval 兩個函數.

從測試程序中取得當前指令

由於咱們的 Lua 版把測試程序存爲一個字符串表 program 的形式, 所以能夠很簡單地取得任意一條指令.

虛擬機有一個用來定位當前指令的地址計數器, 通常被稱爲 指令指針程序計數器, 它指向即將執行的指令, 一般被命名爲 IPPC. 在咱們的 Lua 版中, 由於表的索引以 1 開始, 因此這樣定義:

-- 指令指針初值設爲第一條
IP = 1

那麼結合咱們的 program 表, 很容易理解 program[IP] 的含義: 它以 IP 做爲表的索引值, 去取 program 表中的第 1 條記錄, 完整代碼以下:

IP = 1
instr = program[IP];

若是咱們打印 instr 的值, 會返回字符串 PSH, 這裏咱們能夠寫一個取指函數 fetch, 以下:

function fetch()
    return program[IP]
end

該函數會返回當前被調用的指令, 那麼咱們想要取得下一條指令該如何呢? 很簡單, 只要把指令指針 IP1 便可:

x = fetch() -- 取得指令 PSH
IP = IP + 1 -- 指令指針加 1
y = fetch() -- 取得操做數 5

咱們知道, 虛擬機是會自動執行的, 好比指令指針會在每執行一條指令時自動加 1 指向下一條指令, 那麼咱們如何讓這個虛擬機自動運行起來呢? 由於一個程序直到它執行到 HLT 指令時纔會中止, 因此咱們能夠用一個無限循環來模擬虛擬機, 這個無限循環以遇到 HLT 指令做爲終止條件, 代碼以下:

running = true
-- 設置指令指針指向第一條指令
IP = 1
while running do
    local x = fetch()
    if x == "HLT" then running = false end
    IP = IP + 1 
end

說明: 代碼中的 local 表示 x 是一個局部變量, 其餘不帶 local 的都是全局變量

一個虛擬機最基本的核心就是上面這段代碼了, 它揭示了最本質的東西, 咱們能夠把上面這段代碼看作一個虛擬機的原型代碼, 更復雜的虛擬機均可以在這個原型上擴展.

不過上面這段代碼什麼具體工做也沒作, 它只是順序取得程序中的每條指令, 檢查它們是否是停機指令 HLT, 若是是就跳出循環, 若是不是就繼續檢查下一條, 至關於只執行了 HLT.

執行每一條指令

可是咱們但願虛擬機還可以執行其餘指令, 那麼就須要咱們對每一條指令分別進行處理了, 這裏最適合的語法結構就是 C語言switch-case 了, 讓 switch 中的每個 case 都對應一條咱們定義在指令集 InstructionSet 中的機器指令, 在 C語言版 中是這樣的:

void eval(int instr) {
    switch (instr) {
        case HLT:
            running = false;
            break;
    }
}

不過 Lua 沒有 switch-case 這種語法, 咱們就用 if-then-elseif 的結構來寫一個指令執行函數, 也就是一個求值函數 eval, 處理 HLT 指令的代碼以下:

function eval(instr)
    if instr == "HLT" then 
        running = false
    end
end

咱們能夠這樣調用 eval 函數:

running = true
IP = 1
while running do
    eval(fetch())
    IP = IP + 1
end

增長對其餘指令處理的 eval:

function eval(instr)
    if instr == "HLT" then 
        running = false
    elseif instr == "PSH" then
        -- 這裏處理 PSH 指令, 具體處理後面添加
    elseif instr == "POP" then
        -- 這裏處理 POP 指令, 具體處理後面添加
    elseif instr == "ADD" then
        -- 這裏處理 ADD 指令, 具體處理後面添加
    end
end

棧的數據結構設計

由於咱們的這款虛擬機是基於棧的, 一切的數據都要從存儲器搬運到棧中來操做, 因此咱們在爲其餘指令增長具體的處理代碼以前, 須要先準備一個棧.

注意: 咱們這裏要使用一種最簡單的棧結構:數組

C語言版 中使用了一個固定長度爲 256 的數組, 同時須要一個棧指針 SP, 它其實就是數組的索引, 用來指向棧中的元素, 以下:

int sp = -1;
int stack[256];

咱們的 Lua 版也準備用一個最簡單的表來表示棧, 以下:

SP = 0
stack = {}

注意: 咱們知道 C 的數組是從 0 開始的, 而 Lua 的數組是從 1 開始的, 因此咱們的代碼中以 1 做爲數組的開始, 那麼 SP 的初值就要設置爲 0.

各類指令執行時棧狀態變化的分析

下面是一個形象化的棧, 最左邊是棧底, 最右邊是棧頂:

[] // empty
PSH 5 // put 5 on **top** of the stack
[5]
PSH 6
[5, 6]
POP
[5]
POP
[] // empty
PSH 6
[6]
PSH 5
[6, 5]

先手動分析一下咱們的測試程序代碼執行時棧的變化狀況, 先列出測試程序:

PSH, 5,
PSH, 6,
ADD,
POP,
HLT

先執行 PSH, 5, 也就是把 5 壓入棧中, 棧的狀況以下:

[5]

再執行 PSH, 6, 也就是把 6 壓入棧中, 棧的狀況以下:

[5,6]

再執行 ADD, 由於它須要 2 個參數, 因此它會主動從棧中彈出最上面的 2 個值, 把它們相加後再壓入棧中, 至關於執行 2POP, 再執行一個 PSH, 棧的狀況以下:

[5, 6]
// pop the top value, store it in a variable called a
a = pop; // a contains 6
[5] // stack contents
// pop the top value, store it in a variable called b
b = pop; // b contains 5
[] // stack contents
// now we add b and a. Note we do it backwards, in addition
// this doesn't matter, but in other potential instructions
// for instance divide 5 / 6 is not the same as 6 / 5
result = b + a;
push result // push the result to the stack
[11] // stack contents

上面這段描述很重要, 理解了這個你才清楚如何用代碼來模擬棧的操做.

上述沒有提到棧指針 SP 的變化, 實際上它默認指向棧頂元素, 也就是上述棧中最右邊那個元素的索引, 咱們看到, 最右邊的元素的索引是一直變化的.

空的棧指針在 C語言版 的虛擬機中被設置爲 -1.

若是咱們在棧中壓入 3 個值, 那麼棧的狀況以下:

SP指向這裏(SP = 3)
       |
       V
[1, 5, 9]
 1  2  3  <- 數組下標

如今咱們先從棧上彈出 POP 出一個值, 咱們若是隻修改棧指針 SP, 讓其減 1, 以下:

SP指向這裏(SP = 2)
    |
    V
[1, 5, 9]
 1  2 <- 數組下標

注意: 咱們不能指定彈出棧中的某個元素, 只能彈出位於棧頂的元素

由於咱們是最簡版的山寨棧, 因此執行彈出指令時只修改棧指針的話, 棧中的那個應該被彈出的 9 實際上還在數組裏, 因此咱們在模擬 POP 指令時須要手動把彈出的棧頂元素從棧中刪除, 這樣作的好處在後面可視化時就清楚了.

各指令的處理邏輯

通過上面的詳細分析, 咱們應該對執行 PSHPOP 指令時棧的變化(特別是棧指針和棧數組)比較清楚了, 那麼先寫一下壓棧指令 PSH 5 的處理邏輯, 當咱們打算把一個值壓入棧中時, 先調整棧頂指針的值, 讓其加 1, 再設置當前 SP 處棧的值 stack[SP], 注意這裏的執行順序:

SP = -1;
stack = {};

SP = SP + 1
stack[SP] = 5

C語言版 中寫成這樣的:

void eval(int instr) {
    switch (instr) {
        case HLT: {
            running = false;
            break;
        }
        case PSH: {
            sp++;
            stack[sp] = program[++ip];
            break;
        }
    }
}

C語言版做者用了很多 sp++, stack[sp] = program[++ip] 之類的寫法, 可是我以爲這裏這麼用會下降易讀性, 由於讀者不太容易看出執行順序, 不如拆開來寫成 sp = sp + 1ip = ip + 1, 這樣看起來更清楚.

因此在咱們 Lua 版的 eval 函數中, 能夠這樣寫 PSH 指令的處理邏輯:

function eval(instr)
    if instr == "HLT" then 
        running = false
    elseif instr == "PSH" then
        -- 這裏處理 PSH 指令, 具體處理以下
        SP = SP + 1
        -- 指令指針跳到下一個, 取得 PSH 的操做數
        IP = IP + 1
        stack[SP] = program[IP]
    elseif instr == "POP" then
        -- 這裏處理 POP 指令, 具體處理後面添加   
    elseif instr == "ADD" then  
        -- 這裏處理 ADD 指令, 具體處理後面添加
    end
end

分析一下咱們的代碼, 其實很簡單, 就是發現當指令是 PSH 後, 首先棧頂指針 SP1, 接着指令指針加 1, 取得 PSH 指令後面緊跟着的操做數, 而後把棧數組的第一個元素 stack[SP]賦值爲測試程序數組中的操做數 program[IP].

接着是 POP 指令的處理邏輯, 它要把棧頂指針減 1, 同時最好從棧數組中刪除掉彈出棧的元素:

elseif instr == "POP" then
    -- 這裏處理 POP 指令, 具體處理以下
    local val_popped = stack[SP]
    SP = SP - 1
elseif ...

ADD指令的處理邏輯

最後是稍微複雜一些的 ADD 指令的處理邏輯, 由於它既有壓棧操做, 又有出棧操做, 以下:

elseif instr == "ADD" then  
    -- 這裏處理 ADD 指令, 具體處理以下
    -- 先從棧中彈出一個值
    local a = stack[SP]
    stack[SP] = 0
    SP = SP - 1
            
    -- 再從棧中彈出一個值
    local b = stack[SP]
    stack[SP] = 0
    SP = SP - 1
    
    -- 把兩個值相加
    local result = a + b
         
    -- 把相加結果壓入棧中   
    SP = SP + 1
    stack[SP] = result
end

最終代碼

很好, 如今咱們 Lua 版的虛擬機完成了, 完整代碼以下:

-- 項目名稱: miniVM
-- 項目描述: 用 Lua 實現的一個基於棧的微型虛擬機
--  項目地址: https://github.com/FreeBlues/miniVM
--  項目做者: FreeBlues

-- 指令集
InstructionSet = {"PSH","ADD","POP","SET","HLT"}
Register = {A, B, C, D, E, F,NUM_OF_REGISTERS}

--  測試程序代碼
program = {"PSH", "5", "PSH", "6", "ADD", "POP", "HLT"}

-- 指令指針, 棧頂指針, 棧數組
IP = 1
SP = 0
stack = {}

-- 取指令函數
function fetch()
    return program[IP]
end

-- 求值函數
function eval(instr)
    if instr == "HLT" then 
        running = false
    elseif instr == "PSH" then
        -- 這裏處理 PSH 指令, 具體處理以下
        SP = SP + 1
        -- 指令指針跳到下一個, 取得 PSH 的操做數
        IP = IP + 1
        stack[SP] = program[IP]
    elseif instr == "POP" then
        -- 這裏處理 POP 指令, 具體處理以下 
        local val_popped = stack[SP]
        SP = SP - 1  
    elseif instr == "ADD" then  
        -- 這裏處理 ADD 指令, 具體處理以下
        -- 先從棧中彈出一個值
        local a = stack[SP]
        stack[SP] = 0
        SP = SP - 1
            
        -- 再從棧中彈出一個值
        local b = stack[SP]
        stack[SP] = 0
        SP = SP - 1
    
        -- 把兩個值相加
        local result = a + b
         
        -- 把相加結果壓入棧中   
        SP = SP + 1
        stack[SP] = result
        
        -- 爲方便查看測試程序運行結果, 這裏增長一條打印語句
        print(stack[SP])
    end
end

-- 虛擬機主函數
function main()
    running = true
    while running do
        eval(fetch())
        IP = IP + 1
    end
end

-- 啓動虛擬機
main()

執行結果以下:

Air:miniVM admin$ lua miniVM.lua 
11.0
Air:miniVM admin$

本項目代碼能夠到 Github-miniVM 下載.

虛擬機內部狀態可視化

應該說目前爲止咱們的虛擬機已經完美地實現了, 不過美中不足的是它的一切動做都被隱藏起來, 咱們只能看到最終運行結果, 固然了咱們也能夠增長打印命令來顯示各條指令執行時的狀況, 可是這裏咱們打算把虛擬機運行時內部狀態的變化用圖形的方式繪製出來, 而不只僅是簡單的 print 文本字符.

框架選擇:Love2D

這裏咱們選擇使用 Love2D 來繪圖, 緣由有這麼幾個:

  • 簡單好用:結構很簡單, 框架很好用
  • 跨平臺:同時支持 Windows, Mac OS X, Linux, Android 和 iOS
  • 免費開源:直接下載了就能用

Love2D的簡單介紹

Love2D 寫程序很是簡單方便, 首先新建一個目錄 love(目錄名能夠隨便起), 接着在該目錄下新建一個文件 main.lua(該文件必須使用這個名字), 而後在 main.lua 中編寫遊戲邏輯便可, 能夠試試這段代碼:

function love.draw()
    love.graphics.print("Hello World", 400, 300)
end

執行命令是用 love 調用目錄, 它會自動加載目錄內的 main.lua 文件, 命令以下:

love ./love

它會新建一個窗口, 而後打印 Hello World.

更詳細的能夠參考我寫的這篇文檔Mac 下安裝使用 Love2D

把項目修改成 Love2D 的形式

其實很簡單, 就是在項目文件目錄下新建個目錄 miniVM, 而後拷貝 miniVM.lua 代碼文件到這個新目錄中, 並將新目錄中的代碼文件名修改成 main.lua.

Air:miniVM admin$ cp ./miniVM.lua ./miniVM/main.lua
Air:miniVM admin$ tree
.
├── README.md
├── miniVM
│   └── main.lua
└── miniVM.lua

1 directory, 3 files
Air:miniVM admin$

按照 Love2D 的代碼框架要求修改整合代碼, 在 main.lua 中增長一個加載函數 love.load, 把全部只執行一次的代碼放進去, 再增長一個刷新函數 love.update, 把全部須要重複執行的代碼放進去, 最後增長一個 love.draw 函數, 把全部用於繪圖的代碼放進去, 修改後的 main.lua 以下:

function love.load()
    -- 指令集
    InstructionSet = {"PSH","ADD","POP","SET","HLT"}
    Register = {A, B, C, D, E, F,NUM_OF_REGISTERS}

    --  測試程序代碼
    program = {"PSH", "5", "PSH", "6", "ADD", "POP", "HLT"}

    -- 指令指針, 棧頂指針, 棧數組
    IP = 1
    SP = 0
    stack = {}
    
    running = true
end

function love.update(dt)
    -- 虛擬機主體
    if running then
        eval(fetch())
        IP = IP + 1
    end
end

function love.draw()
    love.graphics.print("Welcome to our miniVM!", 400, 300)
end


-- 取指令函數
function fetch()
    return program[IP]
end

-- 求值函數
function eval(instr)
    if instr == "HLT" then 
        running = false
    elseif instr == "PSH" then
        -- 這裏處理 PSH 指令, 具體處理以下
        SP = SP + 1
        -- 指令指針跳到下一個, 取得 PSH 的操做數
        IP = IP + 1
        stack[SP] = program[IP]
    elseif instr == "POP" then
        -- 這裏處理 POP 指令, 具體處理以下 
        local val_popped = stack[SP]
        SP = SP - 1  
    elseif instr == "ADD" then  
        -- 這裏處理 ADD 指令, 具體處理以下
        -- 先從棧中彈出一個值
        local a = stack[SP]
        stack[SP] = 0
        SP = SP - 1
            
        -- 再從棧中彈出一個值
        local b = stack[SP]
        stack[SP] = 0
        SP = SP - 1
    
        -- 把兩個值相加
        local result = a + b
         
        -- 把相加結果壓入棧中   
        SP = SP + 1
        stack[SP] = result
        
        -- 爲方便查看測試程序運行結果, 這裏增長一條打印語句
        print(stack[SP])
    end
end

代碼整合完畢, 檢查無誤後用 Love2D 加載, 以下:

Air:miniVM admin$ pwd
/Users/admin/GitHub/miniVM
Air:miniVM admin$ love ./miniVM
11
Air:miniVM admin$

咱們會看到彈出一個窗口用於繪製圖形, 同時命令行也會返回執行結果.

編寫繪製函數

目前咱們的虛擬機有一個用來模擬存儲器保存測試程序指令的 program 表, 還有一個用來模擬棧的 stack 表, 另外有兩個指針, 一個是指示當前指令位置的指令指針 IP, 另外一個是指示當前棧頂位置的棧頂指針 SP, 因此, 咱們只須要繪製出這 4 個元素在虛擬機運行時的狀態變化便可.

繪製 program 表和指令指針 IP

首先繪製做爲存儲器使用的 program 表, 咱們準備遵循約定俗成的習慣, 用兩個連在一塊兒的矩形方框來表示它的基本存儲單元, 左邊的矩形表示地址, 右邊的矩形表示在改地址存放的值, 這裏咱們會用到 Love2D 中這三個基本繪圖函數:

  • love.graphics.setColor(0, 100, 100)
  • love.graphics.rectangle("fill", x, y, w, h)
  • love.graphics.print("Welcome to our miniVM!", 400, 300)

咱們一步步來, 先繪製右側矩形和指令, 代碼以下:

-- 繪製存儲器中指令代碼的變化
function drawMemory()
    local x,y = 500, 300
    local w,h = 60, 20
    for k,v in ipairs(program) do
        -- 繪製矩形
        love.graphics.setColor(0, 255, 50)
        love.graphics.rectangle("fill", x, y-(k-1)*h, w, h)
        
        --  繪製要執行的指令代碼       
        love.graphics.setColor(200, 100, 100)
        love.graphics.print(v, x+15,y-(k-1)*h+5)
               
   end    
end

function love.draw()
    -- love.graphics.print("Welcome to our miniVM!", 400, 300)
    -- 繪製存儲器中指令代碼的變化
    drawMemory()
end

顯示效果以下:

接着咱們把左側的地址矩形和地址值, 還有指令指針也繪製出來, 代碼以下:

-- 繪製存儲器中指令代碼的變化
function drawMemory()
    local x,y = 500, 300
    local w,h = 60, 20
    for k,v in ipairs(program) do
        -- 繪製存儲器右側矩形
        love.graphics.setColor(0, 255, 50)
        love.graphics.rectangle("line", x, y+(k-1)*h, w, h)
        
        --  繪製存儲器中要執行的指令代碼       
        love.graphics.setColor(200, 100, 100)
        love.graphics.print(v, x+15,y+(k-1)*h+5)
        
        -- 繪製存儲器左側矩形
        love.graphics.setColor(0, 255, 50)
        love.graphics.rectangle("line", x-w/3-10,y+(k-1)*h,w/3+10, h)
        
        --  繪製表示存儲器地址的數字序號      
        love.graphics.setColor(200, 100, 100)
        love.graphics.print(k,x-w/2-10+10,y+(k-1)*h+5)
        
        -- 繪製指令指針 IP
        love.graphics.setColor(255, 10, 10)
        love.graphics.print("IP".."["..IP.."] ->",x-w-10+10-120,y+(IP-1)*h)          
   end    
end

顯示效果以下:


繪製 stack 表和棧頂指針 SP

接下來就是繪製用來模擬棧的 stack 表和棧頂指針 SP 了, 跟上面相似, 代碼以下:

-- 繪製棧的變化
function drawStack()
    local x,y = 200, 300
    local w,h = 60, 20
    for k,v in ipairs(stack) do
        -- 顯示棧右側矩形
        love.graphics.setColor(0, 255, 50)
        love.graphics.rectangle("line", x, y+(k-1)*h, w, h)
        
       --   繪製被壓入棧內的值       
        love.graphics.setColor(200, 100, 100)
        love.graphics.print(v, x+10,y+(k-1)*h)
        
       
       -- 繪製棧左側矩形
        love.graphics.setColor(0, 255, 50)
        love.graphics.rectangle("line", x-w-20,y+(k-1)*h,w+20, h) 
        
        --  繪製表示棧地址的數字序號      
        love.graphics.setColor(200, 100, 100)
        love.graphics.print(k,x-w-20+10,y+(k-1)*h)
        
       
        -- 繪製棧頂指針 SP
        love.graphics.setColor(255, 10, 10)
        love.graphics.print("SP".."["..SP.."] ->",x-w-10+10-100,y+(SP-1)*h)
    end    
end

function love.draw()
    -- love.graphics.print("Welcome to our miniVM!", 400, 300)
    -- 繪製存儲器中指令代碼的變化
    drawMemory()
    drawStack()
end

顯示效果以下:

很不錯的結果, 終於能看到虛擬機這個黑盒子裏面的內容了, 不過一會兒就執行過去了, 仍是有些遺憾, 那麼就給它增長一項單步調試的功能好了!

說明: 由於 Love2D 的座標軸方向是左手系,也就是說 Y 軸的正向向下, 因此咱們調整了一下 programstack 的地址順序, 小序號在上, 大序號在下.

增長單步調試功能

其實很簡單, 咱們只須要在虛擬機的主體執行流程中增長一個判斷邏輯, 每執行一條指令後都等待用戶的輸入, 這裏咱們設計簡單一些, 就是每執行完一條指令, 虛擬機就自動暫停, 若是用戶用鍵盤輸入 s 鍵, 則繼續執行下一條指令.

須要用到這個鍵盤函數:

  • love.keyreleased(key)

代碼以下:

function love.load()
    ...
    step = false
end

function love.keyreleased(key)
   if key == "s" then
      step = true
   end
end

function love.update(dt)
    -- 虛擬機主體
    if running then 
        if step then 
            step = false
            eval(fetch())
            IP = IP + 1
        end
    end
end

運行中能夠經過按下 s 鍵來單步執行每一條指令, 能夠看看效果:





到如今爲止, 咱們的可視化部分完成了, 並且也能夠經過用戶的鍵盤輸入來單步執行指令, 能夠說用 Lua 實現微型虛擬機的基本篇順利完成. 接下來的擴展篇咱們打算在這個簡單虛擬機的基礎上增長一些指令, 實現一個稍微複雜一些的虛擬機, 同時咱們可能會修改一些數據結構, 好比咱們的指令集的表示方式, 爲後面更有挑戰性的目標提供一些方便.

完整項目代碼

完整項目代碼保存在 Github-miniVM 裏, 歡迎自由下載.

項目文件清單以下:

Air:miniVM admin$ tree
.
├── README.md
├── miniVM
│   └── main.lua
├── miniVM.lua
└── pic
    ├── p01.png
    ├── p02.png
    ├── p03.png
    ├── p04.png
    ├── p05.png
    ├── p06.png
    ├── p07.png
    ├── p08.png
    └── p09.png

2 directories, 12 files
Air:miniVM admin$

後續計劃

由於這種方式很好玩, 因此咱們打算後續在這個基礎上實現一個 Intel 8086 的虛擬機, 包括完整的指令集, 最終目標是能夠在咱們的虛擬機上執行 DOS 時代的 x86 彙編程序代碼.

參考

相關文章
相關標籤/搜索