-
-
[分享]vm解释器入门2
-
发表于: 3天前 477
-
前言
上篇文章笔者做了一个简单的vm解释器作为入门框架,本节继续来完善这个简易vm解释器
本节中将会在之前的解释器基础上添加 栈空间、栈操作(PUSH、POP)、函数调用(CALL、RET)
以及升级宏的方式进行调用代码更加清楚,并且实现对标签的支持 再无需手写跳转位置(很容易写错 写错了崩溃又很难排查)
最后将上节涉及的阶乘例子改为递归调用的方式实现,可充分练习函数调用以及栈区操作
一、新增指令
新增:OP_PUSH, OP_POP, OP_CALL, OP_RET
enum {
OP_MOV_IMM,
OP_MOV_REG,
OP_ADD,
OP_SUB,
OP_PRINT,
OP_JMP,
OP_JZ,
OP_HALT,
OP_PUSH,
OP_POP,
OP_CALL,
OP_RET
};
二、新增解释器规则
case OP_PUSH: {
int r = code[vm.pc++];//取出寄存器号
vm.stack[++vm.sp] = vm.reg[r];//入栈
break;
}
case OP_POP: {
int r = code[vm.pc++];//取出寄存器号
vm.reg[r] = vm.stack[vm.sp--];//出栈
break;
}
case OP_CALL: {
int addr = code[vm.pc++];//取出跳转位置
vm.stack[++vm.sp] = vm.pc; // 保存返回地址
vm.pc = addr;//改变pc跳转
break;
}
case OP_RET: {
vm.pc = vm.stack[vm.sp--];//取出返回地址
break;
}
三、新增以及初始化栈区
这里写了新增栈区的结构,在使用时切记初始化栈指针为-1(栈指针永远指向栈空间,刚开始栈空所以指向-1)
struct VM {
int reg[5]; // 5个通用寄存器 R0~R4
int pc; // 程序计数器
bool running; // 是否运行
int stack[256];//栈区
int sp;//栈指针
};
四、实现自动标签插入
这里使用结构实现了对字节码的全程托管,原理是在设置标签时进行记录标签位置和标签名称,在后续处理根据调用名称自动插入标签位置
struct Assembler {
// 字节码
vector<int> code;
// 标签列表
unordered_map<string, int> labels;
// 需要修复的回填跳转
vector<pair<int, string>> fixups;
//插入字节码
void emit(int v) {
code.push_back(v);
}
//添加标签
void label(const string& name) {
labels[name] = code.size();
}
//记录待修复标签
void jmp_placeholder(const string& name) {
// 添加到待修复列表
fixups.push_back({ (int)code.size(), name });
// 占位
code.push_back(0);
}
//回填标签
void patch() {
for (auto& f : fixups) {
int pos = f.first;
const string& name = f.second;
code[pos] = labels[name];
}
}
//获取字节码
int* getData()
{
//回填标签
patch();
//返回字节码
return this->code.data();
}
};
五、实现宏定义调用
实现宏定义调用,类似于汇编的形式,结构逻辑更清晰
// 操作
#define MOVI(as, reg, v) do { as.emit(OP_MOV_IMM); as.emit(reg); as.emit(v); } while(0)
#define MOVR(as, r1, r2) do { as.emit(OP_MOV_REG); as.emit(r1); as.emit(r2); } while(0)
#define ADDR(as, r1, r2) do { as.emit(OP_ADD); as.emit(r1); as.emit(r2); } while(0)
#define SUBR(as, r1, r2) do { as.emit(OP_SUB); as.emit(r1); as.emit(r2); } while(0)
#define PRINT(as, r) do { as.emit(OP_PRINT); as.emit(r); } while(0)
#define HALT(as) do { as.emit(OP_HALT); } while(0)
// 跳转
#define LABEL(as, name) do { as.label(name); } while(0)
#define JMP(as, name) do { as.emit(OP_JMP); as.jmp_placeholder(name); } while(0)
#define JZ(as, reg, name) do { as.emit(OP_JZ); as.emit(reg); as.jmp_placeholder(name); } while(0)
#define CALL(as, name) do { as.emit(OP_CALL); as.jmp_placeholder(name); } while(0)
#define RET(as) do { as.emit(OP_RET); } while(0)
// 栈
#define PUSH(as, r) do { as.emit(OP_PUSH); as.emit(r); } while(0)
#define POP(as, r) do { as.emit(OP_POP); as.emit(r); } while(0)
六、递归调用阶乘练习
这里的逻辑和上节的一样,不同的是更换成递归调用的实现方式,并且升级了宏调用以及自动标签的形式,逻辑更加清晰,代码如下:
//阶乘练习
int main() {
Assembler as;
VM vm = { 0 };
//入参
int n = 5;
// main
MOVI(as, R0, n); // n = 5
MOVI(as, R2, 1); // const 1
CALL(as, "fact"); //调用fast方法
PRINT(as, R1); //打印
HALT(as);
// ===== 函数 fact =====
LABEL(as, "fact");
JZ(as, R0, "base_case");// if (n == 0)
PUSH(as, R0);// 保存 n(因为后面要改)
SUBR(as, R0, R2);// n = n - 1
CALL(as, "fact");// 递归调用
POP(as, R0);// 恢复 n
// ===== 乘法:R1 = R1 * R0 =====
MOVI(as, R3, 0);// temp = 0
MOVR(as, R4, R0);// counter = n
LABEL(as, "mul_loop");
JZ(as, R4, "mul_end");
ADDR(as, R3, R1); // temp += result
SUBR(as, R4, R2); // counter--
JMP(as, "mul_loop");
LABEL(as, "mul_end");
MOVR(as, R1, R3);// result = temp
RET(as);
// ===== base_case =====
LABEL(as, "base_case");
MOVI(as, R1, 1); // return 1
RET(as);
//执行
run(vm, as.getData());
return 0;
}
七、总结
本节主要内容为完善之前的vm解释器,为后续更为复杂的项目做铺垫,笔者希望借此入门学习解释器并且将其完成为一个可用的vm解释器框架,后续还会持续进行完善,欢迎大家讨论,如有错漏请留言!
最后于 3天前
被mb_binusgki编辑
,原因:
赞赏
他的文章
- [分享]简单编译器实现 415
- [分享]Lexer简单词法解析器入门 361
- [分享]vm解释器入门2 478
- [分享]vm解释器入门 708
- [分享]混淆、加密、反沙箱概念介绍 917
赞赏
雪币:
留言: