实现原创指令集的虚拟机
上两篇文章我介绍了我最近设计的一套指令集及其对应的虚拟机架构,这篇文章就来介绍虚拟机的实现过程。
虚拟机其实很简单,需要做的只是用一种指令去模拟另一种指令的功能。
为了运行速度,当然希望用尽量低级的方法去模拟,所以应该用汇编编写,但为了效率,我先用的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虚拟机的所有步骤,都是很简单的东西,如果读者有兴趣,完全可以自己动手写一个。
但在基本之外的东西其实不少,例如:如何实现控制台串行或异步的输入输出,如何实现逐步、断点等调试功能,如何确保错误的指令不会使虚拟机崩溃,如何确保运行的未知代码安全可控等等。
所以我也写了很多额外的代码,目标是使这个虚拟机变成一个有真正用途的工具,而不是玩具。
如果读者有什么建议和疑问,欢迎留言。
关于这个虚拟机中的一些优化技巧和实现细节,可能还会再写一篇文章聊聊。