豫ICP备17040950号-2

如何实现一个简单的寄存器虚拟机

文章目录

虚拟机总共有两种,基于栈的和基于寄存器的。栈式的生成字节码会更多,但是单个指令比较简单,虚拟机更好设计。基于寄存器方式实现的虚拟机以后更好优化,执行效率会更高。当然硬件层面的寄存器和内存完全是不同的概念。而软件层面的虚拟寄存器就是数据段的内存。比如基于寄存器的虚拟机都是用一个变量(内存)来模拟寄存器。

可以参考一下CPU的相关实现,做一个指令的子集就行。

如果只是做赋值、循环、函数调用的话,我们还需要引入加减法,判断,跳转功能。

然后我们管理两段空间,分别是指令空间数据空间就好了。

指令采用一个简单方式:1个字节的指令编码1个标志判断2个2字节参数

1
2
3
4
5
6
7
8
typedef struct inst
{
unsigned char code; // 指令
unsigned char cond, // 执行该指令的条件
unsigned short p1, p2; // 参数1、2
} inst_t;

typedef unsigned short data_t; // 我们操作的就是16位数

接下来我们需要有一个保存状态的结构

1
2
3
4
5
6
7
typedef struct vm_state
{
int ip = 0; // 指令ptr
int flag; // 记录最后判断的标志
inst_t *code; // 代码段地址
data_t *data; // 数据段地址
} vm_state_t;

定义一下需要的指令:

1
2
3
4
5
6
7
8
9
10
#define IADD    1 // 加法
#define ISUB 2 // 减法
#define ICMP 3 // 判断
#define IJMP 4 // 跳转
#define IMOV 5 // 赋值
#define ISTIP 6 // 保存IP
#define ILDIP 7 // 设置IP(跳转)
#define ILD 8 // 加载一个立即数
#define IOUT 9 // 输出
#define ISTOP 255 // 挂起虚拟机

定义一下执行指令的条件,也就是说在什么条件下才执行该指令

1
2
3
#define FNA     0 // 任何状态下都执行
#define FEQ 1 // 状态为“相等”时执行
#define FNE 2 // 状态为“不等”时执行

好了,然后我们可以写一个虚拟机的执行的函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
void execute(vm_state_t *state)
{
for (;;) // 执行到挂起为止
{
inst_t *current = state.code[state->ip];
state->ip++; // 取出指令以后自动ip后移
if (current->cond != FNA && current->cond != state->flag)
// 该指令要求的状态不符合当前状态,略过
continue;
switch (current->code)
{
case IADD:
// 将p1指向的数据加上p2指向的数据
state->data[current->p1] += state->data[current->p2];
break;
case ISUB:
state->data[current->p1] -= state->data[current->p2];
break;
case ICMP:
// 比较p1指向的数据和p2指向的数据
if (state->data[current->p1] == state->data[current->p2])
state->flag = FEQ;
else
state->flag = FNE;
break;
case IJMP:
// 跳转,指令根据p1进行偏移
state->ip += current->p1;
break;
case IMOV:
// 将p1指向的数据设置为p2指向的数据
state->data[current->p1] = state->data[current->p2];
break;
case ISTIP:
// 把IP保存到p1指向的数据
state->data[current->p1] = (data_t) state->ip;
break;
case ILDIP:
// 将IP设置为p1指向的数据,该指令会导致跳转
state->ip = state->data[current->p1];
break;
case ILD:
// 将立即数p2加载到p1指向的数据
state->data[current->p1] = current->p2;
break;
case IOUT:
// 输出p1指向的数据
printf("%d\n", state->data[current->p1]);
break;
case ISTOP:
return;
}
}
}

这虚拟机就算写完了。

这个虚拟机能做点什么呢?比如做一个类似这样的加法还是可以的:

1
2
3
int sum = 0;
for (int i = 1; i != 101; i++)
sum += i;

我们可以这样处理:

保存sum在数据段0号

保存i在数据段1号

保存101立即数在数据段2号

保存立即数1在数据段3号

翻译的指令如下:

1
2
3
4
5
6
7
8
9
10
11
0000 ILD 2, 101   // 放立即数101到2号位
0001 ILD 3, 1 // 放立即数1到3号位
0002 ILD 1, 1 // 放立即数1到变量i
0003 ILD 0, 0 // 放立即数0到变量sum
0004 ICMP 1, 2 // 比较i和101
0005 [FEQ] IJMP 3 // 如果相等(i==101)就跳转到9,因为指令执行完ip为6,所以+3就到了9
0006 IADD 0, 1 // sum += i
0007 IADD 1, 3 // i++,3号位保存的就是1
0008 IJMP -5 // 跳转到4,因为指令执行完ip为9,所以减5就到了指令4
0009 IOUT 0 // 输出sum
0010 ISTOP // 挂起

要想测试一下,写一个函数把这些指令放进去就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdlib.h>

inst_t sample_code[] =
{
{ ILD, FNA, 2, 100 },
{ ILD, FNA, 3, 1 },
{ ILD, FNA, 1, 1 },
{ ILD, FNA, 0, 0 },
{ ICMP, FNA, 1, 2 },
{ IJMP, FEQ, 3, 0 },
{ IADD, FNA, 0, 1 },
{ IADD, FNA, 1, 3 },
{ IJMP, FNA, -5, 0 },
{ IOUT, FNA, 0, 0 },
{ ISTOP, FNA, 0, 0 }
};
data_t data_seg[16];
void main(int argn, char *argv[])
{
vm_state_t state;
memset(&state, 0, sizeof(state);
state->code = sample_code;
state->data = data_seg;
execute(state);
}

这个简单的虚拟机是否可以实现函数调用呢?答案是可以的,利用ISTIP、ILDIP就可以了,返回地址可以保存在数据段中。

这个虚拟机非常简单,效率低、空间浪费、功能有限,不过在这个基础上可以自己优化、扩充嘛。

关于需要阅读什么书籍?这个要看自己当前的水平了,直接介绍专业著作怕是不一定能看的下去。我建议可以自己先写着玩玩,实现一些功能,尝试编译一些自定义的脚本代码到自己的虚拟机上,然后再阅读一点编译原理、垃圾回收、JIT相关的文章,看看Intel、ARM的CPU的指令手册,把自己实验性质的虚拟机做的像样一些。玩的差不多了,可以看看

https://jilp.org/vol5/v5paper12.pdf

最后:上述代码回答时随手写的,没有编译调试过,难免有很多笔误…