开源开发工具技术(OSDT)博客

OSDT = HelloGCC + HelloLLVM

Copyright (c) 2011 陳韋任 (Chen Wei-Ren)

前言

因為工作上的關係,必須接觸 QEMU。雖然網路上有不少文件,但總覺得講得不夠深入。QEMU 是一個仿真器 (emulator),可以 process mode 或是 system mode 運行。process mode 可以運行不同 ISA 同一 OS 的 binary; system mode 可以在當前作業系統上運行另外一個 OS。我在收集各方資料,閱讀代碼和在郵件列表上發問之後,覺得略有心得。在此對 QEMU internal 作一個較為深入的介紹。憑我個人之力,難免有疏漏或是錯誤。權且當作拋磚引玉吧。希望各位不吝指教。

0. 術語、線上資源和技巧

對 QEMU 而言,被仿真的平台被稱為 guest,又稱 target; 運行 QEMU 的平台稱為 host。QEMU 是利用動態翻譯 (dynamic translation) 的技術將 guest binary 動態翻譯成 host binary,並交由 host 運行翻譯所得的 host binary。Tiny Code Generator (TCG) 是 QEMU 中負責動態翻譯的組件。對 TCG 而言,target 有不同的含意,它代表 TCG 是針對哪一個 host 生成 host binary。

網路上對 QEMU 有較為完整描述的文件為:

然而需要注意的是,上述文件在動態翻譯的部分均是針對 QEMU 0.9 版。QEMU 0.9 版以前是使用 dyngen 技術; QEMU 0.10 版以後採用 TCG。雖說如此,但在 QEMU 的其它部分差異不大,上述文件仍可供參考。SOURCEARCHIVE.com 收集了自 QEMU 0.6.1 版至今的所有 QEMU 源代碼。各位可以邊看文件邊看源代碼。

QEMU 極為依賴 macro,這使得直接閱讀源代碼通常無法確定其函數呼叫,或是執行流程倒底為何。請在編譯 QEMU 的時候加上 --extra-cflags="-save-temps",如此可得展開 marco 的 *.i 檔。

其餘部分請見:

1. TCG

TCG 是 QEMU 的核心。其基本流程如下:

guest binary -> TCG IR -> host binary

1.1 TCG IR

TCG 定義了一組 IR (intermediate representation),熟悉 GCC 的各位對此應該不陌生。TCG IR 大致分成以下幾類:

請見 tcg/*,特別是 tcg.i,可以看到 TCGOpcode。tcg/README 也別忘了。TCG 在翻譯 guest binary 的時候是以一個 translation block (tb) 為單位,其結尾通常是分支指令。

target-ARCH/* 定義了如何將 ARCH binary 反匯編成 TCG IR。tcg/ARCH 定義了如何將 TCG IR 翻譯成 ARCH binary。

1.2 TCG Flow

先介紹一些資料結構:

以 qemu-i386 為例,流程大致如下:

main (linux-user/main.c) -> cpu_exec_init_all (exec.c) -> cpu_init/cpu_x86_init (target-i386/helper.c) -> tcg_prologue_init (tcg/tcg.c) -> cpu_loop (linux-user/main.c)

函式名之所以會出現 cpu_init/cpu_x86_init,是因為 QEMU 經常使用 #define 替換函式名。cpu_init 是 main 裡呼叫的函式,經 #define 替換後,實際上是 cpu_x86_init (target-i386/helper.c)。GDB 下斷點時請注意此種情況。

這邊只介紹 tcg_prologue_init (tcg/tcg.c) -> cpu_loop (linux-user/main.c) 這一段,因為這一段跟 TCG 較為相關。容我先講 cpu_loop (linux-user/main.c)。

/* prepare setjmp context for exception handling */ for(;;) { if (setjmp(env->jmp_env) == 0) { // 例外處理。 }   next_tb = 0; /* force lookup of first TB */ for(;;) { // 判斷是否有中斷。若有,跳回例外處理。   next_tb = tcg_qemu_tb_exec(tc_ptr); // 跳至 code cache 執行。   } }

static void tcg_target_qemu_prologue(TCGContext *s) { /* QEMU (cpu_exec) -> 入棧 */   // OPC_GRP5 (0xff) 為 call,EXT5_JMPN_Ev 是其 opcode extension。 // tcg_target_call_iarg_regs 是函式呼叫負責傳遞參數的暫存器。 // 跳至 code cache 執行。 tcg_out_modrm(s, OPC_GRP5, EXT5_JMPN_Ev, tcg_target_call_iarg_regs[0]);   // 此時,s->code_ptr 指向 code_gen_prologue 中 prologue 和 jmp to code cache // 之後的位址。 // tb_ret_addr 是紀錄 code cache 跳回 code_gen_prologue 的哪個地方。 tb_ret_addr = s->code_ptr;   /* 出棧 -> 返回 QEMU (cpu_exec),確切的講是返回 tcg_qemu_tb_exec */ }

這邊小結一下。

QEMU -> prologue -> code cache -> epilogue -> QEMU

tb_ret_addr 就是用來由 code cache 返回至 code_gen_prologue,執行 epilogue,再返回 QEMU。

在介紹 cpu_exec 之前,我先介紹幾個 QEMU 資料結構,請善用 SOURCEARCHIVE.com。我們要知道所謂仿真或是虛擬化一個 CPU (ISA),簡單來說就是用一個資料結構 (struct) 儲存該 CPU 的狀態。執行該虛擬 CPU,就是從內存中讀取該虛擬 CPU 的資料結構,運算後再存回去。

再回來講 PageDesc。QEMU 替 PageDesc 維護了一個二級頁表 l1_map。page_find 這個函式根據輸入的 address 搜尋 l1_map,返回 PageDesc。這在以 guest page (guest binary) 為單位,清空與它相關聯的 TB (code cache) 的時候會用到。有一個名字很像的資料結構叫 PhysPageDesc,QEMU 也替它維護一個二級頁表 l1_phys_map。這是在 system mode 做地址轉換之用,這邊不談。