上兩篇文章我介紹了我最近設計的一套指令集及其對應的虛擬機架構,這篇文章就來介紹虛擬機的實現過程。
虛擬機其實很簡單,需要做的只是用一種指令去模擬另一種指令的功能。
為了運行速度,當然希望用盡量低級的方法去模擬,所以應該用匯編編寫,但為了效率,我先用的C語言寫出整體邏輯,后期再考慮匯編。
虛擬機原理
LVM虛擬機運行的流程是這樣:
初始化:虛擬機內存和寄存器值。
鏈接:指定虛擬機要運行的代碼。
可以看到虛擬機的功能就是循環處理指令。
我按照運行流程,簡單介紹一下代碼,首先是虛擬機的數據結構:
一台計算機最重要的是它的寄存器,頭文件中首先定義虛擬寄存器的數據類型:
typedef _utype hreg; //虛擬機寄存器,無符號整型,運算時按需轉換
typedef _utype u_hreg; typedef _stype s_hreg;
其中_utype是我提前定義的無符號整型,它的位長和平台類型相同,x86下32位,x64下64位。
之后就是LVM的結構體:
1 //運行VL字節碼的虛擬機器 LVM 2 typedef struct _LMACHINE 3 { 4 hreg *registers; //寄存器指針 5 hreg pc; //程序指針,真實內存地址 6 hreg error; //錯誤flag 7 hreg info_error; //錯誤信息 8 h_debug debug; //調試flag 9 10 byte *vcodes; 11 12 //控制台輸入輸出 13 MessageConsole *console; 14 15 //異常向量表 17 void *exceptionTable; 18 19 t_addr regs_top; 20 hreg max_pc; 21 hreg regs_0[_LVM_REG_COUNT]; //通用寄存器組 22 hreg regs_1[_LVM_REG_COUNT]; 23 hreg regs_2[_LVM_REG_COUNT]; 24 hreg regs_3[_LVM_REG_COUNT]; 25 27 //虛擬機機器內存棧 28 byte memory[0]; 29 }LMachine, LVM;
其中調試標識符,異常表和控制台等不是關鍵部分,后續有機會再介紹。
真正關鍵的寄存器是寄存器指針*registers,程序計數器pc,異常標識符error和程序指針*vcodes。
regs_0、1這些實際和memory[0]沒有區別,項目中已經更改名稱了。
而這個memory[0]是一種比較特殊的C語言語法,它聲明這個變量,但不分配空間。
它的空間實際是結構體尾部后面的額外內存,這樣相當於在動態分配內存時才確定它的長度。
之后是基礎的函數聲明,這是后面將實現的函數:
1 //創建虛擬機 2 LVM *LVM_Create(); 3 //初始化虛擬機 4 LVM *LVM_Init(LVM *machine); 5 6 //綁定V代碼 7 LVM *LVM_AttachV(LVM *machine, byte *code, uint size); 8 //運行V代碼虛擬機 9 int LVM_RunV(LVM *machine); 10 //調試運行V代碼虛擬機 11 int LVM_DebugV(LVM *machine); 12 //運行一句V代碼 13 int ProcessVCode(LVM *machine); 14 15 //清理虛擬機資源 16 LVM *LVM_Clean(LVM *machine);
創建虛擬機
創建虛擬機包括分配內存和初始化。
LVM需要分配一個LVM結構體加上寄存器列的一塊空間,寄存器列空間目前定為64KB。
初始化:
主要是設定寄存器的初始值,全部置零就好了。除此之外還要初始化控制台結構體
特別說明寄存器指針:它是指向寄存器窗口位置的指針,初始時指向結構中的regs_0寄存器組的地址。

