1. 程式人生 > >QEMU原始碼分析系列(三)

QEMU原始碼分析系列(三)

 

從QEMU-0.10.0開始,TCG成為QEMU新的翻譯引擎,使QEMU不再依賴於GCC3.X版本,並且做到了“真正”的動態翻譯(從某種意義上說,舊版本是從編譯後的目標檔案中複製二進位制指令)。TCG的全稱為“Tiny Code Generator”,QEMU的作者Fabrice Bellard在TCG的說明檔案中寫到,TCG起源於一個C編譯器後端,後來被簡化為QEMU的動態程式碼生成器(Fabrice Bellard以前還寫過一個很牛的編譯器TinyCC)。實際上TCG的作用也和一個真正的編譯器後端一樣,主要負責分析、優化Target程式碼以及生成Host程式碼。

Target指令 ----> TCG ----> Host指令

以下的講述以X86平臺為例(Host和Target都是X86)。

我在上篇文章中講到,動態翻譯的基本思想就是把每一條Target指令切分成為若干條微指令,每條微指令由一段簡單的C程式碼來實現,執行時通過一個動態程式碼生成器把這些微指令組合成一個函式,最後執行這個函式,就相當於執行了一條Target指令

這種思想的基礎是因為CPU指令都是很規則的,每條指令的長度、操作碼、運算元都有固定格式,根據前面就可推匯出後面,所以只需通過反彙編引擎分析出指令的操作碼、輸入引數、輸出引數等,剩下的工作就是編碼為目標指令了。

那麼現在的CPU指令這麼多,怎麼知道要分為哪些微指令呢?其實CPU指令看似名目繁多,異常複雜,實際上多數指令不外乎以下幾大類:

資料傳送、算術運算、邏輯運算、程式控制;

例如,資料傳送包括:傳送指令(如MOV)、堆疊操作(PUSH、POP)等

程式控制包括:函式呼叫(CALL)、轉移指令(JMP)等;

基於此,TCG就把微指令按以上幾大類定義(見tcg/i386/tcg-target.c),例如:其中一個最簡單的函式 tcg_out_movi 如下:

// tcg/tcg.c
static inline void tcg_out8(TCGContext *s, uint8_t v)
{
*s->code_ptr++ = v;
}

static inline void tcg_out32(TCGContext *s, uint32_t v)
{
*(uint32_t *)s->code_ptr = v;
s->code_ptr += 4;
}

// tcg/i386/tcg-target.c
static inline void tcg_out_movi(TCGContext *s, TCGType type,
int ret, int32_t arg)
{
if (arg == 0) {
/* xor r0,r0 */
tcg_out_modrm(s, 0x01 | (ARITH_XOR << 3), ret, ret);
} else {
tcg_out8(s, 0xb8 + ret); // 輸出操作碼,ret是暫存器索引
tcg_out32(s, arg); // 輸出運算元
}
}

0xb8 - 0xbf 正是x86指令中的 mov R, Iv 系列操作的16進位制碼,所以,tcg_out_movi 的功能就是輸出 mov 操作的指令碼到緩衝區中。可以看出,TCG在生成目標指令的過程中是採用硬編碼的,因此,要讓TCG執行在不同的Host平臺上,就必須為不同的平臺編寫微操作函式。

接下來,我還是以一條Target指令 jmp f000:e05b 來講述它是如何被翻譯成Host指令的。其中幾個關鍵變數的定義如下:

gen_opc_buf: 操作碼緩衝區
gen_opparam_buf:引數緩衝區
gen_code_buf: 存放翻譯後指令的緩衝區
gen_opc_ptr、gen_opparam_ptr、gen_code_ptr三個指標變數分別指向上述緩衝區。

jmp f000:e05b 的編碼是:EA 5B E0 00 F0,

首先是disas_insn()函式翻譯指令,當碰到第1個位元組EA,分析可知這是一條16位無條件跳轉指令,因此依次從後續位元組中得到offset和selector,然後分為如下微指令操作:

gen_op_movl_T0_im(selector);
gen_op_movl_T1_imu(offset);
gen_op_movl_seg_T0_vm(R_CS);
gen_op_movl_T0_T1();
gen_op_jmp_T0();

這幾個微指令函式的定義如下(功能可看註釋):

static inline void gen_op_movl_T0_im(int32_t val)
{
tcg_gen_movi_tl(cpu_T[0], val); // 相當於 cpu_T[0] = val
}

static inline void gen_op_movl_T1_imu(uint32_t val)
{
tcg_gen_movi_tl(cpu_T[1], val); // 相當於 cpu_T[1] = val
}

