栈虚拟机源码剖析
之前我们介绍过一个《简单虚拟机》,该虚拟机是基于寄存器的。
本文我们剖析一个栈虚拟机的源代码。该代码来自于《实现一个脚本引擎》中的《Part VII:虚拟机(The Virtual Machine)》,该栈虚拟机的源代码下载地址为:source code。
虚拟机的实现方式有三种:内存、堆栈、寄存器。
该虚拟机主要包含三部分:一个指令数组、一个字符串表、一个堆栈:
成员 |
说明 |
指令数组 |
存储程序所包含的指令 |
字符串表 |
实质上是一个指针数组,代表程序的一个变量或者堆栈中的一个临时变量 |
堆栈 |
有整数组成,指向字符串表,以表示什么字符串在堆栈中(这里使用的是整数,而非字符串类的指针);堆栈还存储了布尔值,如果整数是非负数,则它指向一个字符串,如果是负数,则它是一个布尔值(不提倡这样做) |
该虚拟机有三个主要的接口函数:Reset、Read、Execute:
接口 |
说明 |
Reset |
重新初始化虚拟机 |
Read |
用于读取指令 |
Execute |
执行当前在内存中的程序,其内有一个指令指针,根据当前指令,使用switch-case语句执行正确的代码 |
有关其他内容请参见原文。
该堆栈虚拟机源代码共包含五个文件,其中三个头文件和两个源文件:
文件 |
名称 |
说明 |
头文件 |
mystring.h |
字符串类 |
stack.h |
堆栈类 |
|
vm.h |
虚拟机类声明 |
|
源文件 |
vm.cpp |
虚拟机类实现 |
main.cpp |
测试 |
我们首先介绍字符串类mystring.h和堆栈类stack.h。
mystring.h中手工实现了一个字符串类,该字符串类中成员变量只有一个私有型的char* s,该类支持一下操作:
a) 默认构造函数
b) 含有char* _s参数的构造函数
c) 析构函数
d) 用于返回内部字符串指针的Val
e) 计算字符串长度Len
f) 参数为String& n的赋值函数
g) 参数为char* n的赋值函数
h) 参数为String& n的字符串连接函数Concatenate
i) 参数为char* n的字符串连接函数
j) 打印Print
k) 输入Input
stack.h定义了一个堆栈类模板Stack<>
堆栈中每个元素为
template <class t>
class ll_link
{
public:
t data;
ll_link* next;
};
从该元素的定义我们可以得知该堆栈是链表型,而非数组型。
Stack中有两个私有的成员变量,分别为ll_link<t>* llstart, int n,其中 llstart用于标示栈顶元素,n标示堆栈中总共有几个元素。Stack支持以下操作:
a) 默认构造函数
b) 析构函数
c) 清空函数Empty
d) 压栈操作Push
e) 弹栈操作Pop
f) 获取栈顶元素的值GetTop,该函数是inline型的
g) 获取堆栈中第n个元素的值GetNo,起始号为0,栈顶元素号为n-1
h) 对堆栈中每个元素进行统一处理DoForAll(void(*process)(t))
i) 对堆栈中每个元素进行统一处理DoForAll(void(*process)(t, void*), void* arg)
j) 计算堆栈中元素的个数Len,该函数也是inline型的
k) 判断堆栈是否为空IsEmpty,该函数是inline型的
另外,还重载了两个友元的操作符 <<,用于Push和Pop操作:
a) 用于Push操作,重载<<操作符operator << (Stack<t>& stack, t node),该函数是inline型的
b) 用于Pop操作,重载<<操作符operator << (t& node, Stack<t>& stack),该函数也是inline型的
下面我们重点分析vm.h和vm.cpp有关虚拟机类声明和实现的两个文件。
vm.h文件中首先定义了一个枚举类型的操作码:
操作码 |
值 |
说明 |
OP_NOP |
0 |
无操作 |
OP_PUSH |
1 |
压栈 |
OP_GETTOP |
2 |
获取栈顶元素值 |
OP_DISCARD |
3 |
弹栈 |
OP_PRINT |
4 |
打印 |
OP_INPUT |
5 |
输入 |
OP_JMP |
6 |
无条件跳转 |
OP_JMPF |
7 |
如果为非,则跳转 |
OP_STR_EQUAL |
8 |
检测两个字符串是否相等 |
OP_BOOL_EQUAL |
9 |
检测两个布尔型量是否相等 |
OP_CONCAT |
10 |
连接两个字符串 |
OP_BOOL2STR |
11 |
布尔型转换字符串型 |
JUMPTARGET |
12 |
非操作码,用于跳转 |
紧接着定义了一个指令类Instr,该指令类中有两个成员变量:操作码opcode和操作数operand。另外,还定义了三个构造函数。
然后就是定义了虚拟机类VMachine。VMachine中定义了四个私有型的成员变量:
成员变量 |
说明 |
备注 |
Instr* instr |
记录虚拟机中的指令 |
对应于开头说的指令数组 |
int ninstr |
记录指令的数目 |
|
String* str[MAX_STR] |
字符串表,MAX_STR=100 |
对应于开头说的字符串表,这里的字符串表相当于内存 |
VM_Stack stack |
堆栈 |
对应于开头说的堆栈 |
VMachine中定义了四个用于字符串拷贝的私有型成员函数NewTempCopy()、NewTempCopy(int j)、NewTempCopy(char* s)、DelTempCopy(int i)。
下面我们分析VMachine中的五个公有型函数,其中主要是三个接口函数Read、Execute、Reset。
构造函数VMachine
析构函数~VMachine
重新初始化函数Reset:将字符串表str中的元素全部清空并置为0,将指令数组instr清空,将堆栈stack清空
读取指令函数Read:这里是预先设定了指令。首先对字符串表str进行赋值,然后对指令数组instr进行赋值。该函数中没有对堆栈stack进行操作。Read对str和instr操作的说明如下,首先对字符串表str进行存储数据:
str[0] = new String("Answer to the Ultimate Question"
" of Life, the Universe and Everything > ");
str[1] = new String;
str[2] = new String("42");
str[3] = new String("Right! ");
str[4] = new String("Wrong! ");
然后对指令数组instr进行指令编排:
ninstr = 11;
instr = new Instr[ninstr];
instr[0] = Instr (OP_PUSH, 0); 将str中第0个字符压栈,实际压的是5,因为调用了NewTempCopy
instr[1] = Instr (OP_PRINT); 将栈顶元素输出,并将str[5]清空
instr[2] = Instr (OP_INPUT, 1); 对str[1]进行输入
instr[3] = Instr (OP_PUSH, 1); 将str[1]压栈,实际压的是5
instr[4] = Instr (OP_PUSH, 2); 将str[2]压栈,实际压的是6
instr[5] = Instr (OP_STR_EQUAL); 将栈中元素两次弹栈,并比较两个值是否相等
instr[6] = Instr (OP_JMPF, 3); 如果相等,不相等则前进3条指令
instr[7] = Instr (OP_PUSH, 3); 将str[3]进行压栈,实际压得是5
instr[8] = Instr (OP_JMP, 2); 前进2条指令
instr[9] = Instr (OP_PUSH, 4); 将str[4]进行压栈,实际压得是5
instr[10]= Instr (OP_PRINT); 将栈顶元素输出,并将str[5]清空
以上指令的功能是首先将str[0]进行输出,然后对str[1]进行赋值,如果str[1]被赋的值等于str[2]则输出str[3],如果str[1]不等于str[2],则输出str[4]。
上述指令的执行情况为:
从执行结果上,我们可以看出PUSH压栈操作实际压得str索引是NewTempCopy新生成的索引,而非源索引。
执行指令函数Execute:根据OP_*操作码,利用switch-case语句进行相应的执行。处理的巧妙之处在于设定了指令计数器ip和指令递增量ipc,根据ip进行相应指令的执行。
有关Execute代码中存在一个问题:该函数内部自身有重新定义了一个VM_Stack stack,而不是用的VMachine中的成员stack,个人认为函数内部重新定义的stack是多余的,应将其删除。
以上就是对vm.h和vm.cpp的分析。现在就剩下最后一个文件:main.cpp测试代码。首先定义一个VMachine的对象,然后调用Read函数进行相关内存数据的载入和指令的输入。Read完后就是Execute了。
源程序中Read函数中是预先定义好了指定的内存数据载入和指令编排。可以修改Read函数从而实现手动收入内存数据载入和指令输入。
有关虚拟机的相关思考
在《简单虚拟机》中,我们介绍了一个基于寄存器的虚拟机实现;本文中我们对一个基于堆栈的虚拟机实现进行了源代码剖析。下面谈谈个人对虚拟机的相关理解。
我们这里谈的虚拟机可以说是最为上层的虚拟机,是程序实现层的虚拟,用于虚拟一个程序的运行平台或是环境。程序运行无非是需要数据和指令两部分(对应于操作数和操作码)。一个虚拟机的实现,不是是基于寄存器的,还是基于堆栈的,这里的基于什么,实质上是实际的操作运算在哪里进行。也就是说基于寄存器的意思是,实际的操作需要像是在寄存器里操作那样进行,需要遵循寄存器操作的规则;基于堆栈的意思是,操作需要像是在堆栈中那样进行,需要遵循堆栈操作的规则。(我们这里的基于寄存器和基于堆栈都是虚拟的,程序的实际实现还是在内存中进行的,所谓寄存器和堆栈都是相对的概念)。
在实现基于寄存器和基于堆栈的虚拟机时,都需要有“内存”这个东西,内存用于载入和存储“程序”所需的数据,以备指令来操作处理。指令具体操作的场所在“寄存器”或“堆栈”中,从内存与寄存器或堆栈之间的数据来往对应于Load/Store和Push/Pop操作。内存相当于仓库,用于存储数据。寄存器和堆栈是数据的加工地点。
通过以上分析和总结,我们可以归纳实现一个虚拟机需要以下几部分:
1.内存,用于存储代加工或已加工数据
2.数据加工地点,可选用寄存器也可选用堆栈等
3.指令集,根据数据加工地点选择,涉及不同的指令,主要涉及内存到数据加工地点的指令、数据加工地点到内存的指令、输入指令、输出指令、算术运算指令、跳转指令等
4.一小段测试指令,用于验证虚拟机设计和实现是否正确合理
通过以上的讨论,我们对相关虚拟机的知识有了进一步的理解和体会。虚拟机方面的内容还有能多东西需要学习和研究。
本文我们主要是通过对一栈虚拟机源码剖析,学习了虚拟机相关实现细节和技巧。下一步需要自己手动实现一个不需要自定义string和stack,直接利用库提供的string和stack的基于堆栈的虚拟机,然后再根据自定义的string和stack实现另一个版本的基于堆栈的虚拟机。
另外,虚拟机还包括很多内容,比如存储分配allocation schemes、垃圾收集garbage collection、虚拟机的稳定和高速等等,有待我们进一步学习。