为了更容易理解动态翻译技术,我们暂时忽略掉qemu的其他模块,如用户交互模块,硬件模拟等模块,而是从数据结构的设计,数据结构之间的操作及其应用等方面来进行详细地分析,重点关注动态翻译器和微操作库(micro-ops library)的原理,至于细节的东西可以放在以后去深入分析。
qemu利用了一种可移植的动态代码翻译器以快速地完成客户代码的仿真。qemu本身并不能识别它主机体系结构的指令集,作为替代,每一个客户机指定一个c语言实现的微操作库以及一个客户机代码反汇编器和翻译器,用来将客户代码转换成微操作表,这些微操作可以被认为是一种虚拟机,尽管仅仅是对客户系统模拟的一种优化而已。另外,这些操作本身包括寄存器转化,显示的条件代码更新代码,按位操作,整型和浮点型数学函数,内存加载和存储操作等。
2.1 翻译
2.1.1 基本块的翻译
图1描述了一小段带条件跳转x86代码以及其对应的微操作指令表示,每一条op_开头的微操作指令都将被拷贝到翻译缓冲区中,微操作指令中看起来像参数的常量被称为折叠常量(const folding),在2.3.4接将会进行详细的解释,现在只要把它看成是qemu中定义的微操作指令的参数就可以了。微操作指令的参数中,tb是比较特殊的,它指向与当前翻译缓冲区相关的元数据。另外,JNZ指令对应的微操作指令看起来比较怪异,控制流处理相关的细节将会在2.2.3节和2.2.4节详细介绍。
2.1.2 同步的错误安全舱口
严格的按照“基本块”(basic block)的方式译码将会使得翻译缓冲区中包含一条或多条客户机指令,同时几乎每一条指令都可能产生同步的fault(比方说MMU fault)。为了便于理解,在翻译的时候我们重点考虑比较直接的客户机控制流(比方说,分支跳转:条件跳转和非条件跳转),毕竟,同步的错误(fault)很少出现。对于同步错误(fault)需要一种恢复机制,用来处理翻译缓冲区中的fault。qemu使用longjmp()从翻译缓冲区中跳到仿真器核心代码中。这里描述的意思是:当在执行翻译缓冲区的代码时,遇到了fault,就需要将执行路径切换到qemu的代码中,另外,当qemu处理完fault后,会重新创建一个翻译缓冲区。
2.1 翻译缓冲区的高超技巧
qemu采用了一些不同寻常的技巧来提高性能:
- 冗长的、额外开销大的主机操作(如仿真客户的MMU操作)并不会被直接放进翻译缓冲区,而是以可以从微操作中调用的帮助函数的形式存在
- 一方面减少了对翻译缓冲区的占用,另一方面简化了翻译的过程(qemu中事先保存某些固定指令的翻译结果,当动态翻译器碰到这些指令时,直接跳过去执行就行了)
- 翻译后的微操作指令序列的优化
- 启发式的译码和翻译
2.2.1 通过函数调用来达到节省翻译缓冲区的技术
目前操作系统中的MMU操作指令自身非常复杂。对于MMU操作会产生大量的访存操作,通常的解决方法是增加一个cache,但这进一步增加代码的复杂度。如果将每一次访存操作的这些复杂代码都放入翻译缓冲区中,其代价将会非常的昂贵。除了MMU操作指令,一些特殊的客户机指令也是非常复杂的,如CPUID指令,即使在简化的qemu的实现中,一条x86的CPUID指令都需要75行C代码来实现。与ARPL指令相比,CPUID对于不同的寄存器内容将会表现是完全不同的行为,因此,必须需要一大串冗长的微操作指令来实现CPUID的功能。
为了优化这种代价昂贵,需要冗长的微操作指令来实现的客户机指令,qemu采用能够实现类似函数调用功能的微操作指令的机制来实现,比如ldl_kernel,实现了内核模式的大量数据的读功能;helper_cpuid,这个微操作包含了CPUID指令的实现。
2.2.2 惰性赋值
每一条指令都会隐含性修改指令指针(EIP),并且每一条指令都会修改处理器条件码(比如零标志位,溢出标志位、进位标志位等),但实际的情况是,只要指令按照正确的顺序执行,其实客户代码很少去关心这些状态的变化。对于这些条件码除了条件跳转状态位外,我们很少去关注其他的状态位。图1中一段小小的客户代码都会引起指令指针的修改和条件码修改的大量操作:
- ADDL操作会修改条件码的零和进位/借位标志位
- SUBL操作会修改条件码的零和进位/借位标志位
- ADDL和SUBL都会修改EIP,使其指向下一条指令
由于qemu是按照“基本代码块”为单位进行翻译的,所以只有在整个“基本代码块”翻译完成或显示的读取EIP的时候才会对EIP进行更新,这就避免了EIP的频繁更新的问题。在实际情况下,ADDL和SUBL都不会去读指令指针EIP,因此,可以优化掉翻译后的微操作指令中对EIP更新的微操作,具体描述如图2所示。图2中仅仅是在每个“基本代码块”的最后通过op_jmp_im微操作来进行EIP的更新,即每个“基本代码块”只做一次更新操作。