static inline void gen_op_movl_seg_T0_vm(int seg_reg)
{
tcg_gen_andi_tl(cpu_T[0], cpu_T[0], 0xffff); // cpu_T[0] = cpu_T[0]&0xffff
tcg_gen_st32_tl(cpu_T[0], cpu_env,
offsetof(CPUX86State,segs[seg_reg].selector)); // the value of cpu_T[0] store to the 'offset' of cpu_env
tcg_gen_shli_tl(cpu_T[0], cpu_T[0], 4); // cpu_T[0] = cpu_T[0]<<4
tcg_gen_st_tl(cpu_T[0], cpu_env,
offsetof(CPUX86State,segs[seg_reg].base)); // the value of cpu_T[0] store to the 'offset' of cpu_env
}

static inline void gen_op_movl_T0_T1(void)
{
tcg_gen_mov_tl(cpu_T[0], cpu_T[1]); // cpu_T[0] = cpu_T[1]
}

static inline void gen_op_jmp_T0(void)
{
tcg_gen_st_tl(cpu_T[0], cpu_env, offsetof(CPUState, eip)); // // the value of cpu_T[0] store to the 'offset' of cpu_env
}

其中,cpu_T[0]、cpu_T[1]和前面講過的T0、T1功能一樣,都是用來臨時儲存的變數。在32位目標機上,tcg_gen_movi_tl 就是 tcg_gen_op2i_i32 函式,它的定義如下:

static inline void tcg_gen_op2i_i32(int opc, TCGv_i32 arg1, TCGArg arg2)
{
*gen_opc_ptr++ = opc;
*gen_opparam_ptr++ = GET_TCGV_I32(arg1);
*gen_opparam_ptr++ = arg2;
}

static inline void tcg_gen_movi_i32(TCGv_i32 ret, int32_t arg)
{
tcg_gen_op2i_i32(INDEX_op_movi_i32, ret, arg);
}

gen_opparam_buf 是用來存放運算元的緩衝區,它的存放順序是:第1個4位元組代表s->temps(用來存放目標值的陣列,即輸出引數)的索引,第2個4位元組及之後位元組代表輸入引數,對它的具體解析過程可見 tcg_reg_alloc_movi 函式,示例程式碼如下:

TCGTemp *ots;
tcg_target_ulong val;

ots = &s->temps[args[0]];
val = args[1];

ots->val_type = TEMP_VAL_CONST;
ots->val = val; // 把輸入值暫時存放在ots結構中

接下來,根據 gen_opc_buf 儲存的操作碼列表,gen_opparam_buf 儲存的引數列表,以及TCGContext結構,經過 tcg_gen_code_common 函式呼叫,jmp f000:e05b 生成的最終指令如下:

099D0040 B8 00 F0 00 00 mov eax,0F000h
099D0045 81 E0 FF FF 00 00 and eax,0FFFFh
099D004B 89 45 50 mov dword ptr [ebp+50h],eax
099D004E C1 E0 04 shl eax,4
099D0051 89 45 54 mov dword ptr [ebp+54h],eax
099D0054 B8 5B E0 00 00 mov eax,0E05Bh
099D0059 89 45 20 mov dword ptr [ebp+20h],eax
099D005C 31 C0 xor eax,eax
099D005E E9 25 5D CA 06 jmp _code_gen_prologue+8 (10675D88h) /* 返回 */

從上面可以看出,生成的Host程式碼很簡潔,對於Target機的JMP,Host沒有去執行真正的跳轉指令,而只是簡單的將目標地址放到EIP中而已。

QEMU維護著一個稱為 CPUState 的資料結構,這個結構包括了Target機CPU的所有暫存器,像EAX,EBP,ESP,CS,EIP,EFLAGS等。

它總是代表著Target機的當前狀態,我用env變數來表示 CPUState 結構,

QEMU每次解析Target指令時,總是以 env.cs+env.eip 為開始地址的。

像上面說的jmp f000:e05b指令,它分解為如下微操作:

gen_op_movl_T0_im(selector);
gen_op_movl_T1_imu(offset);
gen_op_movl_seg_T0_vm(R_CS);
gen_op_movl_T0_T1();
gen_op_jmp_T0();

幾條微操作的意義概括起來很簡單,就是把selector放到env.cs,把offset放到env.eip在除錯中,把QEMU執行Target指令的過程和Bochs比較是一件很有趣的事情,當然,這只是設計理念的不同,而並沒有技術上的優劣之分。