實現一個堆棧虛擬機
本文我們實現一個基於堆棧的虛擬機,通過前面《簡單虛擬機》和《棧虛擬機源碼剖析》,對虛擬機結構和原理有了更深的理解和體會。下面我們給出堆棧虛擬機的示意圖:
堆棧虛擬機主要包括以上三部分:虛擬機、指令集、外部接口。
其中虛擬機內部構造主要是數據、指令、堆棧三部分,指令對數據進行操作,將數據裝載進堆棧中以備運算和處理。
指令集的設計可以參考別人的設計也可以按照自己的理解逐步擴充改進,主要有PUSH、POP、TOP、INPUT、OUTPUT、JMP、JMP_TRUE、JMP_FALSE、JMP_EQUAL、JMP_NOT_EQUAL、JMP_BIGGER、JMP_SMALLER等指令,這里沒有保護相關算術指令,算術指令可以進一步增加。
虛擬機的外部接口主要是裝載數據、裝載指令、運行指令、復位等。
下面我們給出虛擬機的實現,為了方便呈現,我們將代碼放置於一個文件中:
#include <iostream> #include <string> #include <vector> #include <stack> using namespace std; // 指令集 enum InstructCode { PUSH, // 入棧 POP, // 出棧 TOP, // 棧頂 INPUT, // 輸入 OUTPUT, // 輸出 JMP, // 跳轉 JMP_TRUE, // 為真跳轉 JMP_FALSE, // 為假跳轉 JMP_EQUAL, // 等於跳轉 JMP_NOT_EQUAL, // 不等跳轉 JMP_BIGGER, // 大於跳轉 JMP_SMALLER // 小於跳轉 }; // 定義數據 // 我們定義的數據只有兩種形式:整形和字符串 struct Data { bool isInt; int intData; string strData; Data() : isInt(true), intData(0) {} Data(int _n) : isInt(true), intData(_n) {} Data(const string& _s) : isInt(false), intData(0), strData(_s) {} }; struct Instruct { InstructCode insCode; int operand; Instruct() {} Instruct(InstructCode _ic) : insCode(_ic) {} Instruct(InstructCode _ic, int _d) : insCode(_ic), operand(_d) {} }; class VirtualBox { private: vector<Instruct> instruct; // 指令 stack<Data> stk; // 堆棧 vector<Data> data; // 數據 private: // 讀取data void ReadData(Data& _d) { cout << "讀取整型數 Y/N?"; char ch; cin >> ch; if (toupper(ch) == 'Y') { cin >> _d.intData; _d.isInt = true; } else { cin >> _d.strData; _d.isInt = false; } } // 打印數據 void PrintData(const Data& _d) { if (_d.isInt) { cout << _d.intData << endl; } else { cout << _d.strData << endl; } } public: VirtualBox() { data.resize(101); } ~VirtualBox() { Reset(); } void Reset() { data.clear(); instruct.clear(); while (!stk.empty()) { stk.pop(); } data.resize(101); cout << endl << "虛擬機復位" << endl; } void LoadData() // 加載數據 { } void LoadInstruct() // 加載指令 { } void LoadData_TestBigger() { } // 裝在指令序列,其功能是:輸入兩個數,將其比較,輸出較大的 void LoadInstruct_TestBigger() { instruct.push_back(Instruct(INPUT, 0)); // 讀取數據1 instruct.push_back(Instruct(INPUT, 1)); // 讀取數據2 instruct.push_back(Instruct(PUSH , 0)); // 將數據1壓棧 instruct.push_back(Instruct(PUSH , 1)); // 將數據2壓棧 instruct.push_back(Instruct(JMP_BIGGER, 3)); // 如果數據1大於數據2,則前進3步 instruct.push_back(Instruct(PUSH, 1)); // 如果數據1不大於數據2,則將數據2壓棧 instruct.push_back(Instruct(JMP , 2)); // 前進2步 instruct.push_back(Instruct(PUSH, 0)); // 將數據1壓棧,承接上面的前進3步 instruct.push_back(Instruct(OUTPUT)); // 將較大的數輸出 } void LoadData_TestPrint() // 測試打印數組——加載數據 { for (int i = 0; i != 10; ++i) { data[i] = Data(i); } } void LoadInstruct_TestPrint() // 測試打印數組——加載指令 { for (int i = 0; i != 10; ++i) { instruct.push_back(Instruct(PUSH, i)); instruct.push_back(Instruct(OUTPUT)); } } void Run() { int ip = 0; // 指令索引 int ipc = 1; // 指令前移量 Data a, b; for (; ip < instruct.size(); ) { ipc = 1; switch (instruct[ip].insCode) { case PUSH: stk.push(data[instruct[ip].operand]); // 堆棧中存儲實際的值,而非data的索引 break; case POP: stk.pop(); break; case TOP: data[instruct[ip].operand] = stk.top(); break; case INPUT: ReadData(data[instruct[ip].operand]); break; case OUTPUT: PrintData(stk.top()); stk.pop(); break; case JMP: ipc = instruct[ip].operand; break; case JMP_TRUE: if (stk.top().isInt && stk.top().intData != 0 || !stk.top().isInt && !stk.top().strData.empty()) { ipc = instruct[ip].operand; } stk.pop(); break; case JMP_FALSE: if (!(stk.top().isInt && stk.top().intData != 0 || !stk.top().isInt && !stk.top().strData.empty())) { ipc = instruct[ip].operand; } stk.pop(); break; case JMP_EQUAL: b = stk.top(); stk.pop(); a = stk.top(); stk.pop(); if (a.isInt == b.isInt && a.isInt && a.intData == b.intData) { ipc = instruct[ip].operand; } if (a.isInt == b.isInt && !a.isInt && a.strData == b.strData) { ipc = instruct[ip].operand; } break; case JMP_NOT_EQUAL: b = stk.top(); stk.pop(); a = stk.top(); stk.pop(); if (!(a.isInt == b.isInt && a.isInt && a.intData == b.intData)) { ipc = instruct[ip].operand; } if (!(a.isInt == b.isInt && !a.isInt && a.strData == b.strData)) { ipc = instruct[ip].operand; } if (a.isInt != b.isInt) { ipc = instruct[ip].operand; } break; case JMP_BIGGER: b = stk.top(); stk.pop(); a = stk.top(); stk.pop(); if (a.isInt == b.isInt && a.isInt && a.intData > b.intData) { ipc = instruct[ip].operand; } if (a.isInt == b.isInt && !a.isInt && a.strData > b.strData) { ipc = instruct[ip].operand; } break; case JMP_SMALLER: b = stk.top(); stk.pop(); a = stk.top(); stk.pop(); if (a.isInt == b.isInt && a.isInt && a.intData < b.intData) { ipc = instruct[ip].operand; } if (a.isInt == b.isInt && !a.isInt && a.strData < b.strData) { ipc = instruct[ip].operand; } break; default: break; } ip += ipc; } } }; int main() { VirtualBox vb; vb.LoadData_TestPrint(); vb.LoadInstruct_TestPrint(); vb.Run(); vb.Reset(); vb.LoadData_TestBigger(); vb.LoadInstruct_TestBigger(); vb.Run(); return 0; }
下面我們對虛擬機代碼進行逐步講解。
指令集
我們總共定義了12個指令,指令類型為enum型,便於switch-case結構處理。
數據
我們的數據類型目前支持兩種類型:整型和字符串。Data結構體的定義如下:
struct Data
{
bool isInt;
int intData;
string strData;
Data() : isInt(true), intData(0) {}
Data(int _n) : isInt(true), intData(_n) {}
Data(const string& _s) : isInt(false), intData(0), strData(_s) {}
};
isInt用於標識Data存儲的數據是整型還是字符串。
指令
我們對指令的定義包含了兩部分:操作碼和操作數,其中操作數為Data集的索引,而非實際的Data。
struct Instruct
{
InstructCode insCode;
int operand;
Instruct() {}
Instruct(InstructCode _ic) : insCode(_ic) {}
Instruct(InstructCode _ic, int _d) : insCode(_ic), operand(_d) {}
};
虛擬機VirtualBox
VirtualBox主要包含三部分:
vector<Instruct> instruct; // 指令
stack<Data> stk; // 堆棧
vector<Data> data; // 數據
其中,data用於存儲待處理或已處理的數據,instruct為待執行的指令,stk為根據指令實際操作數據的場所。
VirtualBox定義了兩個private成員函數ReadData、PrintData用於讀取和打印數據。
其余就是VirtualBox的外部接口,其中LoadData用於裝載數據,LoadInstruct用於裝載指令,Run函數式用於實際的執行指令,其內部有局部變量ip用於記錄當前指令索引,ipc用於記錄指令跳轉量,主要是switch-case結構,根據指令集中的12個指令,執行相應的操作。
另外還有一個Reset復位函數。
測試
我們對該虛擬機進行了兩個測試分別是打印0-9十個數字,輸入兩個數(整型數或字符串),打印出其中較大的數。
打印0-9十個數字的LoadData為:
for (int i = 0; i != 10; ++i)
{
data[i] = Data(i);
}
LoadInstruct為:
for (int i = 0; i != 10; ++i)
{
instruct.push_back(Instruct(PUSH, i));
instruct.push_back(Instruct(OUTPUT));
}
打印較大數的LoadData為空,LoadInstruct為:
instruct.push_back(Instruct(INPUT, 0)); // 讀取數據1
instruct.push_back(Instruct(INPUT, 1)); // 讀取數據2
instruct.push_back(Instruct(PUSH , 0)); // 將數據1壓棧
instruct.push_back(Instruct(PUSH , 1)); // 將數據2壓棧
instruct.push_back(Instruct(JMP_BIGGER, 3)); // 如果數據1大於數據2,則前進3步
instruct.push_back(Instruct(PUSH, 1)); // 如果數據1不大於數據2,則將數據2壓棧
instruct.push_back(Instruct(JMP , 2)); // 前進2步
instruct.push_back(Instruct(PUSH, 0)); // 將數據1壓棧,承接上面的前進3步
instruct.push_back(Instruct(OUTPUT)); // 將較大的數輸出
總結
以上是我們實現的基於堆棧的虛擬機,有關指令集方面的擴展尤其是對於算術指令擴展,有待我們進一步改進。VirtualBox的源碼以及后續擴展更新可以參見Github。