QEMU技术分析1

发布于 2022-09-18 02:16:17 字数 5868 浏览 18 评论 0

QEMU技术分析1 - 动态翻译技术(dynamic translation)

QEMU的最大亮点就是动态翻译技术,正是由于这个强劲的引擎,使QEMU可以在不使用任何加速技术的情况下也能达到良好的速度,并能够横跨多种平台运行,借助于特定版本的GCC编译器,还能够仿真多种架构的处理器。这里我说的动态翻译指的是QEMU早期版本使用的“dynamic translation”,因为从0.10版本开始使用的是“TCG”,摆脱了对GCC版本的依赖,并且不再需要编译中间工具。

简单来说,动态翻译的基本思想就是把每一条x86指令切分成为若干条微指令,每条微指令由一段简单的C代码来实现(见'target-i386/op.c'),然后通过中间工具('dyngen')提取相应的目标文件('op.o')来生成一个动态代码生成器,最后把这些微指令组合成一个函数(见'op.h:dyngen_code()')。

在一个真实的CPU里,执行流程由取指、译指、执行指令三部分组成。在QEMU仿真的处理器中同样如此,取指和执行指令不需多说,关键的是译指这道工序,由反汇编器、dyngen程序、动态代码生成器三部分来共同完成。我的实验环境是X86平台+0.7.2版本源码,这里我以BIOS启动代码的第一条指令jmp f000:e05b来详细说明,该指令的汇编代码是:EA 5B E0 00 F0,反汇编器首先分析EA,知道这是一条16位的跳转指令,因此接着取出后面的EIP和CS。具体过程在 translate.c:disas_insn() 可见,它被分解为如下几条微指令:

gen_op_movl_T0_im(selector); // 把0xf000放到T0中
gen_op_movl_T1_imu(offset);  // 把0xe05b放到T1中
gen_op_movl_seg_T0_vm(offsetof(CPUX86State,segs[R_CS])); // 把T0的值放到env结构的CS段寄存器变量中
gen_op_movl_T0_T1(); // T1 -> T0
gen_op_jmp_T0(); // 跳转到T0
gen_op_movl_T0_0(); // 0 -> T0
gen_op_exit_tb(); // 返回

它们的实现函数分别如下:

static inline void gen_op_movl_T0_im(long param1)
{
    *gen_opparam_ptr++ = param1;
    *gen_opc_ptr++ = INDEX_op_movl_T0_im;
}

static inline void gen_op_movl_T1_imu(long param1)
{
    *gen_opparam_ptr++ = param1;
    *gen_opc_ptr++ = INDEX_op_movl_T1_imu;
}

static inline void gen_op_movl_seg_T0_vm(long param1)
{
    *gen_opparam_ptr++ = param1;
    *gen_opc_ptr++ = INDEX_op_movl_seg_T0_vm;
}                           

static inline void gen_op_movl_T0_T1(void)
{
    *gen_opc_ptr++ = INDEX_op_movl_T0_T1;
}

static inline void gen_op_jmp_T0(void)
{
    *gen_opc_ptr++ = INDEX_op_jmp_T0;
}

static inline void gen_op_movl_T0_0(void)
{
    *gen_opc_ptr++ = INDEX_op_movl_T0_0;
}

static inline void gen_op_exit_tb(void)
{
    *gen_opc_ptr++ = INDEX_op_exit_tb;
}

可以看出,以上函数都非常简单,其实就是在操作码缓冲区中放一个索引号。真正调用的函数在op.c中,如下:

void OPPROTO op_movl_T0_im(void)
{
    T0 = (int32_t)PARAM1;
}

void OPPROTO op_movl_T1_imu(void)
{
    T1 = (uint32_t)PARAM1;
}

void OPPROTO op_movl_seg_T0_vm(void)
{
    int selector;
    SegmentCache *sc;
   
    selector = T0 & 0xffff;
    /* env->segs[] access */
    sc = (SegmentCache *)((char *)env + PARAM1);
    sc->selector = selector;
    sc->base = (selector << 4);
}

void OPPROTO op_movl_T0_T1(void)
{
    T0 = T1;
}

void OPPROTO op_jmp_T0(void)
{
    EIP = T0;
}

void OPPROTO op_movl_T0_0(void)
{
    T0 = 0;
}

#define EXIT_TB() asm volatile ("ret")
void OPPROTO op_exit_tb(void)
{
    EXIT_TB();
}

在我的实验环境中,T0和T1的定义如下:
#define T0 (env->t0)
#define T1 (env->t1)
t0和t1都是长整型,分别是env结构的第1和第2个成员变量。上述函数被编译在目标文件op.o,在执行时经过 op.h:dyngen_code 动态翻译后,以上微指令运行在Host上的实际代码如下:

