Linux
分类: LINUX
2014-07-17 16:12:22
为了更容易理解动态翻译技术,我们暂时忽略掉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采用了一些不同寻常的技巧来提高性能:
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中一段小小 的客户代码都会引起指令指针的修改和条件码修改的大量操作:
由于qemu是按照“基本代码块”为单位进行翻译的,所以只有在整个“基本代码块”翻译完成或显示的读取EIP的时候才会对EIP进行更新,这就避 免了EIP的频繁更新的问题。在实际情况下,ADDL和SUBL都不会去读指令指针EIP,因此,可以优化掉翻译后的微操作指令中对EIP更新的微操作, 具体描述如图2所示。图2中仅仅是在每个“基本代码块”的最后通过op_jmp_im微操作来进行EIP的更新,即每个“基本代码块”只做一次更新操作。