在網上看到一篇文章 使用 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
的棧頂元素 "彈出", 在 POP
和 ADD
後, stack
中依然保留着應該彈出的數據,,數據結構
這裏也是本文的核心內容, 實際上虛擬機很簡單, 遵循這樣的模式:框架
爲聚焦於真正的核心, 咱們如今簡化一下這個處理步驟, 暫時忽略虛擬機的編碼部分, 由於比較典型的虛擬機會把一條指令(包括操做碼和操做數)打包成一個數字, 而後再解碼這個數字, 所以, 典型的虛擬機是能夠讀入真實的機器碼並執行的.編輯器
正式開始編程以前, 咱們須要先設置好咱們的項目. 我是在 OSX
上寫這個虛擬機的, 由於 Lua
的跨平臺特性, 因此你也能夠在 Windows
或 Linux
上無障礙地運行這個虛擬機.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語言版
中, 做者用枚舉類型來定義機器指令集, 由於機器指令基本上都是一些從 0
到 n
的數字, 咱們就像在編輯一個彙編文件, 使用相似 PSH
之類的助記符, 再翻譯成對應的機器指令.
假設助記符 PSH
對應的機器指令是 0
, 也就是把 PSH, 5
翻譯爲 0, 5
, 可是這樣咱們讀起來會比較費勁, 由於在 C
中, 以枚舉形式寫的代碼更具可讀性, 因此 C語言版
做者選擇了使用枚舉來設計機器指令集, 以下:
typedef enum { PSH, ADD, POP, SET, HLT } InstructionSet;
看看咱們的 Lua
版本如何選擇數據結構, 衆所周知 Lua
只有一種基本數據結構: table
, 所以咱們若是想使用枚舉這種數據結構. 就須要寫出 Lua
版的枚舉來, 在網絡上搜到這兩篇文檔:
第一篇是直接用 Lua
使用 C
定義的枚舉, 代碼比較多, 就不在這裏列了, 不符合咱們這個項目對於簡單性的要求.
第二篇是用Lua
的table
模擬實現了一個枚舉, 代碼比較短, 列在下面.
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)
不過這種實現對咱們來講也不太適合, 一方面寫起來比較繁瑣, 另外一方面代碼也不太易讀, 因此須要設計本身的枚舉類型.
如今的方案是直接選擇用一個 table
來表示, 以下:
InstructionSet = {"PSH","ADD","POP","SET","HLT"}
這樣的實現目前看來最簡單, 可讀性也很不錯, 不過缺少擴展性, 咱們暫時就用這種方案.
如今須要一段用來測試的程序代碼了, 假設是這樣一段程序: 把 5
和 6
相加, 把結果打印出來.
在 C語言版
中, 做者使用了一個整型數組來表示該段測試程序, , 以下:
const int program[] = { PSH, 5, PSH, 6, ADD, POP, HLT };
注意:
PSH
是前面C語言版
定義的枚舉值, 是一個整數0
, 其餘相似.
咱們的 Lua
版暫時使用最簡單的結構:表, 以下:
program = { "PSH", "5", "PSH", "6", "ADD", "POP", "HLT" }
這段代碼具體來講, 就是把 5
和 6
分別前後壓入堆棧, 調用 ADD
指令, 它會將棧頂的兩個值彈出, 相加後再把結果壓回棧頂, 而後咱們用 POP
指令把這個結果彈出, 最後 HLT
終止程序.
很好, 咱們有了一個完整的測試程序. 如今, 咱們描述了虛擬機的讀取, 解碼, 求值
的詳細過程. 可是實際上咱們並無作任何解碼操做, 由於咱們這裏提供的就是原始的機器指令. 也就是說, 咱們後續只須要關注 讀取
和 求值
兩個操做. 咱們將其簡化爲 fetch
和 eval
兩個函數.
由於咱們的 Lua
版把測試程序存爲一個字符串表 program
的形式, 所以能夠很簡單地取得任意一條指令.
虛擬機有一個用來定位當前指令的地址計數器, 通常被稱爲 指令指針
或 程序計數器
, 它指向即將執行的指令, 一般被命名爲 IP
或 PC
. 在咱們的 Lua
版中, 由於表的索引以 1
開始, 因此這樣定義:
-- 指令指針初值設爲第一條 IP = 1
那麼結合咱們的 program
表, 很容易理解 program[IP]
的含義: 它以 IP
做爲表的索引值, 去取 program
表中的第 1
條記錄, 完整代碼以下:
IP = 1 instr = program[IP];
若是咱們打印 instr
的值, 會返回字符串 PSH
, 這裏咱們能夠寫一個取指函數 fetch
, 以下:
function fetch() return program[IP] end
該函數會返回當前被調用的指令, 那麼咱們想要取得下一條指令該如何呢? 很簡單, 只要把指令指針 IP
加 1
便可:
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
個值, 把它們相加後再壓入棧中, 至關於執行 2
個 POP
, 再執行一個 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
指令時須要手動把彈出的棧頂元素從棧中刪除, 這樣作的好處在後面可視化時就清楚了.
通過上面的詳細分析, 咱們應該對執行 PSH
和 POP
指令時棧的變化(特別是棧指針和棧數組)比較清楚了, 那麼先寫一下壓棧指令 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 + 1
跟 ip = 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
後, 首先棧頂指針 SP
加 1
, 接着指令指針加 1
, 取得 PSH
指令後面緊跟着的操做數, 而後把棧數組的第一個元素 stack[SP]
賦值爲測試程序數組中的操做數 program[IP]
.
接着是 POP
指令的處理邏輯, 它要把棧頂指針減 1
, 同時最好從棧數組中刪除掉彈出棧的元素:
elseif instr == "POP" then -- 這裏處理 POP 指令, 具體處理以下 local val_popped = stack[SP] SP = SP - 1 elseif ...
最後是稍微複雜一些的 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
來繪圖, 緣由有這麼幾個:
Windows, Mac OS X, Linux, Android 和 iOS
用 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
其實很簡單, 就是在項目文件目錄下新建個目錄 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
表, 咱們準備遵循約定俗成的習慣, 用兩個連在一塊兒的矩形方框來表示它的基本存儲單元, 左邊的矩形表示地址, 右邊的矩形表示在改地址存放的值, 這裏咱們會用到 Love2D
中這三個基本繪圖函數:
咱們一步步來, 先繪製右側矩形和指令, 代碼以下:
-- 繪製存儲器中指令代碼的變化 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
了, 跟上面相似, 代碼以下:
-- 繪製棧的變化 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
軸的正向向下, 因此咱們調整了一下program
和stack
的地址順序, 小序號在上, 大序號在下.
其實很簡單, 咱們只須要在虛擬機的主體執行流程中增長一個判斷邏輯, 每執行一條指令後都等待用戶的輸入, 這裏咱們設計簡單一些, 就是每執行完一條指令, 虛擬機就自動暫停, 若是用戶用鍵盤輸入 s
鍵, 則繼續執行下一條指令.
須要用到這個鍵盤函數:
代碼以下:
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
彙編程序代碼.