写在前面
计算机系统建立在不同层次的抽象上,上层的抽象不需要关注底层的实现细节,例如文件是对磁盘的一种抽象。计算机系统的体系结构如下图所示:
软件和硬件通过ISA来进行交互,ISA由接口7和8组成。另外两个重要的概念是ABI(Application Binary Interface,应用二进制接口,由接口3和7组成)和API(Application Program Interface,应用程序接口,由接口2和7组成)
什么是虚拟机
虚拟机是真实机器和虚拟软件的结合,所以弄清楚“机器”和“虚拟软件”的概念非常重要。
在计算机系统体系结构中,关注的对象不同,“机器”所对应的含义也有所不同。从应用程序(进程)的角度来看,机器是ABI接口之下的具体实现,也就是操作系统与底层的一部分硬件组合而成。从操作系统的角度来看,机器仅由底层硬件实现,指令集提供了操作系统与机器之间的接口。
虚拟机软件介于真实机器和用户软件之间,负责对真实机器进行虚拟化,通过虚拟化,虚拟机可以执行为某种机器开发的软件,就像在这种机器上执行一样。虚拟化过程包含两个部分:
(1)把虚拟资源或状态,如寄存器、存储器、文件,映射成底层机器中的真实资源
(2)使用真实机器的指令和/或系统调用来执行虚拟机要执行的指令和/或系统调用
虚拟机类型
虚拟机软件可以针对进程和操作系统进行虚拟化,相应的也就有了进程虚拟机和系统虚拟机。如下图所示:
我们平时比较熟悉的VMWare和JVM都属于进程虚拟机
虚拟机的基本实现方法
虚拟机一般基于仿真来实现,仿真的方法分为两种,一种是解释,另外一种是二进制翻译。
解释—译码分派
译码分派是一种比较简单的解释器实现思路,基本思路就是逐条指令地执行源程序,围绕一个主循环来组织,即译码一条指令,然后将其分派给对应的解释程序,示例代码如下:
while(!halt && !interrupt) {
inst = code[PC];
opcode = extract(inst, 31, 6);
switch(opcode) {
case LoadWordAndZero : loadWordAndZero(inst);
case ALU: ALU(inst);
case Branch: Branch(inst);
......
}
}
Instruction function list
loadWordAndZero(inst) {
RT = extract(inst, 25, 5);
RA = extract(inst, 20, 5);
displacement = extract(inst, 15, 16);
if (RA = 0) source = 0;
else source = regs[RA];
address = source + displacement;
regs[RT] = (data[address] << 32) >> 32;
PC = PC + 4;
}
ALU(inst) {
RT = extract(inst, 25, 5);
RA = extract(inst, 20, 5);
RB = extract(inst, 15, 5);
source1 = regs[RA];
source2 = regs[RB];
extended_opcode = extract(inst, 20, 10);
switch(extended_opcode) {......}
PC = PC + 4;
}
译码-分派解释器的性能不好,主要是因为包含的比较多的直接或者间接的分支指令,如主分派循环中的switch、循环顶部的判断条件、解释程序返回、循环结束,这些分支指令依靠硬件实现,往往会降低性能。于是出现了间接线索解释
解释—间接线索解释
间接线索解释消除了主分派循环,通过在每一个解释程序的末尾读取下一条指令的操作码,利用分派表来查找相应的解释程序地址,并跳转到相应的解释程序,如下所示:
Instruction function list
loadWordAndZero(inst) {
RT = extract(inst, 25, 5);
RA = extract(inst, 20, 5);
displacement = extract(inst, 15, 16);
if (RA = 0) source = 0;
else source = regs[RA];
address = source + displacement;
regs[RT] = (data[address] << 32) >> 32;
PC = PC + 4;
// 以下是相对于 译码-分派的主要区别
if(halt || interrupt) goto exit;
inst = code[PC];
opcode = extract(inst, 31, 6);
extended_opcode = extract(inst, 10, 10);
routine = dispatch[opcode, extended_opcode];
goto *routine;
}
尽管间接线索解释消除了主分派循环,但是集中分派表还是会带来开销,依然存在分支指令(从分派表中查找解释程序)。为了获得更高的效率,还需要消除对集中分派表的访问。
解释—预译码和直接线索解释
要消除对集中分派表的访问,需要在当前的解释程序中能够直接定位到下一个指令的解释程序的地址。如下所示:
PC = PC + 4;
if(halt || interrupt) goto exit;
// 要替换的片段
// inst = code[PC];
// opcode = extract(inst, 31, 6);
// extended_opcode = extract(inst, 10, 10);
// routine = dispatch[opcode, extended_opcode];
// 将以上的片段替换成以下的片段
routine = code[PC].op
goto *routine;
要达到上面的效果,我们需要在“要替换的片段”在第一次执行之后将提取的信息保存起来,方便下次执行的时候复用,这个过程就是预译码。在以上“要替换的片段”中,我们还需要知道下一条指令所对应的解释程序的地址,因此还需要将整个解释器的代码都进行预译码。
预译码之后的中间信息是中间代码(与源代码对应),中间代码中包含每个解释器程序的实际地址,有了下一个解释程序的实际地址之后,就不需要查找分派表进行跳转了,也就是可以进行直接线索解释了
二进制翻译
经过预译码之后生成的中间代码依然需要逐条解释各个指令,如果更进一步,直接将要仿真的源程序转换成底层机器可以直接执行的二进制程序,那么执行效率会更快,这个过程就称为二进制翻译
总结
以上的仿真方法可以用以下的图来进行总结: