上一篇文章,我们实现了一个最小的虚拟机,但是它还不太完善。今天,我们在原来的基础上继续添砖加瓦,变得更加有血有肉。
新增功能如下:
- 实现更多指令,比如
MOV、SUB、DIV、MUL、LOGR、IF、SET、LDR、STR
。 - 引入寄存器。
- 实现条件跳转,也就是上面的 IF 指令。
寄存器定义
寄存器在硬件中的功能是用于存取数据。这里呢,我们使用数组来模拟。照葫芦画瓢,定义如下几种寄存器。
- 通用寄存器:A、B、C、D、E、F。
- IP 寄存器:IP。
- 栈顶指针寄存器:SP。
具体定义如下:
// 寄存器类型定义
typedef enum
{
A,B,C,D,E,F, // 通用寄存器
IP, // IP 寄存器
SP, // 栈顶指针寄存器
NUM_OF_REGISTERS
} Registers;
// 寄存器
int registers[NUM_OF_REGISTERS];
NUM_OF_REGISTERS
是一个取巧的设计,可以很方便的知道有多少个寄存器。
那么获取某个寄存器的方式就很简单了,registers[r]
,r
是寄存器下标。比如获取 A 寄存器,使用 registers[A]
即可。
另外,为了方便存取 IP、SP
,将其定义为宏,简化处理。
#define sp (registers[SP])
#define ip (registers[IP])
同样,将 sp
初始化为 -1
,ip
初始化为 0
。
// 初始化寄存器
sp = -1;
ip = 0;
新增指令定义
新增下图中的几种指令:
指令定义指令实现
有了上一篇的基础后,要完成这几个指令应该是信手拈来。下面,我们一个个来实现。
SET
SET 主要用于给某个寄存器赋值。分 2 步走:
- 取出两个参数,分别是寄存器下标和数据。
// 寄存器下标
int r = program[++ip];
// 值
int value = program[++ip];
- 更新寄存器的值。
registers[r] = value;
MOV
MOV 用于寄存器之间的赋值。分三步走:
- 取出 2 个参数,目标寄存器和源寄存器。
// 目的寄存器
int dr = program[++ip];
// 源寄存器
int sr = program[++ip];
- 取出源寄存器的值。
// 源寄存器的值
int sourceValue = registers[sr];
- 赋值给目的寄存器。
registers[dr] = sourceValue;
SUB
SUB 用于取出栈中的两个数相减,将结果放回栈中,同时也保存到寄存器 A 中。分 4 步走:
- 取出栈中的两个数。
// 从栈中取出两个数
int a = stack[sp--];
int b = stack[sp--];
- 计算相减结果,注意顺序。
int result = b - a;
- 结果放回栈中。
// 入栈
stack[++sp] = result;
- 结果放到寄存器 A 中。
registers[A] = result;
DIV
DIV,除法指令。操作步骤同 SUB。
注意被除数为 0 的情况。
// 从栈中取出两个数,相除,再 push 回栈
int a = stack[sp--];
int b = stack[sp--];
if (a != 0)
{
int result = b / a;
// 入栈
stack[++sp] = result;
registers[A] = result;
}
else
{
printf("exception occur, divid 0 \n");
}
MUL
乘法指令,步骤同 SUB。
LOGR
LOGR 用于打印寄存器的值。实现也很简单,分 2 步走:
- 取出寄存器的值。
// 寄存器下标
int r = program[++ip];
// 寄存器的值
int value = registers[r];
- 调用 printf 进行打印。
printf("log register_%d %d\n", r, value);
STR
STR 相当于存储指令,将指定寄存器中的数据放入栈中。分 2 步走:
- 取出寄存器参数。
int r = program[++ip];
- 将寄存器中数据放入栈中。
stack[++sp] = registers[r];
LDR
LDR 相当于取数据指令,将栈顶数据放入指定寄存器中。分 2 步走:
- 取栈顶数据和寄存器,但栈顶指针不变。
// 只取栈顶数据,sp 指针不变
int value = stack[sp];
// 取出寄存器参数
int r = program[++ip];
- 将数据放入寄存器中
registers[r] = value;
IF
唯一有些不同的就是 IF 条件跳转指令。当满足跳转条件时,会直接跳转到新指令处。
前面的实现中,在执行完某个条指令后,会默认指向下一条指令。但如果要进行跳转,ip 就不能再继续加 1,只需将 ip 设为新指令下标即可。
所以呢,需要额外添加变量 is_jump
来标识是否要执行跳转操作
。在取指令的 while 循环中,判断若不是跳转操作,则 ip++,继续指向下一条指令。
while (running)
{
int instr = program[ip];
eval(instr);
// 非跳转,才 +1
if (!is_jump)
{
ip++;
}
}
IF 指令的处理,分为下面几个步骤:
- 取出前两个参数,分别是寄存器下标和待比较的数据。
// 寄存器下标
int r = program[++ip];
// 要比较的值
int value = program[++ip];
- 判断寄存器中的数据与数值 value 是否相等。如果相等,则进行下面的操作。
- 取出新指令下标,并更新 ip 寄存器的值。
- 更重要的一点,更新标识 is_jump,表示是跳转操作。
if (registers[r] == value)
{
// 新指令下标
ip = program[++ip];
// 更新为跳转操作
is_jump = true;
}
可能有同学会疑惑,当 is_jump 更新为 true 之后,什么时候还原呢?很简单啦,在执行每条指令之前,将其置为 false 就好了。
至此,新增的功能就全部实现了。完整代码可点击文末链接 2 查看。
总结
这篇文章,我们主要完成了寄存器的定义、几个新指令的实现,其中还包括 IF 跳转指令。希望对你有帮助。
下一篇文章,将会进入更高阶的玩法,写一个更贴近真实定义的虚拟机。包括不限于:
- 指令定义标准化,操作码+操作数
- 指令的解析与执行
- 丰富常见指令集,比如逻辑运算、内存读写
- 引入标志寄存器
- 指令以二进制存储
- 中断处理
- ...
准备好了吗?让我们迎着微光出发吧~
网友评论