storage R&D guy.
全部博文(1000)
分类: 服务器与存储
2015-04-22 09:54:41
target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。
target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。
ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。
tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。
guest virtual addr (GVA) → guest physical addr (GPA) → host virtual addr (HVA)
GVA → GPA 由客戶機作業系統負責; GPA → HVA 由 QEMU 負責。HVA → HPA 由宿主機作業系統負責
GVA → HVA。存放 GVA 相對於 HVA 的偏移量。轉換 GVA 到 HVA 的過程中,會先搜尋 TLB。如果命中,則將 GVA 加上該偏移量得到 HVA。若否,則需搜尋 l1_phys_map 並將 PhysPageDesc 填入 TLB。
guest virtual addr → guest physical addr
搜尋 l1_phys_map 得到 PhysPageDesc。
將 phys_ram_base 加上 PhysPageDesc.phys_offset,得到 host virtual addr (physical addr → host virtual addr)
typedef struct CPUTLBEntry { // 以下存放 GVA,同時也代表該頁面的權限。tlb_set_page (exec.c) 填入新的 TLB 項目時會做設置。 target_ulong addr_read; // 可讀 target_ulong addr_write; // 可寫 target_ulong addr_code; // 可執行 // HVA 相對於 GVA 的偏移量。unsigned long addend;} CPUTLBEntry;
tb_find_slow 是利用 guest pc (GVA) 對映的 guest physical address (GPA) 查找該 guest pc 的 TB。
static TranslationBlock *tb_find_slow(target_ulong pc, ...){ phys_pc = get_page_addr_code(env, pc); phys_page1= phys_pc & TARGET_PAGE_MASK; phys_page2 = -1; // 用虛擬位址 pc 對映的物理位址 phys_pc 查找 tb_phys_hash。 h = tb_phys_hash_func(phys_pc); ptb1 = &tb_phys_hash[h]; for(;;){ tb = *ptb1; if (!tb) goto not_found; if (tb->pc == pc && tb->page_addr[0] ==phys_page1 && // 該 TB 所屬物理頁面 (guest code) 是否與 pc 所屬物理頁面相同? tb->cs_base== cs_base && tb->flags == flags) { if (tb->page_addr[1] != -1) { // 該 TB 有跨物理頁面 virt_page2 = (pc & TARGET_PAGE_MASK) +TARGET_PAGE_SIZE; phys_page2 = get_page_addr_code(env, virt_page2); if (tb->page_addr[1] == phys_page2) // 該 TB 所屬的第二個物理頁面是否與 pc 所屬的第二個物理頁面相同? goto found; } else { goto found; } } ptb1 = &tb->phys_hash_next; // 當 phys_pc 雜湊到同一個 tb_phys_hash 項目時。 } 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;}
get_page_addr_code (exec.c) 先查找 TLB。process mode 情況有所不同,此時沒有所謂的 GPA,直接返回 addr。注意! get_page_addr_code 是被 tb_find_slow (cpu-exec.c) 或是 tb_gen_code (exec.c) 這兩個函式呼叫,get_page_addr_code 中的 code 代表存取的地址是一段 code。因此,皆是呼叫到 ld*_code 或是 ldb_cmmu。強烈建議查看 i386-softmmu/exec.i。
static inline tb_page_addr_t get_page_addr_code(CPUState *env1, target_ulong addr){page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); // 計算 GVA 對映的 TLB 索引 mmu_idx = cpu_mmu_index(env1); // TLB 不命中 if (unlikely(env1->tlb_table[mmu_idx][page_index].addr_code != (addr & TARGET_PAGE_MASK))) {ldub_code(addr); } // TLB 命中。檢查欲執行的位址屬於 RAM 之後,計算 GVA 對映的 HVA。 p =(void *)(unsigned long)addr + env1->tlb_table[mmu_idx][page_index].addend; // 返回 HVA 在 RAM 中的偏移量。 return qemu_ram_addr_from_host(p);}
TLB 不命中。ldub_code (softmmu_header.h) 是個透過宏展開的函式。
static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr){ if(unlikely(env->tlb_table[mmu_idx][page_index].ADDR_READ != (addr & (TARGET_PAGE_MASK| (DATA_SIZE - 1))))) { // ADDR_READ 會視情況被替換成 addr_code 或是 addr_read。這裡因為存取的是 code, // ADDR_READ 被替換成 addr_code。 res = glue(glue(__ld, SUFFIX),MMUSUFFIX)(addr, mmu_idx); } else { physaddr = addr + env->tlb_table[mmu_idx][page_index].addend; res = glue(glue(ld, USUFFIX), _raw)((uint8_t*)physaddr); } return res;}
ldb_cmmu (softmmu_template.h),其中的 cmmu 代表存取的是 code。如果是 mmu,代表存取的是 data。
DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr, int mmu_idx){ // 先查找 TLBredo: // ADDR_READ 會被替換成 addr_code。 tlb_addr = env->tlb_table[mmu_idx][index].ADDR_READ; if ((addr & TARGET_PAGE_MASK) == (tlb_addr &(TARGET_PAGE_MASK | TLB_INVALID_MASK))) { // TLB 命中 if (tlb_addr &~TARGET_PAGE_MASK) { // iotlb 緩存 IO 模擬函式 ioaddr = env->iotlb[mmu_idx][index]; } else if (((addr & ~TARGET_PAGE_MASK) + DATA_SIZE - 1) >=TARGET_PAGE_SIZE) { do_unaligned_access: // 這裡會呼叫 slow_ldb_cmmu 做跨頁存取。 } else { addend = env->tlb_table[mmu_idx][index].addend; res =glue(glue(ld, USUFFIX), _raw)((uint8_t *)(long)(addr+addend)); } } else { // GETPC 包裝 __builtin_return_address,請見 。 // 其用途是取得此函式的 return address,藉此可得知從哪個 caller 呼叫到此函式。 // 在 exec.c 的最後已將 GETPC 定為 NULL。retaddr = GETPC(); // 不同 ISA 分別定義不同的 tlb_fill tlb_fill(addr,READ_ACCESS_TYPE, mmu_idx, retaddr); goto redo; } return res;}
在 exec.c 的最後定義如下的宏,並 include softmmu_template.h 將其中的宏展開。請見 exec.i。
#define MMUSUFFIX _cmmu // load code#define GETPC() NULL // tb_find_slow -> get_page_addr_code -> ldub_code -> __ldb_cmmu#define env cpu_single_env#define SOFTMMU_CODE_ACCESS #define SHIFT 0#include "softmmu_template.h" #define SHIFT 1#include "softmmu_template.h" #define SHIFT 2#include "softmmu_template.h" #define SHIFT 3#include "softmmu_template.h" #undef env
請見 [Qemu-devel] When the tlb_fill will be called from generated code?。
tlb_fill (target-i386/op_helper.c)。查找頁表,如果頁存在,將該頁帶進 TLB; 如果頁不存在,發出頁缺失中斷。
void tlb_fill(target_ulong addr, int is_write, int mmu_idx, void*retaddr){ ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx, 1); if (ret){ // 出包了! if (retaddr) { // tlb_fill 由 code cache 或是 helper.c 被呼叫。 pc = (unsigned long)retaddr; tb = tb_find_pc(pc); if (tb) { cpu_restore_state(tb, env, pc, NULL); } } // tlb_fill 以一般的方式 (get_page_addr_code) 被呼叫,並非從 code cache 或是 helper function 被呼叫。 // 發出 guest page fault exception,guest OS 開始 page fault 處理。 raise_exception_err(env->exception_index, env->error_code); } // 頁面已在內存。 env = saved_env;}
retaddr 非 NULL 代表 tlb_fill 並非從 get_page_addr_code 呼叫。例如,target-i386/op_helper.c 或是 code cache。請見 op_helper.i。
uint8_t __ldb_mmu(target_ulong addr, int mmu_idx){ retaddr = ((void *)((unsignedlong)__builtin_return_address(0) - 1)); tlb_fill(addr, 0, mmu_idx, retaddr); gotoredo;}
cpu_x86_handle_mmu_fault (target-i386/helper.c) 查找頁表。如果該頁已在內存,呼叫 tlb_set_page 將該頁寫入 TLB。
intcpu_x86_handle_mmu_fault(CPUX86State *env, target_ulong addr, ...){ do_mapping:tlb_set_page(env, vaddr, paddr, prot, mmu_idx, page_size); return 0; do_fault:return 1;}
tlb_set_page (exec.c) 填入 TLB 項。
void tlb_set_page(CPUState *env, target_ulong vaddr, ...){ CPUTLBEntry *te; // 回傳 host virtual address addend = (unsigned long)qemu_get_ram_ptr(pd &TARGET_PAGE_MASK); // 更新 TLB 項目 te = &env->tlb_table[mmu_idx][index]; te->addend= addend - vaddr; // host virtual address 與 guest virtual address 的偏移量。}
請見 [Qemu-devel] When the tlb_fill will be called from generated code?。共有底下檔案:
softmmu-semi.h
softmmu_defs.h: 宣告 \_\_{ld,st}* 函式原型。
softmmu_exec.h: 利用 softmmu_header.h 生成 {ld,st}_{user,kernel,etc} 函式。{ld,st}_{user,kernel,etc} 函式又會呼叫到 \_\_{ld,st}* 函式。
softmmu_header.h: 生成 {ld,st}* 函式。{ld,st}* 函式又會呼叫到 \_\_{ld,st}* 函式。
softmmu_template.h: 生成 \_\_{ld,st}* 函式。
底下檔案定義相關函式原型。
softmmu_defs.h: 宣告給 TCG IR qemu_ld/qemu_st 使用的 \_\_{ld,st}* 函式原型。被 softmmu_exec.h, tcg/xxx/tcg-target.c 和 exec-all.h 所 #include。
底下檔案生成相關函式體。
softmmu_template.h: exec.c 和 target-*/op_helper.c 使用 softmmu_template.h 生成 \_\_{ld,st}* 函式。tcg_out_qemu_ld (tcg/xxx/tcg-target.c) 在為 qemu_ld/qemu_st 產生 host binary 時,會呼叫到 softmmu_template.h 生成 \_\_{ld,st}* 函式。
tcg_out_qemu_ld (tcg/i386/tcg-target.c)。
#include "../../softmmu_defs.h"// softmmu_defs.h// uint8_t REGPARM __ldb_mmu(target_ulong addr, int mmu_idx);// void REGPARM __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx); // 內存讀指令。會將該指令指定的虛擬位址透過 software MMU 轉換成物理位址。// softmmu_template.h 會透過宏展開定義相對應的函式。static void*qemu_ld_helpers[4] = { __ldb_mmu, // load byte __ldw_mmu, // load word __ldl_mmu,// load long word __ldq_mmu, // load quad word}; static void tcg_out_qemu_ld(TCGContext *s, const TCGArg*args, int opc){ tcg_out_calli(s,(tcg_target_long)qemu_ld_helpers[s_bits]); // 呼叫上述函式。 }
switch_tss (target-i386/op_helper.c)
// #define MMU_MODE0_SUFFIX _kernel// #define MMU_MODE1_SUFFIX _user#include "cpu.h" // softmmu_exec.h 生成 {ld,st}*_{kernel,user,etc} 函式。// #define ACCESS_TYPE 0// #define MEMSUFFIX MMU_MODE0_SUFFIX// #define DATA_SIZE 1// #include "softmmu_header.h"#if !defined(CONFIG_USER_ONLY)#include "softmmu_exec.h"#endif #define MMUSUFFIX _mmu #define SHIFT 0#include "softmmu_template.h" // softmmu_template.h// SUFFIX 代表資料大小,可以是 b (byte, 8)、w (word, 16)、l (long word, 32) 或 q (quadruple word,64)// MMUSUFFIX 代表存取代碼或是資料,可以是 _cmmu 或 _mmu。DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr, int mmu_idx){} // target-i386/op_helper.iuint8_t __ldb_mmu(target_ulong addr, int mmu_idx){} # 1 "/tmp/chenwj/qemu/softmmu_exec.h" 1# 27 "/tmp/chenwj/qemu/softmmu_exec.h"# 1 "/tmp/chenwj/qemu/softmmu_header.h" 1# 83 "/tmp/chenwj/qemu/softmmu_header.h"// 存取內核態資料static __attribute__ (( always_inline )) __inline__ uint32_tldub_kernel(target_ulong ptr){ if (__builtin_expect(!!(env->tlb_table[mmu_idx][page_index].addr_read != (addr & (~((1 << 12) - 1) | (1 -1)))), 0) ) { res = __ldb_mmu(addr, mmu_idx); // softmmu_defs.h 定義函式原型,其函式體由 softmmu_template.h 實現。 } else { physaddr = addr + env->tlb_table[mmu_idx][page_index].addend; // softmmu_exec.h 定義函式原型,其函式體由 softmmu_header.h 實現。 res = ldub_p((uint8_t *)(long)(((uint8_t *)physaddr)));}} static void switch_tss(int tss_selector, ...){ v1 = ldub_kernel(env->tr.base); v2 = ldub_kernel(env->tr.base + old_tss_limit_max); }
softmmu_exec.h: target-*/op_helper.c #include softmmu_exec.h,softmmu_exec.h 再利用 softmmu_header.h 生成 {ld,st}*_{kernel,user,etc} 函式。
softmmu_exec.h #include softmmu_defs.h,softmmu_defs.h 定義函式原型 \_\_{ld,st},其函式體由 softmmu_template.h 實現。softmmu_exec.h 也 #include softmmu_header.h,softmmu_header.h 定義巨集生成 {ld,st}*_{kernel,user,etc} 函式。
// softmmu_defs.huint8_t REGPARM __ldb_mmu(target_ulong addr, int mmu_idx);voidREGPARM __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx); // softmmu_template.hDATA_TYPE REGPARM glue(glue(__ld, SUFFIX),MMUSUFFIX)(target_ulong addr, int mmu_idx){} // softmmu_header.hstatic inlineRES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr){ res =glue(glue(__ld, SUFFIX), MMUSUFFIX)(addr, mmu_idx); }
helper_fldt (target-i386/op_helper.c)
#if !defined(CONFIG_USER_ONLY)#include "softmmu_exec.h"#endif static inline floatx80 helper_fldt(target_ulong ptr){ CPU_LDoubleU temp; temp.l.lower = ldq(ptr); // #define ldub(p) ldub_data(p) in softmmu_exec.h temp.l.upper = lduw(ptr + 8); return temp.d;}
softmmu_header.h: 定義巨集。被 softmmu_exec.h 和 exec-all.h #include 進而展開巨集,根據 MMU mode 和 data size 定義 inli ne ld/st 函式。
softmmu_exec.h: 被 target-xxx/op_helper.c 所 #include。根據 MMU mode (user/kernel) 和 data size 生成 inline ld/st 函式。
// target-xxx/cpu.h 自行定義 MMU_MODE?_SUFFIX。// 以 i386 為例: _kernel,_user。#define MEMSUFFIX MMU_MODE1_SUFFIX
// target-i386/op_helper.c#include "cpu.h"#include "softmmu_exec.h" // softmmu_exec.h#include "softmmu_defs.h" #define ACCESS_TYPE 0#define MEMSUFFIX MMU_MODE0_SUFFIX#define DATA_SIZE 1#include "softmmu_header.h" // softmmu_header.h// op_helper.i -> ldub_kernel// ld/st 最後會呼叫到 __ld/__st// kernel mode: env->tlb_table[0]// user mode: env->tlb_table[1]// data: env->tlb_table[(cpu_mmu_index(env))]static inline RES_TYPE glue(glue(ld, USUFFIX),MEMSUFFIX)(target_ulong ptr){}
exec-all.h。定義給 code cache 使用的 softmmu 函式。
#include "softmmu_defs.h" // uint64_t REGPARM __ldq_mmu(target_ulong addr, int mmu_idx);// void REGPARM __stq_mmu(target_ulong addr, uint64_t val, int mmu_idx);//// uint8_t REGPARM __ldb_cmmu(target_ulong addr, int mmu_idx);// void REGPARM __stb_cmmu(target_ulong addr, uint8_t val, int mmu_idx); #define ACCESS_TYPE (NB_MMU_MODES + 1)#define MEMSUFFIX _code#define env cpu_single_env #define DATA_SIZE 1#include "softmmu_header.h" // softmmu_header.h#elif ACCESS_TYPE == (NB_MMU_MODES + 1) #define CPU_MMU_INDEX (cpu_mmu_index(env))#define MMUSUFFIX _cmmu #else // 生成 ldub_cmmu -> __ldb_cmmu// env->tlb_table[(cpu_mmu_index(env))]
cpu-exec.i: {ld, st}{sb, ub}_{kernel, user, data, p} p: 直接讀。
QEMU softmmu 有幾處可以加速15)16)17)。
以 x86 為例,有幾種情況會呼叫 tlb_flush。
cpu_x86_update_crN。在 target-i386/translate.c 中,遇到 mov reg, crN 或是 mov crN, reg 會呼叫 helper_write_crN (target-i386/op_helper.c),helper_write_crN 再視情況呼叫 cpu_x86_update_crN (target-i386/helper.c)。
cpu_register_physical_memory_log
cpu_reset
cpu_x86_set_a20
tlb_flush (exec.c)。
void tlb_flush(CPUState *env, int flush_global){ int i; env->current_tb = NULL; for(i = 0; i < CPU_TLB_SIZE; i++) { int mmu_idx; for (mmu_idx =0; mmu_idx < NB_MMU_MODES; mmu_idx++) { env->tlb_table[mmu_idx][i] =s_cputlb_empty_entry; } } // 此時 softmmu (tlb) 失效,GVA -> HVA 的對映不再合法,所以要清空以 GVA (guest pc) 當索引的 tb_jmp_cache。 memset (env->tb_jmp_cache, 0,TB_JMP_CACHE_SIZE * sizeof (void *)); env->tlb_flush_addr = -1; env->tlb_flush_mask= 0; tlb_flush_count++;}
這時 QEMU 被迫以 tb_find_slow 改以 GPA 查找是否有以翻譯過的 TranslationBlock,同時會進行額外檢查。注意! 這時候有可能會重翻!
static TranslationBlock *tb_find_slow(CPUState *env, ...){ for(;;) { tb = *ptb1; if(!tb) goto not_found; if (tb->pc == pc && tb->page_addr[0] == phys_page1 && tb->cs_base == cs_base && tb->flags == flags) { if (tb->page_addr[1] != -1) { tb_page_addr_t phys_page2; virt_page2 = (pc &TARGET_PAGE_MASK) + TARGET_PAGE_SIZE; phys_page2 = get_page_addr_code(env,virt_page2); if (tb->page_addr[1] == phys_page2) goto found; } else { goto found; }} ptb1 = &tb->phys_hash_next; }}
可參考底下文件,
第 171 頁。
main (linux-user/main.c) 為進入點。
int main(int argc, char **argv, char **envp){ // 初始化 TCG cpu_exec_init_all(0); // 初始化 CPUState env = cpu_init(cpu_model); // 傳遞環境變數和命令行參數給 guest programtarget_environ = envlist_to_environ(envlist, NULL); // 初始化 TaskState 数据结构init_task_state(ts); ts->info = info; ts->bprm = &bprm; env->opaque = ts; // 後續透過 env->opaque 取得 TaskState task_settid(ts); // 載入 guest program。此時 regs 存放程序進入點,之後將此進入點賦值給 virtual CPU。 // 這樣 virtual CPU 就知道從哪裡開始執行。 ret = loader_exec(filename, target_argv, target_environ, regs,info, &bprm); target_set_brk(info->brk); // 設置 guest process 的 brk 指針。syscall_init(); signal_init(); tcg_prologue_init(&tcg_ctx); // 設置 CPU#if defined(TARGET_I386) cpu_x86_set_cpl(env, 3); // 將 x86 virtual CPU 特權級設為 ring 3。 env->cr[0] = CR0_PG_MASK | CR0_WP_MASK | CR0_PE_MASK; env->hflags |=HF_PE_MASK; if (env->cpuid_features & CPUID_SSE) { env->cr[4] |= CR4_OSFXSR_MASK;env->hflags |= HF_OSFXSR_MASK; } env->eflags |= IF_MASK; #ifndef TARGET_ABI32 env->regs[R_EAX] = regs->rax; env->regs[R_EBX] = regs->rbx; env->regs[R_ECX] = regs->rcx; env->regs[R_EDX] = regs->rdx; env->regs[R_ESI] = regs->rsi; env->regs[R_EDI] = regs->rdi; env->regs[R_EBP] = regs->rbp; env->regs[R_ESP] =regs->rsp; env->eip = regs->rip;#else env->regs[R_EAX] = regs->eax; env->regs[R_EBX]= regs->ebx; env->regs[R_ECX] = regs->ecx; env->regs[R_EDX] = regs->edx; env->regs[R_ESI] = regs->esi; env->regs[R_EDI] = regs->edi; env->regs[R_EBP] = regs->ebp; env->regs[R_ESP] = regs->esp; env->eip = regs->eip; // 前面已將 guest binary 進入點存進 regs。#endif // 設置中斷 #ifndef TARGET_ABI32env->idt.limit = 511;#else env->idt.limit = 255;#endif env->idt.base =target_mmap(0, sizeof(uint64_t) * (env->idt.limit + 1), PROT_READ|PROT_WRITE,MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); idt_table = g2h(env->idt.base); // 取得 guest idt (中斷描述符表) 在 host 的位址。 set_idt(0, 0); // 設定 set_idt(1, 0); // 開始 guest binary -> TCG IR -> host binary,並執行 host binary。 cpu_loop(env);}
cpu_exec_init_all 初始化 TCG。
void cpu_exec_init_all(unsigned long tb_size){ // 呼叫 tcg_context_init (tcg/tcg.c) 初始化 TCG。tcg_context_init // 再呼叫 tcg_target_init (tcg/i386/tcg-target.c) 針對宿主機做相應的設置。 cpu_gen_init(); code_gen_alloc(tb_size); // 使用 mmap 將 code_gen_prologue 和 code_gen_buffer 設為可讀、可寫和可執行。 code_gen_ptr =code_gen_buffer; page_init(); // 扫描 /proc/self/maps。針對每個頁調用 page_set_flags。}
page_init (exec.c)。
static void page_init(void){ last_brk = (unsigned long)sbrk(0); // 取得目前 QEMU brk 指針,brk 指向堆 (heap) 的頂端,代表程序資料段的結尾。 f =fopen("/compat/linux/proc/self/maps", "r"); // 取得 QEMU 虛擬內存的映象。 if (f) {mmap_lock(); do { // 掃描 maps 的內容,透過 h2g (cpu-all.h) 將 host addr 轉換成 guest addr,透過減去 guest addr base。 // 並檢查 guest addr 是否落在 guest addr space。 // page_set_flags 檢視該頁面是否被寫入。若被寫入,則清空所有與該頁面有關的 TB。這裡只是要建該頁面的 PageDesc。 page_set_flags(startaddr, endaddr, PAGE_RESERVED); }while (!feof(f)); }}
cpu_init 依據不同的 guest CPU 型號進行初始化。如 x86,則呼叫 cpu_x86_init (target-i386/helper.c)。
init_task_state (linux-user/main.c) 每個 guest process 都有一個 。
void init_task_state(TaskState *ts){ int i; ts->used = 1; ts->first_free = ts->sigqueue_table; for (i = 0; i < MAX_SIGQUEUE_SIZE - 1; i++) { ts->sigqueue_table[i].next = &ts->sigqueue_table[i + 1]; } ts->sigqueue_table[i].next =NULL;}
loader_exec (linux-user/linuxload.c) 載入 guest program。檢查其執行檔格式和權限。
// ret = loader_exec(filename, target_argv, target_environ, regs,// info, &bprm);int loader_exec(const char * filename, char ** argv, char ** envp, structtarget_pt_regs * regs, struct image_info *infop, struct linux_binprm *bprm){retval = open(filename, O_RDONLY); retval = prepare_binprm(bprm); if(retval>=0){ if (bprm->buf[0] == 0x7f && bprm->buf[1] == 'E' && bprm->buf[2] == 'L' && bprm->buf[3] == 'F') { retval = load_elf_binary(bprm, regs, infop); } } if(retval>=0){ // do_init_thread (linux-user/elfload.c) 呼叫 init_thread (linux-user/elfload.c) 根據不同架構 // 初始化 CPU 暫存器。以 x86_64 為例, // // regs->rax = 0; // regs->rsp = infop->start_stack; // regs->rip = infop->entry; do_init_thread(regs, infop); return retval; }}
load_elf_binary (linux-user/elfload.c) 將 guest binary 載入成為 image。
int load_elf_binary(struct linux_binprm * bprm, struct target_pt_regs * regs,struct image_info * info){ // 將 guest binary 載入成為 image。load_elf_image(bprm->filename, bprm->fd, info, &elf_interpreter, bprm->buf); // 如果該 guest binary 為動態連結,載入 dynamic linker。透過檢查 guest binary 的 program header 是否帶有 PT_INTERP。 if (elf_interpreter) {load_elf_interp(elf_interpreter, &interp_info, bprm->buf); } bprm->p =create_elf_tables(bprm->p, bprm->argc, bprm->envc, &elf_ex, info,(elf_interpreter ? &interp_info : NULL)); info->start_stack = bprm->p; // 將進入點設為 dynamic linker。 if (elf_interpreter) { info->load_bias =interp_info.load_bias; info->entry = interp_info.entry; free(elf_interpreter);}}
create_elf_tables (linux-user/elfload.c)
static abi_ulong create_elf_tables(abi_ulong p, int argc, int envc, struct elfhdr*exec, struct image_info *info, struct image_info *interp_info){ info->saved_auxv= sp; sp = loader_build_argptr(envc, argc, sp, p, 0); return sp;}
loader_build_argptr (linux-user/linuxload.c) 在 guest (target) 的棧上放置環境變數和命令行參數。put_user_ual (linux-user/qemu.h) 用來將資料搬移至 guest 內存。
abi_ulong loader_build_argptr(int envc, int argc, abi_ulong sp, ...){ TaskState *ts =(TaskState *)thread_env->opaque; sp -= (envc + 1) * n; // 調整棧指針給予環境變數空間。 envp = sp; sp -= (argc + 1) * n; // 調整棧指針給予命令行參數空間。 argv = sp; // 將命令行參數寫至 guest 棧上。 while (argc-- > 0) { put_user_ual(stringp, argv); argv += n; stringp +=target_strlen(stringp) + 1; } // 將環境變數寫至 guest 棧上。 while (envc-- > 0) { put_user_ual(stringp, envp); envp += n;stringp += target_strlen(stringp) + 1; } }
syscall_init (linux-user/syscall.c)
signal_init (linux-user/signal.c) 會處理 guest 跟 host 之間 signal 如何對映和處理。
// linux-user/syscall_defs.h 定義 TARGET_XXXstatic uint8_thost_to_target_signal_table[_NSIG] = { [SIGHUP] = TARGET_SIGHUP,} static uint8_ttarget_to_host_signal_table[_NSIG]; void signal_init(void){ for(i = 1; i < _NSIG; i++) { if (host_to_target_signal_table[i]== 0) host_to_target_signal_table[i] = i; } for(i = 1; i < _NSIG; i++) { j =host_to_target_signal_table[i]; target_to_host_signal_table[j] = i; } memset(sigact_table, 0, sizeof(sigact_table)); sigfillset(&act.sa_mask);act.sa_flags = SA_SIGINFO; act.sa_sigaction = host_signal_handler; for(i = 1; i <=TARGET_NSIG; i++) { host_sig = target_to_host_signal(i); sigaction(host_sig, NULL,&oact); if (oact.sa_sigaction == (void *)SIG_IGN) { sigact_table[i - 1]._sa_handler= TARGET_SIG_IGN; } else if (oact.sa_sigaction == (void *)SIG_DFL) { sigact_table[i- 1]._sa_handler = TARGET_SIG_DFL; } if (fatal_signal (i))sigaction(host_sig, &act, NULL); }}
設定 virtual CPU 暫存器、中斷描述符表、全域描述符表和段描述符。
static uint64_t *idt_table; static void set_gate(void *ptr, unsigned int type,unsigned int dpl, uint32_t addr, unsigned int sel){ uint32_t *p, e1, e2; e1 = (addr& 0xffff) | (sel << 16); e2 = (addr & 0xffff0000) | 0x8000 | (dpl << 13) | (type <<8); p = ptr; p[0] = tswap32(e1); p[1] = tswap32(e2);} static void set_idt(int n,unsigned int dpl){ set_gate(idt_table + n, 0, dpl, 0, 0);}
cpu_loop (linux-user/main.c) 為主要執行迴圈,呼叫 cpu_x86_exec 進行 guest binary → TCG → host binary 並執行的流程。cpu_x86_exec/cpu_exec (cpu-exec.c) 會返回例外號,cpu_loop 視例外號的不同做相對應的處理,之後處理訊號。不同架構定義不同的 cpu_loop。以 x86 為例,
void cpu_loop(CPUX86State *env){ int trapnr; abi_ulong pc; target_siginfo_t info; for(;;) { trapnr = cpu_x86_exec(env); switch(trapnr) { case 0x80: env->regs[R_EAX] = do_syscall(env, env->regs[R_EAX], env->regs[R_EBX], env->regs[R_ECX], env->regs[R_EDX], env->regs[R_ESI], env->regs[R_EDI], env->regs[R_EBP], 0, 0); break; default: pc =env->segs[R_CS].base + env->eip; fprintf(stderr, "qemu: 0xlx: unhandled CPU exception 0x%x - aborting\n", (long)pc, trapnr); abort(); }process_pending_signals(env); // linux-user/signal.c }}
QEMU user mode 在處理系統呼叫時,不像 system mode 需要翻譯 system call handler。
cpu_loop (linux-user/main.c)。
void cpu_loop(CPUX86State *env){ int trapnr; abi_ulong pc; target_siginfo_t info; for(;;) { trapnr = cpu_x86_exec(env); switch(trapnr) { case 0x80: env->regs[R_EAX] = do_syscall(env, env->regs[R_EAX], env->regs[R_EBX], env->regs[R_ECX], env->regs[R_EDX], env->regs[R_ESI], env->regs[R_EDI], env->regs[R_EBP], 0, 0); break; ... 略 ... } ... 略 ... }
do_syscall (linux-user/syscall.c) 將客戶 system call 做些包裝之後,轉交給宿主 system call。
abi_long do_syscall(void *cpu_env, int num, abi_long arg1, abi_long arg2, abi_long arg3, abi_long arg4, abi_long arg5, abi_long arg6, abi_long arg7, abi_long arg8){switch(num) { ... 略 ... case TARGET_NR_open: if (!(p = lock_user_string(arg1)))goto efault; ret = get_errno(do_open(cpu_env, p, target_to_host_bitmask(arg2,fcntl_flags_tbl), arg3)); unlock_user(p, arg1, 0); break; ... 略 ... } ... 略 ...}
do_open (linux-user/syscall.c)。
static int do_open(void *cpu_env, const char *pathname, int flags, mode_t mode){ // 前置處理。 // 呼叫 host system call。 return get_errno(open(path(pathname), flags,mode));}
以 x86 為例,
cpu-defs.h 定義例外號。
#define EXCP_INTERRUPT 0x10000 #define EXCP_HLT 0x10001 #define EXCP_DEBUG 0x10002 #define EXCP_HALTED 0x10003
有些函式前面會加上 QEMU_NORETURN (compiler.h),這代表該函式不會返回。
#define QEMU_NORETURN __attribute__ ((__noreturn__))
cpu_exec (cpu-exec.c)
int cpu_exec(CPUState *env){ if (env->halted) { // system mode 才會拉起 env->halted。if (!cpu_has_work(env)) { return EXCP_HALTED; } env->halted = 0; } cpu_single_env= env; // 保存當前 env。待 lonjmp 時,可以用 cpu_single_env 回復 env。 if(unlikely(exit_request)) { env->exit_request = 1; } // 不同架構會有不同前置處理。#if defined(TARGET_I386) CC_SRC = env->eflags & (CC_O | CC_S | CC_Z | CC_A | CC_P |CC_C); DF = 1 - (2 * ((env->eflags >> 10) & 1)); CC_OP = CC_OP_EFLAGS; env->eflags&= ~(DF_MASK | CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);#else#error unsupported target CPU#endif // cpu_exec 返回值即為 env->exception_index。以 process mode 為例,cpu_loop 在呼叫 cpu_exec 之後,會檢視其返回值並做相應處理。 env->exception_index = -1; // 進行翻譯並執行的迴圈。 for(;;) {if (setjmp(env->jmp_env) == 0) { // 正常流程。 next_tb = 0; for(;;) { } } else { env =cpu_single_env; } } }
QEMU 支持 。當例外發生時,執行流程會將舊的 env (cpu_single_env) 存回 env。
外層迴圈。cpu_exec 利用 setjmp/longjmp 處理例外。cpu_loop_exit (cpu-exec.c) 和 cpu_resume_from_signal (cpu-exec.c) 會呼叫 longjmp 回到 setjmp 設定的例外處理分支。18)
for(;;) { if (setjmp(env->jmp_env) == 0) { if (env->exception_index >= 0) { // exception_index 非 -1 代表有事要處理。if (env->exception_index >= EXCP_INTERRUPT) { // 來自 cpu_exec 以外的例外。 ret = env->exception_index; if (ret ==EXCP_DEBUG) { cpu_handle_debug_exception(env); } break; } else { #if defined(CONFIG_USER_ONLY) #if defined(TARGET_I386)do_interrupt(env);#endif ret = env->exception_index; break;#else } } } else { env = cpu_single_env; } } }
cpu_loop_exit (cpu-exec.c)
void cpu_loop_exit(CPUState *env){ env->current_tb = NULL; longjmp(env->jmp_env,1);}
cpu_resume_from_signal (cpu-exec.c)
#if defined(CONFIG_SOFTMMU)voidcpu_resume_from_signal(CPUState *env, void *puc){ env->exception_index = -1; longjmp(env->jmp_env,1);}#endif
user mode 和 system mode 有不同處置。
void do_interrupt(CPUState *env1){ CPUState *saved_env; saved_env = env; env =env1;#if defined(CONFIG_USER_ONLY) do_interrupt_user(env->exception_index, env->exception_is_int, env->error_code, env->exception_next_eip); env->old_exception = -1;#else do_interrupt_all(env->exception_index, env->exception_is_int, env->error_code, env->exception_next_eip, 0); env->old_exception = -1;#endif env = saved_env;}
user mode。
#if defined(CONFIG_USER_ONLY)static voiddo_interrupt_user(int intno, int is_int, int error_code, target_ulong next_eip){SegmentCache *dt; target_ulong ptr; int dpl, cpl, shift; uint32_t e2; dt =&env->idt; // 取出中斷描述符表 if (env->hflags & HF_LMA_MASK) { shift = 4; } else{ shift = 3; } ptr = dt->base + (intno << shift); // 取出中斷處理常式 e2 =ldl_kernel(ptr + 4); dpl = (e2 >> DESC_DPL_SHIFT) & 3; cpl = env->hflags &HF_CPL_MASK; if (is_int && dpl < cpl)raise_exception_err(EXCP0D_GPF, (intno << shift) + 2); // target-i386/cpu.h if (is_int) EIP = next_eip; // target-i386/cpu.h:#define EIP (env->eip)} #else
raise_exception_err (target-i386/op_helper.c) 轉呼叫 raise_interrupt (target-i386/op_helper.c)。
static void QEMU_NORETURN raise_exception_err(int exception_index, interror_code){ raise_interrupt(exception_index, 0, error_code, 0);}
raise_interrupt (target-i386/op_helper.c) 最後呼叫 cpu_loop_exit (cpu-exec.c) longjmp 回外層迴圈 else 分支。
static voidQEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code, intnext_eip_addend){ if (!is_int) {helper_svm_check_intercept_param(SVM_EXIT_EXCP_BASE + intno, error_code); intno= check_exception(intno, &error_code); } else {helper_svm_check_intercept_param(SVM_EXIT_SWINT, 0); } env->exception_index =intno; env->error_code = error_code; env->exception_is_int = is_int; env->exception_next_eip = env->eip + next_eip_addend; cpu_loop_exit(env);}
system mode。
do_interrupt_all (target-i386/op_helper.c)。
static void do_interrupt_all(int intno, int is_int, int error_code, target_ulong next_eip, int is_hw){ ... 略 ... // 檢查當前處於何種模式,交由相對應的函式處理。如果客戶機是 64 bit,還有 do_interrupt_64。 if (env->cr[0] & CR0_PE_MASK) { if (env->hflags & HF_SVMI_MASK) handle_even_inj(intno, is_int, error_code, is_hw, 0); {do_interrupt_protected(intno, is_int, error_code, next_eip, is_hw); } } else {do_interrupt_real(intno, is_int, error_code, next_eip); } ... 略 ...}
內核態與用戶態分別有內核棧和用戶棧。
do_interrupt_real。
static void do_interrupt_real(int intno, int is_int, int error_code, unsignedint next_eip){ ... 略 ... // 取得內核棧棧頂。 ssp = env->segs[R_SS].base; esp =ESP; // #define ESP (env->regs[R_ESP]) (target-i386/cpu.h) ssp = env->segs[R_SS].base; // 將用戶態資訊放至內核棧。 PUSHW(ssp, esp, 0xffff, compute_eflags()); PUSHW(ssp, esp, 0xffff, old_cs);PUSHW(ssp, esp, 0xffff, old_eip); // 將 env->eip 指向中斷向量。返回 cpu_exec 後便會翻譯中斷向量並執行。 ESP = (ESP & ~0xffff) | (esp &0xffff); env->eip = offset; env->segs[R_CS].selector = selector; env->segs[R_CS].base = (selector << 4); env->eflags &= ~(IF_MASK | TF_MASK | AC_MASK| RF_MASK);}
helper_iret_real。當中斷向量執行完畢後,會執行 iret 返回用戶態。
void helper_iret_real(int shift){ ... 略 ... // 將用戶態資訊從內核棧取出。 if(shift == 1) { POPL(ssp, sp, sp_mask, new_eip); POPL(ssp, sp,sp_mask, new_cs); new_cs &= 0xffff; POPL(ssp, sp, sp_mask, new_eflags); } else { POPW(ssp, sp, sp_mask, new_eip); POPW(ssp, sp, sp_mask, new_cs);POPW(ssp, sp, sp_mask, new_eflags); } ESP = (ESP & ~sp_mask) | (sp & sp_mask);env->segs[R_CS].selector = new_cs; env->segs[R_CS].base = (new_cs << 4); env->eip = new_eip; // 返回用戶態後,欲執行的 pc。 if (env->eflags & VM_MASK)eflags_mask = TF_MASK | AC_MASK | ID_MASK | IF_MASK | RF_MASK | NT_MASK; elseeflags_mask = TF_MASK | AC_MASK | ID_MASK | IF_MASK | IOPL_MASK | RF_MASK |NT_MASK; if (shift == 0) eflags_mask &= 0xffff; load_eflags(new_eflags,eflags_mask); env->hflags2 &= ~HF2_NMI_MASK;}
內層迴圈。
next_tb = 0; for(;;) { interrupt_request = env->interrupt_request; // 檢視 interrupt_request 是何種中斷,並將 interrupt_request 復位。// 設置 env->exception_index,再跳至 cpu_loop_exit。 // cpu_loop_exit 再 longjmp 到外層迴圈 setjmp 的點,跳到處理中斷的分支。 if (unlikely(interrupt_request)) { } // 檢視 env->exit_request。 if (unlikely(env->exit_request)) { env->exit_request = 0; env->exception_index = EXCP_INTERRUPT; cpu_loop_exit(env); } tb = tb_find_fast(env); env->current_tb = tb; barrier(); // 若無 exit_request,跳入 code cache 開始執行。 if(likely(!env->exit_request)) { } env->current_tb = NULL; }
要求當某一條 guest 指令發生例外,例如溢位或是頁缺失,此條指令之前的運算必須完成,此條指令之後的運算必須捨棄。當 QEMU 在 code cache 中執行翻譯過後的指令,其中發生例外時,QEMU 必須要能維護例外發生之前 guest 的暫存器和內存內容,並能反查出發生例外的 basic block 相對映開頭的 guest pc。從那個 guest pc 重新開始翻譯直到發生例外的位址,並把這個 guest pc 存回 CPUState,這是因為 QEMU 不會執行完一條 guest 指令就更新 guest pc。
暫存器: 在有可能發生例外的運算之前,如: guest load/store 或是呼叫 helper function,QEMU 會把 dirty CPUState 寫回內存。
內存: QEMU 不會將原本指令亂序。
QEMU 會用到底下定義在 translate-all.c 資料結構:
target_ulong gen_opc_pc[OPC_BUF_SIZE]; // 紀錄 guest pc。uint16_tgen_opc_icount[OPC_BUF_SIZE];uint8_t gen_opc_instr_start[OPC_BUF_SIZE]; // 當作標記之用。
針對 x86,又在 target-i386/translate.c 定義以下資料結構:
static uint8_t gen_opc_cc_op[OPC_BUF_SIZE]; // 紀錄 condition code。
QEMU 會以 retaddr 判斷在哪裡發生例外,這用到 擴展。注意! QEMU 在兩個地方定義 GETPC,分別位於 exec-all.h 和 exec.c。
exec-all.h。定義給 code cache 使用的 softmmu 函式。
// __builtin_return_address 返回當前函式 (參數 0) 的返回位址。#else# define GETPC() ((void *)((unsigned long)__builtin_return_address(0) - 1))#endif #include "softmmu_defs.h" #define ACCESS_TYPE (NB_MMU_MODES + 1)#define MEMSUFFIX _code#define env cpu_single_env #define DATA_SIZE 1#include "softmmu_header.h" #define DATA_SIZE 2#include "softmmu_header.h" #define DATA_SIZE 4#include "softmmu_header.h" #define DATA_SIZE 8#include "softmmu_header.h" #undef ACCESS_TYPE#undef MEMSUFFIX#undef env #endif
exec.c。定義給一般 C 函式使用的 softmmu 函式。
#define MMUSUFFIX _cmmu#undef GETPC#define GETPC() NULL#define env cpu_single_env#define SOFTMMU_CODE_ACCESS #define SHIFT 0#include "softmmu_template.h" #define SHIFT 1#include "softmmu_template.h" #define SHIFT 2#include "softmmu_template.h" #define SHIFT 3#include "softmmu_template.h" #undef env #endif static inline void svm_load_seg(target_phys_addr_t addr, SegmentCache*sc){ unsigned int flags; sc->selector = lduw_phys(addr + offsetof(struct vmcb_seg,selector)); sc->base = ldq_phys(addr + offsetof(struct vmcb_seg, base)); sc->limit =ldl_phys(addr + offsetof(struct vmcb_seg, limit)); flags = lduw_phys(addr +offsetof(struct vmcb_seg, attrib)); sc->flags = ((flags & 0xff) << 8) | ((flags &0x0f00) << 12);}
tlb_fill。env 是 dyngen-exec.h 中定義的全域變數,是一個指向 CPUState 的指針,該指針存放在宿主機特定的暫存器20)。
#if !defined(CONFIG_USER_ONLY)void tlb_fill(CPUState *env1, target_ulong addr, int is_write, int mmu_idx, void*retaddr){ TranslationBlock *tb; int ret; unsigned long pc; CPUX86State *saved_env; saved_env = env; // 備份 global env。 env = env1; // 將當前 env (env1) 賦值與 global env。 ret = cpu_x86_handle_mmu_fault(env, addr, is_write, mmu_idx); if (ret) { if(retaddr) { pc = (unsigned long)retaddr; tb =tb_find_pc(pc); if (tb) { cpu_restore_state(tb, env, pc); } }raise_exception_err(env->exception_index, env->error_code); } env =saved_env;}#endif
tb_find_slow → get_page_addr_code → ldub_code → \_\_ldb_cmmu → tlb_fill。此時,retaddr 為 NULL,代表是從一般 C 函式呼叫到 tlb_fill。
code cache → \_\_ldl_mmu → tlb_fill。此時,retaddr 不為 NULL,代表是從 code cache 呼叫到 tlb_fill,guest binary 通常在做 guest memory operation。
透過 tb_find_pc 以發生例外 host binary (retaddr) 位址反查 TranslationBlock。
TranslationBlock *tb_find_pc(unsigned long tc_ptr){ int m_min, m_max, m;unsigned long v; TranslationBlock *tb; if (nb_tbs <= 0) return NULL; // code_gen_buffer 是 code cache 起始位址,code_gen_ptr 是 code cache 目前生成 host binary 會放的位址。 if (tc_ptr < (unsigned long)code_gen_buffer || tc_ptr >= (unsignedlong)code_gen_ptr) return NULL; m_min = 0; m_max =nb_tbs - 1; while (m_min <= m_max) { m = (m_min + m_max) >> 1; tb = &tbs[m]; v =(unsigned long)tb->tc_ptr; // 例外發生所在的 host 位址和該 TranslationBlock 在 code cache 的位址一致。 if (v == tc_ptr) return tb; else if (tc_ptr < v) { m_max = m - 1; }else { m_min = m + 1; } } return &tbs[m_max]; // 返回發生例外的 TranslationBlock。}
cpu_restore_state (translate-all.c) 重翻該 TranslationBlock (guest binary → TCG IR → host binary),同時附帶上 guest pc 資訊。searched_pc 為發生例外的 host binary 位址。
intcpu_restore_state(TranslationBlock *tb, CPUState *env, unsigned long searched_pc){TCGContext *s = &tcg_ctx; int j; unsigned long tc_ptr; tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr // 轉呼叫 gen_intermediate_code_internal,要求在生成 TCG IR 的同時,為其生成相關的 pc 資訊。 gen_intermediate_code_pc(env, tb); if(use_icount) { env->icount_decr.u16.low += tb->icount; env->can_do_io = 0; } // tc_ptr 指向 host binary 在 code cache 的起始位址。 // 如果 searched_pc 小於 tc_ptr,代表此 tb 並非是負責人。 tc_ptr =(unsigned long)tb->tc_ptr; if (searched_pc < tc_ptr) return -1; s->tb_next_offset =tb->tb_next_offset;#ifdef USE_DIRECT_JUMP s->tb_jmp_offset = tb->tb_jmp_offset; s->tb_next = NULL;#else s->tb_jmp_offset = NULL; s->tb_next = tb->tb_next;#endif // 轉呼叫 tcg_gen_code_common (tcg/tcg.c) j = tcg_gen_code_search_pc(s, (uint8_t *)tc_ptr,searched_pc - tc_ptr); if (j < 0) return -1; while (gen_opc_instr_start[j] == 0) j--; env->icount_decr.u16.low -=gen_opc_icount[j]; // 此時,用 j 索引 gen_opc_pc 可以得到對應的 guest pc。restore_state_to_opc(env, tb, j); return 0;}
gen_intermediate_code_pc (target-i386/translate.c) 轉呼叫 gen_intermediate_code_internal,search_pc 為真。將 guest binary 翻成 TCG IR 的同時,紀錄下 guest pc。
static inline void gen_intermediate_code_internal(CPUState *env, TranslationBlock*tb, int search_pc){ for(;;) { if (search_pc) { // gen_opc_ptr 為 TCG opcode buffer 目前位址,gen_opc_buf 為 TCG opcode buffer。 j = gen_opc_ptr - gen_opc_buf; if (lj < j) gen_opc_instr_start[lj++] = 0; // 不到 j 的部分填零。 }gen_opc_pc[lj] = pc_ptr; // 紀錄 guest pc。 gen_opc_cc_op[lj] = dc->cc_op; // 紀錄 condition code。 gen_opc_instr_start[lj] = 1; // 填 1 作為標記。 gen_opc_icount[lj]= num_insns; } } if (tb->cflags & CF_LAST_IO) gen_io_end(); gen_icount_end(tb,num_insns); *gen_opc_ptr = INDEX_op_end; if (search_pc) { j = gen_opc_ptr - gen_opc_buf; lj++; while (lj <= j)gen_opc_instr_start[lj++] = 0; }}
tcg_gen_code_search_pc (tcg/tcg.c) 轉呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 翻成 host binary。search_pc 應該改叫 offset。
static inline inttcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf, long search_pc){ for(;;){ switch(opc) { case INDEX_op_nopn: args += args[0]; goto next; caseINDEX_op_call: dead_args = s->op_dead_args[op_index]; args +=tcg_reg_alloc_call(s, def, opc, args, dead_args); goto next; } args += def->nb_args; next: // 如果 offset (search_pc) 落在目前 code cache 起始位址和 code cache 目前存放 host binary 的位址之間, // 返回 TCG op index。 if (search_pc >= 0 &&search_pc < s->code_ptr - gen_code_buf) { return op_index; } op_index++; }}
restore_state_to_opc (target-i386/translate.c)
void restore_state_to_opc(CPUState *env, TranslationBlock *tb, int pc_pos){ intcc_op; env->eip = gen_opc_pc[pc_pos] - tb->cs_base; cc_op =gen_opc_cc_op[pc_pos]; if (cc_op != CC_OP_DYNAMIC) env->cc_op = cc_op;}
raise_exception_err (target-i386/op_helper.c)
static void QEMU_NORETURN raise_exception_err(int exception_index, int error_code){raise_interrupt(exception_index, 0, error_code, 0);}
raise_interrupt (target-i386/op_helper.c)
static void QEMU_NORETURN raise_interrupt(int intno, int is_int, int error_code, int next_eip_addend){ if(!is_int) { // 走這裡。 helper_svm_check_intercept_param(SVM_EXIT_EXCP_BASE + intno,error_code); intno = check_exception(intno, &error_code); } else {helper_svm_check_intercept_param(SVM_EXIT_SWINT, 0); } env->exception_index =intno; env->error_code = error_code; env->exception_is_int = is_int; env->exception_next_eip = env->eip + next_eip_addend; cpu_loop_exit(env);}
這裡操作的 env 是 global env,在 tlb_fill 已經同步成當前 env。
helper_svm_check_intercept_param (target-i386/op_helper.c)
cpu_loop_exit (cpu-exec.c)
void cpu_loop_exit(CPUState *env){ env->current_tb = NULL; longjmp(env->jmp_env,1);}
以 linux-0.11 為例,
(gdb) b gen_intermediate_code_internal if tb->pc == 0xe4c0(gdb) r -m 1024M -boot a -fda linux-0.11/Image -hda rootfs/hdc-0.11-new.img -vnc 0.0.0.0:1
Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731 {(gdb) bt#0 gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7731#1 0x0000000000535551 in gen_intermediate_code (env=0x110e5d0, tb=0x7fffe75b9e60) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7907#2 0x000000000050fdee in cpu_x86_gen_code (env=0x110e5d0, tb=0x7fffe75b9e60, gen_code_size_ptr=0x7fffffffdd30) at /tmp/chenwj/qemu-0.13.0/translate-all.c:73#3 0x000000000050871f in tb_gen_code (env=0x110e5d0, pc=58560, cs_base=0, flags=2740, cflags=0) at /tmp/chenwj/qemu-0.13.0/exec.c:962#4 0x0000000000510a7a in tb_find_slow (pc=58560, cs_base=0, flags=2740) at /tmp/chenwj/qemu-0.13.0/cpu-exec.c:167
下 ls。0x4011809f 位在 code cache 之內。
Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=1) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731 {(gdb) bt#0 gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75b9e60, search_pc=1) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7731#1 0x000000000053559e in gen_intermediate_code_pc (env=0x110e5d0, tb=0x7fffe75b9e60) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7912#2 0x000000000050ff4d in cpu_restore_state (tb=0x7fffe75b9e60, env=0x110e5d0, searched_pc=1074888863, puc=0x0) at /tmp/chenwj/qemu-0.13.0/translate-all.c:130#3 0x000000000054effd in tlb_fill (addr=268513280, is_write=1, mmu_idx=0, retaddr=0x4011809f) at /tmp/chenwj/qemu-0.13.0/target-i386/op_helper.c:4836#4 0x000000000054d4f0 in __stb_mmu (addr=268513280, val=150 '\226', mmu_idx=0) at /tmp/chenwj/qemu-0.13.0/softmmu_template.h:272#5 0x00000000401180a0 in ?? ()
----------------IN:0x0000e4c0: sub $0x4,%esp0x0000e4c3: mov 0x8(%esp),?x0x0000e4c7: mov %al,(%esp)0x0000e4ca: movzbl (%esp),?x0x0000e4ce: mov 0xc(%esp),?x0x0000e4d2: mov %al,%fs:(?x) <---0x0000e4d5: add $0x4,%esp0x0000e4d8: ret ---- 0xe4d2 mov_i32 tmp2,edx ld_i32 tmp4,env,$0x84 add_i32 tmp2,tmp2,tmp4 mov_i32 tmp0,eax qemu_st8 tmp0,tmp2,$0x0 <---RESTORE:0x0000: 0000e4c00x0007: 0000e4c30x000d: 0000e4c70x0011: 0000e4ca0x0015: 0000e4ce0x001b: 0000e4d2// spc 是觸發例外的 host binary 位址,等同 retaddr。// eip 是觸發例外的 guest binary 位址。spc=0x401255bf pc_pos=0x1b eip=0000e4d2 cs_base=0Servicing hardware INT=0x20
0x40125577: mov ?x,?p // 保存 CPUState0x40125579: mov ?p,?x0x4012557b: mov 0x84(%r14),%r12d0x40125582: add %r12d,?x0x40125585: mov (%r14),%r12d0x40125588: mov ?p,0x8(%r14)0x4012558c: mov ?x,%esi // 查詢 TLB0x4012558e: mov ?x,?i0x40125590: shr $0x7,%esi0x40125593: and $0xfffff000,?i0x40125599: and $0x1fe0,%esi0x4012559f: lea 0x34c(%r14,%rsi,1),%rsi0x401255a7: cmp (%rsi),?i0x401255a9: mov ?x,?i0x401255ab: jne 0x401255b6 // TLB 不命中,跳至 0x401255b6。0x401255ad: add 0xc(%rsi),%rdi0x401255b1: mov %r12b,(%rdi)0x401255b4: jmp 0x401255c00x401255b6: mov %r12d,%esi // 不命中,呼叫 __stb_mmu0x401255b9: xor ?x,?x0x401255bb: callq 0x54d38a // 執行 __stb_mmu 的時候,發生頁缺失例外。0x401255c0: mov 0x10(%r14),?p
執行 \_\_stb_mmu 的時候,會呼叫 tlb_fill。tlb_fill 呼叫 cpu_x86_handle_mmu_fault 查找客戶機頁表,若該頁存在,將其頁表項填入 tlb; 否則觸發頁缺失例外。
數個 tb 可能會相對應 (不同進程的) guest pc。
Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75d2eb0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731 {Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe76232c0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:77317731 {
請閱讀以下文章:
TCG 是 dynamic binary tranlation。QEMU 1.0 預計會加入 TCI (tiny code interpreter)。請見 [Qemu-devel] [PATCH 0/8] tcg/interpreter: Add TCG + interpreter for bytecode (virtual machine)。在上下文有提到 TCG 的話,其所指的 target 必定是指 QEMU 運行其上的 host。21)
底下是 TCG 動態翻譯流程中會使用到的緩衝區:
gen_opc_buf 和 gen_opparam_buf (translate-all.c) 是 TCG IR 所在位置。
uint16_t gen_opc_buf[OPC_BUF_SIZE]; // 放置 TCG opcode。TCGArg gen_opparam_buf[OPPARAM_BUF_SIZE]; // 放置 TCG opcode 會用到的 operand。
如果使用靜態配置的緩衝區,static_code_gen_buffer (exec.c) 是 translation block code cache (host binary) 所在位置。
#ifdef USE_STATIC_CODE_GEN_BUFFERstatic uint8_tstatic_code_gen_buffer[DEFAULT_CODE_GEN_BUFFER_SIZE] __attribute__((aligned(CODE_GEN_ALIGN)));#endif
在跳入/出 code cache 執行之前/後,要執行 prologue/epilogue。
// 根據不同平台,code_gen_section 用不同的 __attribute__ 修飾 code_gen_prologue。uint8_tcode_gen_prologue[1024] code_gen_section;
以 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)
tcg_prologue_init (tcg/tcg.c) → tcg_target_qemu_prologue (tcg/i386/tcg-target.c)
進 translation block code cache 之前和之後,需要執行 prologue 和 epilogue。請參考 和 。
void tcg_prologue_init(TCGContext *s){ s->code_buf = code_gen_prologue; s->code_ptr = s->code_buf;tcg_target_qemu_prologue(s); flush_icache_range((unsigned long)s->code_buf,(unsigned long)s->code_ptr);} static void tcg_target_qemu_prologue(TCGContext *s){// QEMU -> prologue -> code cache。prologue -> code cache 被當作一個函式。 // 寫入 host binary。 tcg_out_push(s, tcg_target_callee_save_regs[i]); // 將 callee saved 的暫存器入棧 tcg_out_addi(s, TCG_REG_ESP, -stack_addend); // 調整棧指針,加大棧 // OPC_GRP5 (0xff) 為 call,EXT5_JMPN_Ev 是其 opcode extension。 // tcg_target_call_iarg_regs 是函式呼叫負責傳遞參數的暫存器。 tcg_out_modrm(s, OPC_GRP5,EXT5_JMPN_Ev, tcg_target_call_iarg_regs[0]); // 跳至 code cache 執行 // 此時,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; tcg_out_addi(s, TCG_REG_ESP, stack_addend); // 調整棧指針,縮小棧 tcg_out_pop(s, tcg_target_callee_save_regs[i]); // 回復 callee saved 的暫存器 tcg_out_opc(s, OPC_RET, 0, 0, 0); // 返回 QEMU}
cpu_loop (linux-user/main.c) → cpu_x86_exec/cpu_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c)
tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c) → 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)
tcg_gen_code_common (tcg/tcg.c) → tcg_reg_alloc_op (tcg/tcg.c) → tcg_out_op (tcg/i386/tcg-target.c)
cpu_exec (cpu-exec.c) 為主要執行迴圈。cpu_exec_nocache 是在 system mode 啟用 icount 的時候才會被用到
next_tb = 0; for(;;) { // 處理中斷。這裡會檢視是何種中斷,並將 interrupt_request 復位。設置 exception_index,再跳至 cpu_loop_exit。 // cpu_loop_exit 再 longjmp 到外層迴圈 setjmp 的點,跳到處理中斷的分支。 if(unlikely(interrupt_request)) { if (interrupt_request & CPU_INTERRUPT_DEBUG) { env->interrupt_request &= ~CPU_INTERRUPT_DEBUG; env->exception_index = EXCP_DEBUG;cpu_loop_exit(); } } tb_find_fast(); // 查詢 TB 是否已存在 code cache。若無,則呼叫 tb_find_slow // 執行 TB,也就是 tc_ptr 所指到的位址。注意,產生 TCG IR 的過程中,在 block 的最後會是 // exit_tb addr,此 addr 是正在執行的這個 block 起始位址,同時也是 tcg_qemu_tb_exec 的回傳值。 // 該位址後兩位會被填入 0、1 或 2 以指示 block chaining 的方向。 next_tb = tcg_qemu_tb_exec(tc_ptr);}
tb_find_fast 會以 pc (虛擬位址)試圖尋找已翻譯過的 translation block。
tb = env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];
如果失敗,則呼叫 tb_find_slow 以 pc (cs + eip) 對映的物理位址尋找 TB。如果成功,則將該 TB 寫入 tb_jmp_cache; 若否,則進行翻譯。
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;
tb_gen_code 配置內存給 TB,再交由 cpu_gen_code。
// 注意! 這裡會將虛擬位址轉成物理位址。phys_pc 將交給之後的 tb_link_page 使用。// get_page_addr_code 在 process mode 直接返回 pc; system mode 則透過查找// env 的 tlb_table 返回 GPA 在客戶機內存中的偏移量。phys_pc = get_page_addr_code(env, pc);tb =tb_alloc(pc);if (!tb) { // 清空 code cache} // 初始 tb cpu_gen_code(env, tb,&code_gen_size); // 開始 guest binary -> TCG IR -> host binary 的翻譯。 // 將 tb 加入 tb_phys_hash 和二級頁表 l1_map。// phys_pc 和 phys_page2 分別代表 tb (guest pc) 對映的物理位址和所屬的第二個頁面 (如果 tb 代表的 guest binary 跨頁面的話)。tb_link_page(tb,phys_pc, phys_page2);return tb;
cpu_gen_code 負責 guest binary → TCG IR → host binary 的翻譯。
tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr gen_intermediate_code(env, tb); // 呼叫 gen_intermediate_code_internal 產生 TCG IR gen_code_size = tcg_gen_code(s,gen_code_buf); // TCG IR -> host binary
tcg_func_start (tcg/tcg.c)
void tcg_func_start(TCGContext *s){ int i; tcg_pool_reset(s); s->nb_temps = s->nb_globals; for(i = 0; i < (TCG_TYPE_COUNT * 2); i++) s->first_free_temp[i] =-1; s->labels = tcg_malloc(sizeof(TCGLabel) * TCG_MAX_LABELS); s->nb_labels = 0;s->current_frame_offset = s->frame_start; gen_opc_ptr = gen_opc_buf;gen_opparam_ptr = gen_opparam_buf;}
gen_intermediate_code_internal (target-*/translate.c) 初始化並呼叫 disas_insn 反組譯 guest binary 成 TCG IR。
disas_insn 呼叫 tcg_gen_xxx (tcg/tcg-op.h) 產生 TCG IR。分別將 opcode 寫入 gen_opc_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opc_buf); operand 寫入 gen_opparam_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opparam_buf )。
tcg_gen_code (tcg/tcg.c) 呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 轉成 host binary。
tcg_reg_alloc_start(s); s->code_buf = gen_code_buf;s->code_ptr = gen_code_buf; // host binary 會寫入 TCGContext s 的 code_ptr 所指向的緩衝區。
tb_link_page (exec.c) 把新的 TB 加進 tb_phys_hash 和 l1_map 二級頁表。tb_find_slow 會用 pc 對映的物理位址的哈希值索引 tb_phys_hash (存放 TranslationBlock *) 取得下一個 TB。
h = tb_phys_hash_func(phys_pc);ptb =&tb_phys_hash[h];tb->phys_hash_next = *ptb; // 如果兩個以上的 TB 其 phys_pc 的哈希值相同,則做 chaining。*ptb = tb; // 新加入的 TB 放至 chaining 的開頭。 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; // jmp_first 代表跳至此 TB 的其它 TB 中的頭一個。jmp_first 初值為自己,末兩位為 10 (2)。// 將來做 block chaining 時,jmp_first 指向跳至此 TB 的其它 TB 中的頭一個 tb1,末兩位為 00// 或 01,這代表從 tb1 的哪一個分支跳至此 TB。tb->jmp_first = (TranslationBlock*)((long)tb | 2);// jmp_next[n] 代表此 TB 條件分支的目標 TB。// 注意! 如果目標 TB,tb1,孤身一人,jmp_next 就真的指向 tb1。// 如果其它 TB,tb2,跳至 tb1,則賦值給 tb->jmp_next 的是 tb1 的 jmp_first,也就是 tb1 (末兩位編碼 tb2 跳至 tb1 的方向)。tb->jmp_next[0] = NULL;tb->jmp_next[1] = NULL; // tb_next_offset 代表此 TB 在 code cache 中分支跳轉要被 patch 的位址 (相對於其 code cache 的偏移量),// 為了 direct block chaining 之用。if (tb->tb_next_offset[0] != 0xffff) tb_reset_jump(tb, 0);if (tb->tb_next_offset[1] != 0xffff) tb_reset_jump(tb, 1);
tb_alloc_page 建立新的 PageDesc。(exec.c)。
static inline void tb_alloc_page(TranslationBlock *tb, unsigned int n,tb_page_addr_t page_addr){ // 代表 tb (guest binary) 所屬頁面。 tb->page_addr[n] =page_addr; // 在 l1_map 中配置一個 PageDesc,返回該 PageDesc。 p =page_find_alloc(page_addr >> TARGET_PAGE_BITS, 1); tb->page_next[n] = p->first_tb; // 將該頁面目前第一個 TB 串接到此 TB。將來有需要將某頁面所屬所有 TB 清空。 // n 為 1 代表 tb 對應的 guest binary 跨 page。 p->first_tb = (TranslationBlock*)((long)tb | n); invalidate_page_bitmap(p);}
tb_reset_jump 呼叫 tb_set_jmp_target 使得該 TB 不會鏈結到其它 TB。根據是否使用 direct jump 做 block chaining 與否,tb_set_jmp_target 直接修改 TB (code cache) 跳躍目標或是 TB 的 tb_next。
tb_find_fast/slow 傳回 translation block 後,交給 tcg_qemu_tb_exec 執行。
next_tb = tcg_qemu_tb_exec(tc_ptr);
tcg_qemu_tb_exec 被定義在 tcg/tcg.h。
// code_gen_prologue(tb_ptr) is casted to a function with one parameter,// in such a way, execute host machine code stored in code_gen_prologue[]#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。code_gen_prologue 所做的事為一般函式呼叫前的 prologue,之後將控制交由 tc_ptr 指向的 host binary 並開始執行。
有幾種情況會需要打斷 code cache 的執行,將控制權將還給 QEMU。
cpu_interrupt: 虛擬外設會發出中斷,最後透過 apic_local_deliver (hw/apic.c) 呼叫 cpu_interrupt 發出中斷給虛擬 CPU。QEMU 0.15 將system mode 和 process mode 的 cpu_interrupt 分離,請見 cpu-all.h 和 exec.c。
cpu_exit: system mode 會註冊 host alarm signal handler,該 handler 會呼叫 cpu_exit。其它諸如 gdb stub,DMA,IO thread 都會呼叫到 cpu_exit。
針對 self-modifying code 或是 JIT,在內存生成代碼之後通常需要清空快取22)。
請閱讀一下文章
為了盡量在 code cache 中執行,QEMU 會做 block chaining。block chaining 是將 code cache 中的 translation block 串接起來。有兩種做法: 第一,採用 direct jump。此法直接修改 code cache 中分支指令的跳躍目標,因此依據 host 有不同的 patch 方式。第二,則是透過修改 TB 的 tb_next 欄位達成 block chaining。exec-all.h 中定義那些 host 可以使用 direct jump。
tb = tb_find_fast(env); if (tb_invalidated_flag) { next_tb = 0; // 注意! next_tb 也被用來控制是否要做 block chaining。 tb_invalidated_flag = 0;} // 注意!! next_tb 的名字會讓人誤解。block chaining 的方向為: next_tb -> tb。// next_tb 不為 NULL 且 tb (guest binary) 不跨頁面的話,做 block chaining。if (next_tb != 0 && tb->page_addr[1] == -1) { // 這邊利用 TranlationBlock 指針的最低有效位後兩位指引 block chaining 的方向。 tb_add_jump((TranslationBlock *)(next_tb &~3), next_tb & 3, tb);} // 執行 TB,也就是 tc_ptr 所指到的位址。注意,產生 TCG IR 的過程中,在 block 的最後會是 // exit_tb addr,此 addr 是正在執行的這個 block 起始位址,同時也是 tcg_qemu_tb_exec 的回傳值。 // 該位址後兩位會被填入 0、1 或 2 以指示 block chaining 的方向。next_tb = tcg_qemu_tb_exec(env, tc_ptr);
tb_add_jump 呼叫 tb_set_jmp_target 做 block chaining 的 patch。另外,會利用 tb 的 jmp_next 和 tb_next 的 jmp_first 把 block chaining 中的 tb 串成一個 circular list。這邊要注意到,QEMU 利用 TranslatonBlock 位址後兩位必為零的結果做了一些手腳。(exec-all.h)。
// block chaining 方向為: tb -> tb_next。n 用來指示 tb 條件分支的方向。static inlinevoid tb_add_jump(TranslationBlock *tb, int n, TranslationBlock *tb_next){ // jmp_next[0]/jmp_next[1] 代表 tb 條件分支的目標。 if (!tb->jmp_next[n]) { tb_set_jmp_target(tb, n, (unsigned long)tb_next->tc_ptr); // tb_jmp_remove 會用到 jmp_next 做 unchain。 // tb_next->jmp_first 初值為自己,末兩位設為 10 (2)。 // 如果已有其它 TB,tb1,跳至 tb_next,則 tb->jmp_next 指向 tb1 (末兩位代表 tb1 跳至 tb_next 的方向)。 // tb_next->jmp_first 改指向 tb。 tb->jmp_next[n] = tb_next->jmp_first; // tb_next 的 jmp_first 指回 tb,末兩位代表由 tb 哪一個條件分支跳至 tb_next。tb_next->jmp_first = (TranslationBlock *)((long)(tb) | (n)); }}
依據是否採用 direct jump,tb_set_jmp_target (exec-all.h) 有不同做法。採用 direct jump 的話,tb_set_jmp_target 會根據 host 呼叫不同的 tb_set_jmp_target1。tb_set_jmp_target1 會用到 TB 的 tb_jmp_offset。如果不採用 direct jump 做 block chaining,tb_set_jmp_target 會直接修改 TB 的 tb_next。
tb_set_jmp_target (exec-all.h)。
static inline void tb_set_jmp_target(TranslationBlock *tb, int n, unsigned longaddr){ unsigned long offset; offset = tb->tb_jmp_offset[n]; // tb 要 patch 的位址相對於 tb->tc_ptr 的偏移量。 tb_set_jmp_target1((unsigned long)(tb->tc_ptr + offset),addr);}
tb_set_jmp_target1 (exec-all.h)。
#elif defined(__i386__) || defined(__x86_64__)static inline voidtb_set_jmp_target1(unsigned long jmp_addr, unsigned long addr){ *(uint32_t *)jmp_addr = addr - (jmp_addr + 4); // jmp 的參數為 jmp 下一條指令與目標地址的偏移量。 }
由 guest binary → TCG IR 的過程中,gen_goto_tb 會做 block chaining 的準備。請見 2.2.3 和 2.2.4 節。以 i386 為例: (target-i386/translate.c):
// tb_num 代表目前 tb block linking 分支情況。eip 代表跳轉目標。static inline voidgen_goto_tb(DisasContext *s, int tb_num, target_ulong eip){ TranslationBlock *tb;target_ulong pc; // s->pc 代表翻譯至目前 guest binary 的所在位址。tb->pc 表示 guest binary 的起始位址。 // 注意! 這裡 s->cs_base + eip 代表跳轉位址; s->pc 代表目前翻譯到的 guest pc。見 target-i386/translate.c 中的 case 0xe8。 pc = s->cs_base + eip; // 計算跳轉目標的 pc tb = s->tb; // 目前 tb // http://lists.nongnu.org/archive/html/qemu-devel/2011-08/msg02249.html// 滿足底下兩個條件之一,則可以做 direct block linking // 第一,跳轉目標和目前 tb 起始的 pc 同屬一個 guest page。 // 第二,跳轉目標和目前翻譯到的 pc 同屬一個 guest page。 if ((pc &TARGET_PAGE_MASK) == (tb->pc & TARGET_PAGE_MASK) || (pc & TARGET_PAGE_MASK) == ((s->pc- 1) & TARGET_PAGE_MASK)) { // 如果 guest jump 指令和其跳轉位址同屬一個 guest page,則做 direct block linking。tcg_gen_goto_tb(tb_num); // 生成準備做 block linking 的 TCG IR。詳情請見之後描述。 // 更新 env 的 eip 使其指向此 block 之後欲執行指令的位址。 // tb_find_fast 會用 eip 查找該 TB 是否已被翻譯過。 gen_jmp_im(eip); // 最終回到 QEMU tcg_qemu_tb_exec,賦值給 next_tb。 // 注意! tb_num 會被 next_tb & 3 取出,由此可以得知 block chaining 的方向。tcg_gen_exit_tb((tcg_target_long)tb + tb_num); } else { gen_jmp_im(eip); gen_eob(s); }}
tcg_gen_goto_tb 生成 TCG IR。
static inline void tcg_gen_goto_tb(int idx){ tcg_gen_op1i(INDEX_op_goto_tb, idx);}
tcg/i386/tcg-target.c 將 TCG IR 翻成 host binary。注意! 這邊利用 patch jmp 跳轉位址達成 block linking。
static inline void tcg_out_op(TCGContext *s, TCGOpcode opc, const TCGArg *args,const int *const_args){ case INDEX_op_goto_tb: if (s->tb_jmp_offset) { tcg_out8(s, OPC_JMP_long); // 紀錄將來要 patch 的地方。 s->tb_jmp_offset[args[0]] = s->code_ptr - s->code_buf; // jmp 的參數為 jmp 下一個指令與目標的偏移量。 // 如果還沒做 block chaining,則 jmp 0 代表 fall through。 tcg_out32(s, 0);} else { tcg_out_modrm_offset(s, OPC_GRP5, EXT5_JMPN_Ev,-1, (tcg_target_long)(s->tb_next + args[0])); } s->tb_next_offset[args[0]] = s->code_ptr - s->code_buf; break;
遇到 guest binary 中的條件分支和直接跳轉都會呼叫 gen_goto_tb (target-i386/translate.c)。以 i386 直接跳轉為例:
static void gen_jmp_tb(DisasContext *s, target_ulong eip, int tb_num){ if (s->jmp_opt) { // 使用 direct jump 實現 block chaining gen_update_cc_op(s);gen_goto_tb(s, tb_num, eip); // tb_num 指示目前 tb 分支方向,eip 是下一個 tb 位址。 s->is_jmp = DISAS_TB_JUMP; } else { gen_jmp_im(eip); gen_eob(s); }} static voidgen_jmp(DisasContext *s, target_ulong eip){ // 注意! DisasContext 的 pc 值是 eip + cs_base。 // 直接跳轉至 eip。 gen_jmp_tb(s, eip, 0);}
請見,
Re: [Qemu-devel] The reason behind block linking constraint?
http://lists.gnu.org/archive/html/qemu-devel/2012-04/msg01032.html
非常重要!
當 TLB (QEMU 替每個 env 維護的 software TLB,負責 GVA → HVA 的轉換) 不命中,會尋訪 guest page table (GVA → GPA),再由 tlb_set_page 將 guest page table entry 帶入 TLB,這裡會將 GPA 換成 HVA 以便之後能做 GVA → HVA 的轉換。當 TLB 的內容經由 tlb_set_page 改變時,代表原本 GVA → HVA 的對映會失效,這代表 guest program 的某個 guest physical page 被 swap out,或是 guest OS 做 task switching。
注意! QEMU 是將 guest binary (位於 guest physical page) 翻譯成 host binary 並執行。因此,當某個 guest physical page 被 swap out,或是 guest OS 做 task switching,則屬於該 guest physical page 的所有 TB 皆屬失效,不應該被執行。QEMU 一次會清空一個 guest physical page 的所有 TB。
假設我們要連結 tb1 和 tb2,也就是 tb1 → tb2。只有在 tb2 (對應的 guest binary) 起始位址和 tb1 (對應的 guest binary) 起始位址或尾巴落在同一個 guest page 才能做 direct block linking。這是因為當 TLB (QEMU 替每個 env 維護的 software TLB) 的內容經由 tlb_set_page 改變時,原本 guest page 內的 TB 必須被沖掉。
如果 block linking 沒有上述限制,則會執行到不該執行的 host binary。
假設跨 guest page 的 tb1 和 tb2 之間沒有 direct block chaining,亦即 tb1 和 tb2 中間會回到 QEMU。QEMU 就可以透過 tb_find_fast → tb_find_slow → get_page_addr_code 查找 TLB 並發出 guest exception。
移除該限制,運行 linux-0.11。登入後,下 bash 會出問題。
static inline void gen_goto_tb(DisasContext *s, int tb_num, target_ulong eip){TranslationBlock *tb; target_ulong pc; pc = s->cs_base + eip; tb = s->tb;#if 0 if ((pc &TARGET_PAGE_MASK) == (tb->pc & TARGET_PAGE_MASK) || (pc & TARGET_PAGE_MASK) == ((s->pc- 1) & TARGET_PAGE_MASK)) { #endiftcg_gen_goto_tb(tb_num); gen_jmp_im(eip); tcg_gen_exit_tb((long)tb + tb_num);#if 0 }else { gen_jmp_im(eip);gen_eob(s); }#endif}
$ git clone git://jcmvbkbc.spb.ru/dumb/qemu-test-kernel.git$ cd qemu-test-kernel$ ./autogen.sh; configure; make