QEMU技術分析1 - 動態翻譯(dynamic translation)

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指令、直接使用常量等特性,但是發現打字實在是很累,代碼太多了大家也看的眼花,所以就先說到這裏吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章