View on GitHub

开源开发工具技术博客

编译器/工具链、模拟器、虚拟机、Linux内核、调试和性能分析工具

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

1.2 TCG Flow

介紹完一些資料結構之後,我開始介紹 cpu_exec 的流程。底下複習一下 process mode 的流程:

cpu_loop (linux-user/main.c) -> cpu_x86_exec/cpu_exec (cpu-exec.c)

cpu_exec 有兩層 for 迴圈。我們先看內層:

next_tb = 0; /* force lookup of first TB */ for(;;) {   tb = tb_find_fast();   tc_ptr = tb->tc_ptr;   next_tb = tcg_qemu_tb_exec(tc_ptr); }

// pc = eip + cs_base cpu_get_tb_cpu_state(env, &pc, &cs_base, &flags);   // CPUState 中的 tb_jmp_cache 即是做此用途。 tb = env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];   // 檢查該 tb 是否合法。這是因為不同的 eip + cs_base 可能會得到相同的 pc。 if (unlikely(!tb || tb->pc != pc || tb->cs_base != cs_base || tb->flags != flags)) { tb = tb_find_slow(pc, cs_base, flags); }   // code cache 已有翻譯過的 host binary,返回 TranslationBlock。 return tb;

phys_pc = get_page_addr_code(env, pc); phys_page1 = phys_pc & TARGET_PAGE_MASK;   // 除了 env->tb_jmp_cache 這個以 guest virtual address 為索引的緩存之外, // QEMU 還維護了一個 tb_phys_hash,這個是以 guest physical address 為索引。 h = tb_phys_hash_func(phys_pc); ptb1 = &tb_phys_hash[h]; for (;;) {   not_found:   found:   // TranslationBlock 中的 phys_hash_next 用在這裡。 // 如果 phys_pc 索引到同一個 tb_phys_hash 欄位,用 phys_hash_next 串接起來。 ptb1 = &tb->phys_hash_next; }   not_found: tb = tb_gen_code(env, pc, cs_base, flags, 0);   found: env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb; return tb;

這裡小結一下流程。

cpu_exec (cpu-exec.c) -> tb_find_fast (cpu-exec.c) -> tb_find_slow (cpu-exec.c)

QEMU 先以 guest virtual address (GVA) 查找是否已有翻譯過的 TB,再以 guest physical address (GPA) 查找是否已有翻譯過的 TB。

如果沒有翻譯過的 TB,開始進行 guest binary -> TCG IR -> host binary 的翻譯。大致流程如下:

tb_gen_code (exec.c) -> cpu_gen_code (translate-all.c) -> gen_intermediate_code (target-i386/translate.c) -> tcg_gen_code (tcg/tcg.c) -> tcg_gen_code_common (tcg/tcg.c)

// 注意! 這裡會將 GVA 轉成 GPA。phys_pc 將交給之後的 tb_link_page 使用。 phys_pc = get_page_addr_code(env, pc); tb = tb_alloc(pc); if (!tb) { // 清空 code cache }   // 初始 tb   // 開始 guest binary -> TCG IR -> host binary 的翻譯。 cpu_gen_code(env, tb, &code_gen_size);   // 將 tb 加入 tb_phys_hash 和二級頁表 l1_map。 // phys_pc 和 phys_page2 分別代表 tb (guest pc) 對映的 GPA 和所屬的第二個 // 頁面 (如果 tb 代表的 guest binary 跨頁面的話)。 tb_link_page(tb, phys_pc, phys_page2); return tb;

我底下分別針對 cpu_gen_code 和 tb_link_page 稍微深入的介紹一下。

// 初始 TCGContext 的 gen_opc_ptr 和 gen_opparam_ptr,使其分別指向 // gen_opc_buf 和 gen_opparam_buf。gen_opc_buf 和 gen_opparam_buf // 分別存放 TCGOpcode 和 operand。 tcg_func_start(s);   // 呼叫 gen_intermediate_code_internal 產生 TCG IR gen_intermediate_code(env, tb);   // TCG IR -> host binary gen_code_size = tcg_gen_code(s, gen_code_buf);

tcg_reg_alloc_start(s);   s->code_buf = gen_code_buf; // host binary 會寫入 TCGContext s 的 code_ptr 所指向的緩衝區。 s->code_ptr = gen_code_buf;

至此,guest binary -> TCG IR -> host binary 算是完成了。剩下把 TranslationBlock (TB) 納入 QEMU 的管理,這是 tb_link_page 做的事。

/* 把新的 TB 加進 tb_phys_hash */ h = tb_phys_hash_func(phys_pc); ptb = &tb_phys_hash[h]; // 如果兩個以上的 TB 其 phys_pc 的哈希值相同,則做 chaining。 tb->phys_hash_next = *ptb; *ptb = tb; // 新加入的 TB 放至 chaining 的開頭。   // 在 l1_map 中配置 PageDesc 給 TB,並設置 TB 的 page_addr 和 page_next。 tb_alloc_page(tb, 0, phys_pc & TARGET_PAGE_MASK); if (phys_page2 != -1) // TB 對應的 guest binary 跨頁 tb_alloc_page(tb, 1, phys_page2); else tb->page_addr[1] = -1;   // 以下和 block chaining 有關,留待下次再講,這邊暫且不提。 tb->jmp_first = (TranslationBlock *)((long)tb | 2);

static inline void tb_alloc_page(TranslationBlock *tb, unsigned int n, tb_page_addr_t page_addr) { // 代表 tb (guest binary) 所屬 guest page。 tb->page_addr[n] = page_addr; // 在 l1_map 中配置一個 PageDesc,返回該 PageDesc。 p = page_find_alloc(page_addr >> TARGET_PAGE_BITS, 1); // 將該頁面目前第一個 TB 串接到此 TB。將來有需要將某頁面所屬所有 TB 清空。 tb->page_next[n] = p->first_tb; // n 為 1 代表 tb 對應的 guest binary 跨 page。 p->first_tb = (TranslationBlock *)((long)tb | n); // PageDesc 會維護一個 bitmap,這是給 SMC 之用。這裡不提。 invalidate_page_bitmap(p); }

這裡先回顧一下,QEMU 查看當前 env->pc 是否已翻譯過。若否,則進行翻譯。

tb_find_fast (cpu-exec.c) -> tb_find_slow (cpu-exec.c) -> tb_gen_code (exec.c)

tb_gen_code 講到這裡,guest binary -> host binary 已翻譯完成,相關資料結構也已設置完畢。返回 TB (TranslationBlock *) 給 tb_find_fast。

tb = tb_find_fast();   tc_ptr = tb->tc_ptr; // tc_ptr 指向 code cache (host binary)   next_tb = tcg_qemu_tb_exec(tc_ptr);

很好,我們準備從 QEMU 跳入 code cache 開始執行了。:-) tcg_qemu_tb_exec 被定義在 tcg/tcg.h。

#define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM (*)(void *))code_gen_prologue)(tb_ptr)

(long REGPARM (*)(void *)) 將 code_gen_prologue 轉型成函式指針,void * 為該函式的參數,返回值為 long。REGPARM 指示 GCC 此函式透過暫存器而非棧傳遞參數。至此,(long REGPARM (*)(void *)) 將數組指針 code_gen_prologue 轉型成函式指針。tb_ptr 為該函式指針的參數。綜合以上所述,code_gen_prologue 被視為一函式,其參數為 tb_ptr,返回當前 TB (tc_ptr 代表的 TB,等講到 block chaining 會比較清楚)。code_gen_prologue 所做的事為一般函式呼叫前的 prologue,之後將控制交由 tc_ptr 指向的 host binary 並開始執行。