第八章 虚拟机.docx
- 文档编号:23739584
- 上传时间:2023-05-20
- 格式:DOCX
- 页数:15
- 大小:55.87KB
第八章 虚拟机.docx
《第八章 虚拟机.docx》由会员分享,可在线阅读,更多相关《第八章 虚拟机.docx(15页珍藏版)》请在冰豆网上搜索。
第八章虚拟机
第八章虚拟机
虚拟机技术并不是一项全新的技术。
现在我们经常可以看到很多的虚拟机。
首先,比如说我们很熟悉的JAVA虚拟机、还有GWBasic解释器、MicrosoftWord的WordBasic宏解释器等等。
虚拟机的应用场合很多,它的主要作用是能够运行一定规则的描述语言。
虚拟机的“虚拟”二字,有着两方面的含义:
其一是指运行一定规则的描述语言的机器并不一定是一台真实地以该语言为机器代码的计算机,比如JAVA想做到跨平台兼容,那么每一种支持JAVA运行的计算机都要运行一个解释环境,这就是JAVA虚拟机;另一个含义是指运行对应规则描述语言的机器并不是该描述语言的原设计机器,这种情况也称为仿真环境。
比如Windows的MS-DOSPrompt就是工作在V86方式的一个虚拟机,虽然在V86方式,实x86指令的执行和在实地址方式非常相似,但是Windows为MS-DOS程序提供了仿真的(相对于物理1M以下内存是虚假的)内存空间。
跨越计算机平台的虚拟机也有很多,比较典型的是在很多Unix下运行MS-DOS或Windows程序的仿真器。
在一台非WinTel计算机上运行MS-DOS应用程序,首先需要对MS-DOS应用程序所使用的x86指令进行解释执行,并要提供完整的仿真MS-DOS中断、功能调用和绝大多数BIOS调用,并要解决MS-DOS环境所使用的内存特点。
根据仿真的彻底程度不同,所获得的兼容性也是不同的。
如果要仿真MS-Windows程序的运行环境,除了上述工作以外,基本上要完整地再做一份完全兼容的WindowsAPI,并且会遇到DDE、OLE、DirectX等类似的令人头疼的兼容性问题。
同时,仿真运行的程序必定是以比宿主计算机慢得多的速度来运行的。
因此,我们大致可以看到一个比较完整的虚拟机需要在很多的层次上做仿真,总的来说是分为“描述仿真”和“环境仿真”两大部分。
最简单的仿真环境几乎不能算是虚拟机:
比如为了运行使用磁盘加密而制作的钥匙盘仿真驻留程序,它仅仅做一些诸如修改Int13这样的小手段,而不必包括指令执行的“描述仿真”;而象Sourcer这样的反汇编工具则是更注重指令解释,而不必关心程序的运行结果,因此在环境仿真上所做的工作要少得多。
完整、复杂的虚拟机是几乎没有尽头的,假想我们要在一台Unix计算机上运行一个看VCD的Windows95程序,或是运行一个使用8259中断、8237DMA的程序……,有些仿真在理论上是可以实现的,有些则很可能行不通。
所以,一个实用的虚拟机是根据需要和可能这两方面的因素来构造的,要权衡时间/空间复杂度、仿真兼容性、运行性能和代价等诸多因素,根据实际情况来设计和实现。
8.1虚拟机结构设计
粗略的介绍完虚拟机的概念以后,接下来将介绍在我们设计的虚拟机中是如何实现对“仿真”的实现的,在我们设计的虚拟机中,它主要包含下面几个部分:
一个完整的指令系统、一组寄存器、一个栈、一个用于对象存储的堆、一组符号表和一个代码存储区。
8.1.1指令系统
这里只简单介绍各条指令,详细的说明将在附录1汇编指令的使用说明中给出:
MOV
功能:
把源操作数送给目的操作数
语法:
MOV目的操作数,源操作数
PUSH
功能:
把操作数压入堆栈
语法:
PUSH操作数
POP
功能:
把操作数取出堆栈
语法:
POP操作数
ADD
功能:
加法指令
语法:
ADDOP1,OP2
SUB
功能:
减法指令
语法:
SUBOP1,OP2
INC
功能:
把OP的值加一或减一
语法:
INCOPDECOP
DEC
功能:
把OP的值减一
语法:
DECOP
MUL
功能:
乘法指令
语法:
MULOP1,OP2
DIV
功能:
除法指令
语法:
DIVOP1,OP2
JMP
功能:
无条件跳往指定地址执行
语法:
JMP地址
条件转移指令JXX
功能:
当特定条件成立则跳往指定地址执行
语法:
JXX地址
说明:
JA/JNBE不小于或不等于时转移.
JAE/JNB大于或等于转移.
JB/JNAE小于转移.
JBE/JNA小于或等于转移.
JG/JNLE大于转移.
JGE/JNL大于或等于转移.
JL/JNGE小于转移.
JLE/JNG小于或等于转移.
JE/JZ等于转移.
JNE/JNZ不等于时转移.
CALL
功能:
子程序调用指令
语法:
CALL地址
VCALL
功能:
虚函数调用指令
语法:
VCALL地址
RET
功能:
子程序返回指令
语法:
RET/RETn
NEW
功能:
对象地址分配指令
语法:
NEWXXX
DELET
功能:
地址回收指令
语法:
DELETXXX
HALT
功能:
停机指令
语法:
HALT
THROW
功能:
异常抛出指令
语法:
THROWXXX
8.1.2寄存器设计
在我们的虚拟机中,寄存器分为操作寄存器和通用寄存器两大类:
1.操作寄存器
操作寄存器通常用于虚拟机自身运行时的控制数据暂存:
Bp:
栈底指针寄存器
Pc:
指令地址寄存器
Sp:
栈顶指针寄存器
Hp:
堆指针寄存器
2.通用寄存器
通用寄存器通常用于虚拟机运行时的运算数据暂存:
AX,BX,CX
8.1.3.寻址方式
1.立即寻址
立即操作数可以是一个整数,并且是指令的一部分。
立即数据总是紧跟在指令操作码之后并和操作码一起存放在代码段中,因而立即数据总是和操作码一起被放入指令队列里,在指令执行时不需再存取存储器。
使用立即寻址的指令主要用来给寄存器赋初值,
例如:
MOVAX,5
执行结果将使寄存器AX中的值为5。
2.寄存器寻址
操作数存放在指令规定的寄存器中,在我们的虚拟机中,寄存器可以是AX,BX,CX,BP,PC,SP
例如:
MOVAX,BX
执行结果将使寄存器AX中的值为BX的值。
3.直接寻址
操作数的有效地址是指令的一部分,它与操作码一起存放在代码段中,但操作数一般是在数据段中,在我们的虚拟机中,只涉及了简单的处理,存储器的设计也相对的来说比较简单,因而这种寻址方式并不是像在80X86机器中一样,以DS的内容为基准,而是一栈的起始位置为基准。
例如:
MOVAX,[20]
这则指令式将栈中物理地址为20单元的内容传送AX寄存器。
4.寄存器间接寻址
操作数的地址存放在寄存器中,即AX,BX,CX,BP,PC,SP
例如:
MOVAX,[BX]
它表示操作数在数据段中,存放单元地址在寄存器BX中。
这则指令式将栈中物理地址为BX的内容的单元的值传送AX寄存器。
5.寄存器相对寻址方式
操作数的地址是一个整数的位移量与基址指示器BP或某个变址寄存器之和,例如:
MOVAX,[BP+5]
它是BP的内容加上5的结果作为操作数存放的单元地址。
这则指令式将栈中物理地址BP的内容加上5的单元的值传送AX寄存器。
8.1.4内存设计
在我们的虚拟机中,存储区主要有三个部分构成:
全局变量存储区、活动记录区、对象存储区(堆)。
1.全局变量存储区
在我们的虚拟机中,每个程序它都使用栈最底部的那几个单元来作为全局变量存储区。
这样做的原因是和后面的活动记录区设计联系在一起的。
因为在活动纪录区,存储空间的分配大小是与具体的函数参数相关的,并且它们是生存期也会随着函数的返回而结束。
2.活动记录区
在活动记录区中包含的信息用于对象的动态链接,正常的方法返回以及异常传播。
·动态链接
运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。
方法的class文件代码在引用要调用的方法和要访问的变量时使用符号。
动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。
动态链接方法和变量使得方法中使用的其它类的变化不会影响到本程序的代码。
·正常的方法返回
如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。
执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。
3.对象存储区(堆)
目前的语言中尚不需要在堆中分配空间;因此在这里不做介绍。
4.栈中内容介绍
栈中内容如右图8.1所示:
从栈的最底部开始,我们在那里存储了程序中的全局变量,将需要对全局变量进行访问时,我们首先通过查询符号表,获取它在栈中相对栈底的偏移位置,然后直接读取这个单元的内容即可。
分配了全局变量之后,就是程序的活动记录区,每个函数的活动记录格式如下:
Bp:
栈底指针
Pc:
返回地址
Localvars:
局部变量
Params:
形式参数
图8.1栈中的内容
8.2虚拟机运行过程
在前一节中,我们介绍了虚拟机的结构,在接下来的内容中,我们将详细地讲述虚拟机怎样执行我们生成的代码。
8.2.1程序的装入
第一步:
对源文件进行语法分析、词法分析……直至生成目标代码。
我们在这里使用一个文件名位t.cm的测试源文件,它的内容为:
voidmain()
{
inta;
intb;
a=4;
b=5+a;
}
最后我们将得到一组文件:
t_syntaxTree.xml,t_SymbolTable.xml,t_asm.xml,t.err,t.xml,asm.txt。
第二步:
运行虚拟机程序,我们将见到以下的界面:
图8.2界面
第三步:
点击“装入元数据”按键,将符号表数据导入,即文件t_SymbolTable.xml。
第四步:
点击“装入中间代码”按键,将指令数据导入,即文件t_asm.xml。
到这里,程序的装入工作就已经完成了,然后我们点击“单步执行”按键,就可以运行代码了。
8.2.2程序的执行
首先,我们来看看生成的代码:
line0:
CALLNEARPTR_MAIN
line1:
HALT
line2:
main:
MAINPROCNEAR
line3:
ADDSP6
line4:
MOV[BP+2]0
line5:
MOV[BP+3]0
line6:
MOVAX-1
line7:
MOV[BP+4]AX
line8:
MOV[BP+5]SP
line9:
MOVAX4
line10:
MOV[BP+6+0+0]AX
line11:
MOVAX[BP+6+0+0]
line12:
MOVBXAX
line13:
MOVAX5
line14:
ADDAXBX
line15:
MOV[BP+6+1+0]AX
line16:
RET
line17:
MAINENDP
在虚拟机的初始阶段,寄存器PC是指在指令的第一行的,即line0,也就是说,虚拟机是从第一行代码开始执行的。
STEP1:
调用_MAIN函数:
当前的函数栈基址BP、函数返回地址PC进栈,通过查表得到_MAIN函数的地址,line2,PC转至line2;
STEP2——STEP8:
_MAIN函数活动纪录的初始化;
STEP3:
根据符号表中_MAIN函数的信息,设置栈顶指针;
STEP4:
初始_MAIN函数信息;
STEP5:
初始_MAIN函数回退信息;
STEP6——STEP7:
因为是_MAIN函数,置BP+4位置值为-1;
STEP8:
保存当前栈顶指针;
STEP9——STEP10:
借助AX寄存器,初始变量a的值;
STEP11:
准备进行加法操作,先将变量a的值从栈中取到寄存器AX中;
STEP12:
然后将a的值寄存器AX中赋给BX;
STEP13:
将另一个操作数“5”,传给寄存器AX;
STEP14:
进行加法操作,结果保存在AX中;
STEP15:
将保存在AX中的结果传回b在栈上的单元
STEP16:
_MAIN函数执行完,清除活动纪录,退栈,返回返回地址,PC转至line1
STEP17:
停机指令,结束执行。
8.3核心代码的实现
在这一节中,我们将更加深入的介绍虚拟机的执行过程,也就是,单个指令如何在虚拟机中实现。
8.3.1算术运算指令:
Add
Add指令是虚拟机中的加法运算指令,它在代码中的格式如下:
AddDes,Source
其中,Des为目的操作数,Source为源操作数;Des的寻址范围为Ax,Bx,Sp寄存器,Source的寻址范围为Ax,Bx,Sp寄存器和存储器空间。
这条指令的运行结果为,将目的操作数与源操作数相加,并将结果放回目的操作数中保存。
所以,在虚拟机中,我们可以按照如下方式来实现Add指令:
FunctionAdd(des,source)
Val←ArgValue(source);//根据寻址方式获取源操作数
Casedesof
AX:
cpu.ax←val+cpu.ax;
break;
BX:
cpu.bx←val+cpu.bx;
break;
SP:
cpu.sp←val+cpu.sp;
break;
default:
break;
8.3.2跳转指令:
Jmp
Jmp指令是虚拟机中的加法运算指令,它在代码中的格式如下:
JmpDes,
其中,Des为该指令唯一操作数;Des的寻址范围为Ax,Bx,Sp寄存器和存储器空间。
这条指令的运行结果为,虚拟机跳转到操作数表明的地址处继续执行。
FunctionJmp(des)
Addr←ArgValue(des);//根据寻址方式获取操作数
cpu.pc←Addr;//将sp寄存器的值置为跳转地址
所以,在虚拟机中,我们可以按照如下方式来实现Jmp指令:
8.3.3条件跳转指令:
Je
Je指令是虚拟机中的加法运算指令,它在代码中的格式如下:
JeDes,
其中,Des为该指令唯一操作数;Des的寻址范围为Ax,Bx,Sp寄存器和存储器空间。
这条指令的运行结果为,虚拟机跳转到操作数表明的地址处继续执行。
FunctionJe(des)
If(cpu.zf=0)//判断标志寄存器
Addr←ArgValue(des);//根据寻址方式获取操作数
cpu.pc←Addr;//将pc寄存器的值置为跳转地址
else
cpu.pc++;
所以,在虚拟机中,我们可以按照如下方式来实现Je指令:
8.3.4栈指令:
Push
Push指令是虚拟机中的加法运算指令,它在代码中的格式如下:
PushSource,
其中,Source为该指令唯一操作数;Source的寻址范围为Ax,Bx,Bp,Sp,This寄存器和立即数。
这条指令的运行结果为,将Source处的内容放入栈中。
FunctionPush(source)
Casedesof
AX:
Val←cpu.ax;
break;
BX:
Val←cpu.bx;
break;
BP:
Val←cpu.bp;
break;
SP:
Val←cpu.sp;
break;
THIS:
Val←cpu.this;
break;
default:
Val←int(Source);
break;
this.stack[cpu.sp]←Val;
cpu.sp++;
所以,在虚拟机中,我们可以按照如下方式来实现Push指令:
8.3.5函数调用指令:
Call
Call指令是虚拟机中的加法运算指令,它在代码中的格式如下:
CallFunname,
其中,Funname为该指令唯一操作数;Funname为符号表中存在的一个函数名。
这条指令的运行结果为,保存主调函数的信息;创建被调用函数的活动栈,并且,虚拟机从被调函数入口处继续执行。
所以,在虚拟机中,我们可以按照如下方式来实现Call指令:
FunctionPush(Funname)
this.stack[cpu.sp]←cpu.bp;//保存主调函数bp
cpu.bp←cpu.sp//修改bp寄存器为当前sp
this.stack[cpu.bp+1]←cpu.pc;//保存返回地址
cpu.sp+=2//sp寄存器移至栈顶
fun←Funname.Substring(i+1);//对函数名预处理
fs←fsGet(fun);//从符号表获取函数信息
entryaddr←fs.Entry_addr;//从函数信息获取入口地址
cpu.pc←entryaddr;//将pc寄存器的值置为入口地址
8.3.6函数返回指令:
Ret
Ret指令是虚拟机中的加法运算指令,它在代码中的格式如下:
Ret
这条指令的运行结果为,销毁被调用函数的活动栈,并且,虚拟机从主调函数返回地址处继续执行。
FunctionRet
cpu.pc←this.stack[cpu.bp+1];//将pc寄存器的值置为返回地址
cpu.sp←cpu.bp;//修改sp寄存器为当前bp
cpu.bp←this.stack[cpu.bp];//将bp寄存器的值置为保存的主调函数bp值
所以,在虚拟机中,我们可以按照如下方式来实现Ret指令:
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 第八章 虚拟机 第八