1 machine->pc = 0; 2 machine->max_pc = -1; 3 machine->error = 0; 4 machine->info_error = 0xE7707EEE; //表示erroreee,只是好看 5 machine->debug = h_flags_debug_on; 6 //窗口指針 7 machine->registers = machine->regs_0; 8 //寄存器列的頂部,窗口不能超出這個范圍 9 machine->regs_top = (t_addr)machine + _LVM_SIZE; 10 //*初始化控制台 11 ... 12 //*初始化所有通用寄存器 13 ...
鏈接代碼
指定要運行的代碼,只需設定程序指針和最大pc
LVM *LVM_AttachV(LVM *machine, byte *code, uint size) { machine->vcodes = code; machine->max_pc = (hreg)code + size; return machine; }
運行虛擬機
正如上面所說,是一個運行指令的循環:
1 int LVM_RunV(LVM *machine) 2 { 3 machine->pc = (hreg)machine->vcodes; 4 while (!ProcessVCode(machine)); 5 if (machine->error) LVM_PrintError(machine); 6 return 0; 7 }
首先將程序地址作為PC的起點,因為我的PC是絕對地址。
然后循環運行指令,直到處理函數返回ERROR,可能是有錯誤,也可能是程序結束。
跳出循環后,檢查是否因錯誤才終止的,是則打印錯誤信息。
處理指令函數
之后就是最重要的處理指令部分,這部分只是單純的繁雜,但並不難。
首先取當前PC指向的指令的OP碼,然后是一個巨大的switch語句,對應每條指令的處理代碼。
1 int ProcessVCode(LVM *m) 2 { 3 //pc越界檢查 4 //if (m->pc < (hreg)m->vcodes) { m->error = ERROR_PC_NEG; return FALSE; } 5 //if (m->pc > m->max_pc) { m->error = ERROR_PC_OVF; return FALSE; } 6 7 //int op = m->vcodes[m->pc]; 8 int op = *(byte *)m->pc; 9 switch (op) 10 { 11 case 0: { m->error = ERROR_NULL; return RV_ERROR; } 12 case VOP_NOP: return VL_nop(m);
13 case VOP_MOVE: return VL_move_rd_rs(m);
14 case VOP_SET_S8: return VL_set_rd_s8(m);
15 case VOP_SET_S32: return VL_set_rd_s32(m);
16 ... 17 case VOP_END: { return RV_ERROR; } 18 default: { m->error = ERROR_INVALID; return RV_ERROR; } 19 } 20 return TRUE; 21 }
因為指令有七十多條,這里只取幾個舉例。

1 //復制寄存器值 2 static inline hreturn VL_move_rd_rs(LVM *m) 3 { 4 M_VCODE(code, VCode_reg2); 5 m->registers[code->rd] = m->registers[code->rs]; 6 M_PC_ADD(VLEN_REG2); 7 return RV_FINE; 8 } 9 10 //間接尋址讀取,寄存器 (u8) 11 static inline hreturn VL_load_rd_rs_u8(LMachine *m) 12 { 13 M_VCODE(code, VCode_reg2); 14 u8 *source = (u8 *)m->registers[code->rs]; 15 if (IsReadUnsafePtr(source)) 16 { 17 m->info_error = (hreg)source; 18 return m->error = ERROR_MEM_READ; 19 } 20 m->registers[code->rd] = *source; 21 M_PC_ADD(LEN_LD); 22 return RV_FINE; 23 } 24 25 static inline hreturn VL_xcall(LVM *m) 26 { 27 m->registers += *(s8 *)V_CODE(1); 28 //寄存器溢出 29 if ((t_addr)m->registers > m->regs_top) return m->error = ERROR_REG_OVF; 30 m->registers[-1] = m->pc + 6; //加上xcall語句長度 31 32 u32 address32 = *(u32 *)V_CODE(2); 33 M_PC_SET(address32); 34 return RV_FINE; 35 } 36 37 static inline hreturn VL_xreturn(LVM *m) 38 { 39 s8 offset = *(s8 *)V_CODE(1); 40 //m->pc = m->registers[-1]; 41 m->registers -= offset; 42 if ((t_addr)m->registers < (t_addr)m->regs_0) return m->error = ERROR_REG_NEG; 43 m->pc = m->registers[offset - 1]; 44 return RV_FINE; 45 }
里面包含幾個簡單的宏,比如M_VCODE()和V_CODE(),只是為了減少重復代碼,總之就是將pc指針轉為指令的結構體。
其中load,save等內存操作指令處理的都是絕對地址,所以判斷地址是否安全有效比較困難,后續考慮增加由虛擬機管理的內存塊,所有內存地址都改為相對內存塊頭的偏移。
到這里我已經介紹完實現這個LVM虛擬機的所有步驟,都是很簡單的東西,如果讀者有興趣,完全可以自己動手寫一個。
但在基本之外的東西其實不少,例如:如何實現控制台串行或異步的輸入輸出,如何實現逐步、斷點等調試功能,如何確保錯誤的指令不會使虛擬機崩潰,如何確保運行的未知代碼安全可控等等。
所以我也寫了很多額外的代碼,目標是使這個虛擬機變成一個有真正用途的工具,而不是玩具。
如果讀者有什么建議和疑問,歡迎留言。
關於這個虛擬機中的一些優化技巧和實現細節,可能還會再寫一篇文章聊聊。