• 栈虚拟机源码剖析


    栈虚拟机源码剖析

             之前我们介绍过一个《简单虚拟机》,该虚拟机是基于寄存器的。

       本文我们剖析一个栈虚拟机的源代码。该代码来自于《实现一个脚本引擎》中的《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、虚拟机的稳定和高速等等,有待我们进一步学习。

  • 相关阅读:
    Java多态的学习
    Java显示的静态初始化和非静态实例初始化
    Java类方法访问权限
    JavaScript数组详解
    JavaScript倒计时的具体实现
    JavaScript之Date对象
    JavaScript内置对象之Math对象
    JavaScript对象
    JS的作用域,作用域链及预解析
    JavaScript函数
  • 原文地址:https://www.cnblogs.com/unixfy/p/3335874.html
Copyright © 2020-2023  润新知