mov         eax,dword ptr [env (1FD1F14h)]         // -> gen_op_movl_T0_im(selector)
mov         dword ptr [eax],0F000h
mov         eax,dword ptr [env (1FD1F14h)]         // -> gen_op_movl_T1_imu(offset)
mov         dword ptr [eax+4],0E05Bh
mov         edx,dword ptr [env (1FD1F14h)]         // -> gen_op_movl_seg_T0_vm(offsetof(CPUX86State,segs[R_CS]))
mov         eax,dword ptr [edx]
and         eax,0FFFFh
mov         dword ptr [edx+58h],eax
shl         eax,4
mov         dword ptr [edx+5Ch],eax
mov         edx,dword ptr [env (1FD1F14h)]         // -> gen_op_movl_T0_T1()
mov         eax,dword ptr [edx+4]
mov         dword ptr [edx],eax
mov         edx,dword ptr [env (1FD1F14h)]         // -> gen_op_jmp_T0()
mov         eax,dword ptr [edx]
mov         dword ptr [edx+2Ch],eax
mov         eax,dword ptr [env (1FD1F14h)]         // -> gen_op_movl_T0_0()
mov         dword ptr [eax],0
ret                                                                        // -> gen_op_exit_tb()

现在可以清楚看到了,这就是Target上一条JMP指令在Host上的对应代码实现。

本来还应该再讲讲 rep、call 之类的指令,因为这也是QEMU比其它仿真器(如Bochs之类)快的原因之一,包括翻译后指令的重用、一次性执行多条Target指令、直接使用常量等特性,但是发现打字实在是很累,代码多了大家也看的眼花,所以就先说到这里吧。论坛上高手很多,希望有感兴趣的来一起讨论下。

[ 本帖最后由 vxasm 于 2009-8-19 17:16 编辑 ]

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

顾忌 2022-09-25 02:16:17

很好,我受这种思想的影响很大,所以在自己的方案里面也用了很多虚拟机,引擎,解释器一类的东西。
动态翻译是我喜欢的。

断桥再见 2022-09-25 02:16:17

放索引值是干什么用的?

嘿哥们儿 2022-09-25 02:16:17

不错,LZ 继续分析下去

煞人兵器 2022-09-25 02:16:17

好啊,“软化”

黑凤梨 2022-09-25 02:16:17

多谢老大的支持。

始终不够 2022-09-25 02:16:17

原帖由 emmoblin 于 2009-8-19 22:50 发表
放索引值是干什么用的?

我在文中所说的“动态代码生成器”指的是dyngen_code()函数,在这个函数中,将根据这些索引值从目标文件中提取出相应的二进制,从而组合成一段完整的代码,这段代码运行在Host上。

怪我闹别瞎闹 2022-09-25 02:16:17

动态翻译+优化是虚拟机的标准技术了,有些极端情况出现过虚拟出来的系统比原始系统更快,因为动态编译时可以根据执行情况做一些优化而静态编译不行。
目前我们用的编译器采用了类似的功能,叫Feedback优化,其核心也思想也就是先编译一次由编译器插入一些统计性操作,在目标平台上执行它统计数据,然后根据统计数据对源代码进行二次编译,它根据执行时的统计数据如Cache冲突等来进行优化。官方手册是讲差不多提高10%~30%,以前我测试过一个MD5小应用提高了13%,而有人测试过极端的有提高100%的;过于简单的程序没任何提高。目前好像仅此一家,以后说不定在编译器中也会流行起来。

屋檐 2022-09-25 02:16:17

原帖由 Cyberman.Wu 于 2009-8-20 14:00 发表
动态翻译+优化是虚拟机的标准技术了,有些极端情况出现过虚拟出来的系统比原始系统更快,因为动态编译时可以根据执行情况做一些优化而静态编译不行。
目前我们用的编译器采用了类似的功能,叫Feedback优化,其 ...

动态翻译这一块已经不是虚拟机的瓶颈了,以后主流的虚拟机都将会采用VT来做。

请恕我寡闻,不知你们的编译器叫什么?有介绍么?

泪之魂 2022-09-25 02:16:17

原帖由 vxasm 于 2009-8-20 15:08 发表

动态翻译这一块已经不是虚拟机的瓶颈了,以后主流的虚拟机都将会采用VT来做。

请恕我寡闻,不知你们的编译器叫什么?有介绍么?

两回事吧,VT能支持不同的CPU吗?它只是能使用同构的虚拟机如VMWare等执行更快,甚至可以在32位的XP中跑x86_64的系统(我跑过FreeBSD for AMD64),但不可能虚拟出一个PowerPC和ARM吧、

我们用的那个编译器是Tilera多核处理器的开发环境MDE中带的,没什么特别的名字,是专门针对Tilera的平台开发的,不过兼容GCC的扩展和选项。

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文