-
-
[分享]vm解释器入门
-
发表于: 3天前 708
-
前言
我们日常接触到的Cpu、VMware、Java JVM、VMProtect等等 都有核心的解释指令的功能,解释器也在多个领域有着举足轻重的地位,今天笔者将实现一个简单的自定义解释器程序作为入门练习,并使用自定义语法实现一个简单的阶乘练习
一、定义解释器结构
这是一个极简的解释器,里面仅包含5个通用寄存器,一个程序计数器,以及一个运行状态标识:
struct VM {
int reg[5]; // 5个通用寄存器 R0~R4
int pc; // 程序计数器
bool running; // 是否运行
};
二、定义指令
指令方面定义了:寄存器赋值、加法、减法、自定义输出寄存器、直接跳转、条件跳转、停止功能
其中R代表 Register 寄存器,IMM代表 Immediate 立即数,笔者暂时没有做MOV R,R\IMM的整合,因为是简单实现就分开为OP_MOV_IMM 和 OP_MOV_REG
/// < summary >
/// | 指令 | 含义 |
/// | ---------- | ------ |
/// | OP_MOV_IMM R1, R2 | 寄存器赋值1 |
/// | OP_MOV_REG R, IMM | 寄存器赋值2 |
/// | ADD R1, R2 | 加法 |
/// | SUB R1, R2 | 减法 |
/// | PRINT R | 输出 |
/// | OP_JMP IMM | 跳转 |
/// | OP_JZ R, IMM | 条件跳转 |
/// | OP_HALT | 停止 |
/// < summary >
enum {
OP_MOV_IMM,
OP_MOV_REG,
OP_ADD,
OP_SUB,
OP_PRINT,
OP_JMP,
OP_JZ,
OP_HALT
};
三、解释器实现
run方法的实现是整个解释器的核心,其中包含解释每个指令的逻辑,所有指令是混合在一起的,这会导致不同版本的解释器和不同版本的代码要做兼容,这就是微软一直以来在干的工作
例如其中OP_MOV_IMM后是接收2个参数,OP_PRINT后是接收一个参数,如果一个对不上后续指令都会出错
//解释器
void run(VM& vm, int* code) {
//初始化解释器
vm.pc = 0;//计数器置零
vm.running = true;//设置运行态
//循环解释
while (vm.running) {
int op = code[vm.pc++]; // 取指令
switch (op) {//判断指令
case OP_MOV_IMM: {
int reg = code[vm.pc++];//取寄存器编号
int val = code[vm.pc++];//取值
vm.reg[reg] = val;//寄存器赋值
break;
}
case OP_MOV_REG: {
int dst = code[vm.pc++];//R1编号
int src = code[vm.pc++];//R2编号
vm.reg[dst] = vm.reg[src];//R1 = R2
break;
}
case OP_ADD: {
int r1 = code[vm.pc++];//取操作数1
int r2 = code[vm.pc++];//取操作数2
vm.reg[r1] += vm.reg[r2];//加法操作
break;
}
case OP_SUB: {
int r1 = code[vm.pc++];//取操作数1
int r2 = code[vm.pc++];//取操作数2
vm.reg[r1] -= vm.reg[r2];//减法操作
break;
}
case OP_PRINT: {
int reg = code[vm.pc++];//取寄存器编号
printf("R%d = %d\n", reg, vm.reg[reg]);//输出寄存器的值
break;
}
case OP_JMP: {
int addr = code[vm.pc++];
vm.pc = addr;
break;
}
case OP_JZ: {
int reg = code[vm.pc++];
int addr = code[vm.pc++];
if (vm.reg[reg] == 0) {
vm.pc = addr;
}
break;
}
case OP_HALT: {
vm.running = false;//改变运行态
break;
}
default://未知指令
printf("Unknown opcode: %d\n", op);
vm.running = false;
break;
}
}
}
四、阶乘练习
最后是实现的阶乘练习,实现为5!=54321=120
要特别说明一下,本轮练习笔者并未实现乘法功能,具体实现实在 inner_loop 中用加法实现的乘法功能
入参R0=5,内部用到R3作为临时变量,R1作为返回结果,R4为乘法计数
大体思路为:首先初始化参数,进入outer_loop先判断n是否等于0,因为n每次循环会--,等于0则退出,如果不为0则做resultn=15=5,此时第一轮result=5 n--,第二轮为resultn=54,以此类推
//阶乘练习
int main() {
VM vm = { 0 };
//入参
int n = 5;
//| 寄存器 | 含义 |
//| ---- | ------ |
//| R0 | n |
//| R1 | result |
//| R2 | 常量1 |
//| R3 | temp |
//| R4 | counter |
int program[] = {
// 初始化
OP_MOV_IMM, R0, 5, // n = 5
OP_MOV_IMM, R1, 1, // result = 1
OP_MOV_IMM, R2, 1, // const 1
// ===== outer_loop (addr = 9) =====
OP_JZ, R0, 37, // if n == 0 -> end
OP_MOV_IMM, R3, 0, // temp = 0
OP_MOV_REG, R4, R0, // counter = n
// ===== inner_loop (addr = 18) ===== 乘法功能
OP_JZ, R4, 29, // if counter == 0 -> inner_end
OP_ADD, R3, R1, // temp += result
OP_SUB, R4, R2, // counter--
OP_JMP, 18, // 回到 inner_loop
// ===== inner_end (addr = 29) =====
OP_MOV_REG, R1, R3, // result = temp
OP_SUB, R0, R2, // n--
OP_JMP, 9, // 回到 outer_loop
// ===== end (addr = 37) =====
OP_PRINT, R1,
OP_HALT
};
//运行
run(vm, program);
return 0;
}
五、总结
本次vm解释器实现为极简写法,用于入门原理了解,对于现实的情况会复杂很多,例如笔者并未做区分标志寄存器,是直接用4字节的通用寄存器作为标志寄存器判断,这会导致空间的浪费
本小结作为vm练习的开端,后续还会继续完善栈区和宏指令实现 欢迎讨论 如有错漏 请留言
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
赞赏
- [分享]简单编译器实现 415
- [分享]Lexer简单词法解析器入门 362
- [分享]vm解释器入门2 478
- [分享]vm解释器入门 709
- [分享]混淆、加密、反沙箱概念介绍 917