QEMU 的簡介請見 QEMU internals。
QEMU 的簡介請見 QEMU internals。
QEMU 中的 target 有兩種意義,
QEMU 0.9 版以前使用 dyngen,對於 dyngen 的描述可以參考以下文件。QEMU 0.10 以後改採 TCG。可以從下載 QEMU Source Archive 源碼。
請先閱讀 Documentation/GettingStartedDevelopers。
–extra-cflags="-save-temps"
可以得到宏展開之後的檔案 *.i。簡單的 patch 請送到 ,請見 Contribute/TrivialPatches。
QEMU 1.0 將有變動。請見 HACKING、docs/memory.txt 或 Planning/1.0。
請見 memory.[ch]。
以宿主機作業系統的角度來看,QEMU 就是一般的使用者進程。QEMU 會在自己的虛擬位址空間分配內存給客戶機作業系統。以客戶機作業系統的角度來看,該塊內存即是客戶機作業系統的物理內存。該物理內存分成一般使用的內存和內存映射 IO。透過 cpu_register_physical_memory_offset (exec.c) 註冊。hw/* 會依序呼叫 cpu_register_io_memory 註冊 IO 模擬的函式和 cpu_register_physical_memory。因為 QEMU 看不到宿主機物理內存,所以註解中所提到的 physical 代表的是 guest physical。
QEMU 分配給客戶機的內存是以 RAMBlock 和 RAMList 來管理。內存主要分為底下幾類 (cpu-common.h):
ram_addr_t qemu_ram_alloc(DeviceState *dev, const char *name, ram_addr_t size) { RAMBlock *new_block, *block; // RAMBlock 會被賦予一個字串名稱。 pstrcat(new_block->idstr, sizeof(new_block->idstr), name); // 檢視 RAMList 中是否已有具有相同名稱的 RAMBlock。 QLIST_FOREACH(block, &ram_list.blocks, next) { } if (mem_path) { } else { // 指向宿主的虛擬內存位址。 new_block->host = qemu_vmalloc(size); } new_block->offset = find_ram_offset(size); // 該 RAMBlock 在 RAMList 的偏移量。 new_block->length = size; // 該 RAMBlock 的大小。 QLIST_INSERT_HEAD(&ram_list.blocks, new_block, next); // 將新增的 RAMBlock 加入 RAMList。 ram_list.phys_dirty = qemu_realloc(ram_list.phys_dirty, last_ram_offset() >> TARGET_PAGE_BITS); memset(ram_list.phys_dirty + (new_block->offset >> TARGET_PAGE_BITS), 0xff, size >> TARGET_PAGE_BITS); return new_block->offset; // 回傳該 RAMBlock 在 RAMList 的偏移量。
void cpu_register_physical_memory_offset(target_phys_addr_t start_addr, ram_addr_t size, ram_addr_t phys_offset, ram_addr_t region_offset) { PhysPageDesc *p; for(addr = start_addr; addr != end_addr; addr += TARGET_PAGE_SIZE) { p = phys_page_find(addr >> TARGET_PAGE_BITS); // 用客戶機物理位址 start_addr 查找 l1_phys_map if (p && p->phys_offset != IO_MEM_UNASSIGNED) { // 1_phys_map 中已存在該客戶機物理位址的項目。 } else { // 針對該客戶機物理位址在 1_phys_map 中配置 PhysPageDesc 並更新相應的欄位。 p = phys_page_find_alloc(addr >> TARGET_PAGE_BITS, 1); } } }
static void cirrus_init_common(CirrusVGAState * s, int device_id, int is_pci) { s->vga.vga_io_memory = cpu_register_io_memory(cirrus_vga_mem_read, cirrus_vga_mem_write, s); cpu_register_physical_memory(isa_mem_base + 0x000a0000, 0x20000, s->vga.vga_io_memory); }
PhysPageDesc 用來描述客戶機物理頁面和宿主機虛擬頁面的對映。有一個二級頁表 l1_phys_map 存放 PhysPageDesc。phys_page_find_alloc 用客戶機物理位址查詢 l1_phys_map 取得 PhysPageDesc,視情況配置新的 PhysPageDesc。
static PhysPageDesc *phys_page_find_alloc(target_phys_addr_t index, int alloc) { // 取得一級頁表項 lp = l1_phys_map + ((index >> P_L1_SHIFT) & (P_L1_SIZE - 1)); // 視 alloc 是否要分配二級頁表項 for (i = P_L1_SHIFT / L2_BITS - 1; i > 0; i--) { } pd = *lp; if (pd == NULL) { for (i = 0; i < L2_SIZE; i++) { pd[i].phys_offset = IO_MEM_UNASSIGNED; pd[i].region_offset = (index + i) << TARGET_PAGE_BITS; } } }
PageDesc 維護 TB 和虛擬頁面/客戶機物理頁面之間的關係 (視 process/system mode 而定)。同樣有一個二級頁表 l1_map 存放 PageDesc。page_find_alloc 查詢 l1_map 取得 PageDesc,視情況配置新的 PageDesc。
static PageDesc *page_find_alloc(tb_page_addr_t index, int alloc) { // ALLOC 在 process mode 使用 mmap; 在 system mode 使用 qemu_mallocz。 /* Level 1. Always allocated. */ lp = l1_map + ((index >> V_L1_SHIFT) & (V_L1_SIZE - 1)); /* Level 2..N-1. */ for (i = V_L1_SHIFT / L2_BITS - 1; i > 0; i--) { } }
QEMU 基本上是以 page 為單位將該 page 所屬 TB 清掉。
void tb_invalidate_phys_page_range(tb_page_addr_t start, tb_page_addr_t end, ...) { p = page_find(start >> TARGET_PAGE_BITS); /* we remove all the TBs in the range [start, end[ */ tb = p->first_tb; while (tb != NULL) { } }
#if !defined(CONFIG_SOFTMMU) static void tb_invalidate_phys_page(tb_page_addr_t addr, unsigned long pc, void *puc) { addr &= TARGET_PAGE_MASK; p = page_find(addr >> TARGET_PAGE_BITS); // 取得該 page 的第一個 tb。 // tb 末兩位如果是 01 (1),代表 tb 對應的 guest bianry 跨 page。 tb = p->first_tb; while (tb != NULL) { n = (long)tb & 3; // 取得 block chaing 的方向 tb = (TranslationBlock *)((long)tb & ~3); // 去掉末兩位的編碼,還原回真正的 tb tb_phys_invalidate(tb, addr); tb = tb->page_next[n]; // 取得 tb 所屬 page (或下一個 page) 的下一個 tb } p->first_tb = NULL; }
void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr) { // 將該 tb 從 tb_phys_hash 中移除 phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣 h = tb_phys_hash_func(phys_pc); tb_remove(&tb_phys_hash[h], tb, offsetof(TranslationBlock, phys_hash_next)); // 將 tb 從相應的 PageDesc 中移除 if (tb->page_addr[0] != page_addr) { p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) { p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } tb_invalidated_flag = 1; // 將 tb 從 tb_jmp_cache 移除 h = tb_jmp_cache_hash_func(tb->pc); // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。 for(env = first_cpu; env != NULL; env = env->next_cpu) { if (env->tb_jmp_cache[h] == tb) env->tb_jmp_cache[h] = NULL; } // 處理 tb1 (tb -> tb1) tb_jmp_remove(tb, 0); tb_jmp_remove(tb, 1); // 處理 tb1 (tb1 -> tb) tb1 = tb->jmp_first; for(;;) { n1 = (long)tb1 & 3; if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb break; tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1 tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache) tb1->jmp_next[n1] = NULL; tb1 = tb2; } tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己 }
static inline void tb_jmp_remove(TranslationBlock *tb, int n) { ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向 tb1 = *ptb; // 處理 tb1 (tb -> tb1) if (tb1) { /* find tb(n) in circular list */ for(;;) { tb1 = *ptb; n1 = (long)tb1 & 3; // 取出 tb1 末兩位 tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1 if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb break; if (n1 == 2) { ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1 } else { ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) } } /* now we can suppress tb(n) from the list */ *ptb = tb->jmp_next[n]; tb->jmp_next[n] = NULL; } }
if (tb_invalidated_flag) { /* as some TB could have been invalidated because of memory exceptions while generating the code, we must recompute the hash index here */ next_tb = 0; tb_invalidated_flag = 0; }
請見 memory.[ch]
和 doc/memory.txt
。
MemoryRegion
(memory.h
)。struct MemoryRegion { /* All fields are private - violators will be prosecuted */ const MemoryRegionOps *ops; void *opaque; MemoryRegion *parent; Int128 size; target_phys_addr_t addr; void (*destructor)(MemoryRegion *mr); ram_addr_t ram_addr; bool subpage; bool terminates; bool readable; bool ram; bool readonly; /* For RAM regions */ bool enabled; bool rom_device; bool warning_printed; /* For reservations */ MemoryRegion *alias; target_phys_addr_t alias_offset; unsigned priority; bool may_overlap; QTAILQ_HEAD(subregions, MemoryRegion) subregions; QTAILQ_ENTRY(MemoryRegion) subregions_link; QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced; const char *name; uint8_t dirty_log_mask; unsigned ioeventfd_nb; MemoryRegionIoeventfd *ioeventfds; };
以 QEMU 1.0 版以前,qemu (i386-softmmu) 為例,主要流程如下:
main (vl.c) → init_clocks (qemu-timer.c) → module_call_init(MODULE_INIT_MACHINE) (module.c) → cpu_exec_init_all (初始 dynamic translator) (exec.c) → module_call_init(MODULE_INIT_DEVICE) (module.c) → machine→init (初始 machine) (vl.c) → main_loop (vl.c)
check exception -> check interrupt (setjmp) -> tb_find_fast -> tb_exec -> check exception (check interrupt) main_loop_wait -> select (alarm)
QEMU 會設置定時器 (qemu_signal_init),定時發出 SINGALARM 將 QEMU 從 code cache 拉出,去檢查 exception 或 interrupt。
int main(int argc, char **argv, char **envp) { // QEMU 內部維護三個 clock,分別為: rt_clock,vm_clock 和 host_clock。 // 之後會根據命令行參數將 rtc_clock 設為前述三者之一。 init_clocks(); // module_call_init -> pc_machine_init -> qemu_register_machine // 會有預設 QEMUMachine,之後處理命令行參數時可被替換。 module_call_init(MODULE_INIT_MACHINE); /* 處理命令行參數,並初始化環境 */ // 初始 QEMU 會用到的鎖以及使用的 signal number if (qemu_init_main_loop()) { fprintf(stderr, "qemu_init_main_loop failed\n"); exit(1); } // alarm_timers 數組存放各種 timer 相對應的啟動/終止函式指針,以及其它資料。 // init_timer_alarm 依序呼叫 alarm_timers 數組中各個 timer 的啟動函式。 // dynticks_start_timer 會註冊 SIGALRM 相對應的信號句柄。 if (init_timer_alarm() < 0) { fprintf(stderr, "could not initialize alarm timer\n"); exit(1); } /* init the dynamic translator */ cpu_exec_init_all(tb_size * 1024 * 1024); // drive_init_func 最後會呼叫到 paio_init 註冊 SIGUSR2 的信號句柄。 if (qemu_opts_foreach(&qemu_drive_opts, drive_init_func, &machine->use_scsi, 1) != 0) exit(1); // 初始化設備 module_call_init(MODULE_INIT_DEVICE); // 建立 QEMUMachine (hw/pc_piix.c) 並呼叫 machine->init (pc_init_pci) 初始化。 machine->init(ram_size, boot_devices, kernel_filename, kernel_cmdline, initrd_filename, cpu_model); /* 初始化剩下的設備以及輸出設備 */ main_loop(); // 主要執行迴圈 quit_timers(); net_cleanup(); return 0; }
/* Must be called before using the QEMU cpus. 'tb_size' is the size (in bytes) allocated to the translation buffer. Zero means default size. */ void cpu_exec_init_all(unsigned long tb_size) { cpu_gen_init(); code_gen_alloc(tb_size); code_gen_ptr = code_gen_buffer; page_init(); #if !defined(CONFIG_USER_ONLY) io_mem_init(); // 註冊 MMIO 回掉函式 #endif #if !defined(CONFIG_USER_ONLY) || !defined(CONFIG_USE_GUEST_BASE) /* There's no guest base to take into account, so go ahead and initialize the prologue now. */ tcg_prologue_init(&tcg_ctx); #endif }
/* PC hardware initialisation */ static void pc_init1(ram_addr_t ram_size, ...) { // 呼叫 pc_new_cpu (hw/pc.c) -> cpu_init/cpu_x86_init (target-i386/helper.c) 初始化 CPU。 pc_cpus_init(cpu_model); // 配置客戶機內存,載入 BIOS。 // 這部分在 QEMU 1.0 會用 memory API 改寫。 // http://lists.gnu.org/archive/html/qemu-devel/2011-07/msg02716.html pc_memory_init(ram_size, kernel_filename, kernel_cmdline, initrd_filename, &below_4g_mem_size, &above_4g_mem_size); // 呼叫 qemu_allocate_irqs (hw/irq.c) 設置中斷處理常式。 cpu_irq = pc_allocate_cpu_irq(); pc_vga_init(pci_enabled? pci_bus: NULL); /* init basic PC hardware */ pc_basic_device_init(isa_irq, &floppy_controller, &rtc_state); pc_vga_init(pci_enabled? pci_bus: NULL); /* init basic PC hardware */ pc_basic_device_init(isa_irq, &floppy_controller, &rtc_state); }
void pc_memory_init(ram_addr_t ram_size, ...) { // 透過 qemu_ram_alloc 跟 QEMU 申請內存空間。QEMU 以 RAMBlock 為單位分配內存,並以 RAMList 管理所有 RAMBlock。 // QEMU 依命令行參數的不同,會從檔案或是跟宿主機作業系統申請 (posix_memalign) 配置空間。 // 回傳的是 RAMBlock 在 RAMList 的偏移量。 ram_addr = qemu_ram_alloc(NULL, "pc.ram", below_4g_mem_size + above_4g_mem_size); // 所有類型的 RAM (一般內存、內存映射 IO) 皆要透過 cpu_register_physical_memory 跟 QEMU 註冊。 // 將該資訊記錄在 PhysPageDesc。 cpu_register_physical_memory(0, 0xa0000, ram_addr); cpu_register_physical_memory(0x100000, below_4g_mem_size - 0x100000, ram_addr + 0x100000); }
static void main_loop(void) { // 若是沒有開啟 IO 執行緒的話,無作用。 qemu_main_loop_start(); // 主要執行的無窮迴圈。 for (;;) { do { bool nonblocking = false; #ifndef CONFIG_IOTHREAD nonblocking = cpu_exec_all(); // 翻譯並執行客戶端代碼 #endif main_loop_wait(nonblocking); // 處理 IO } while (vm_can_run()); // 如果此虛擬機沒有收到關機或是重開機等諸如此類的請求,則繼續執行。 /* 檢查系統是否收到關機或是重開機的要求。若是關機,則跳離此無窮迴圈 */ } bdrv_close_all(); // 關閉所有設備 pause_all_vcpus(); // 暫無作用 }
bool cpu_exec_all(void) { // 依序檢視虛擬處理器 for (; next_cpu != NULL && !exit_request; next_cpu = next_cpu->next_cpu) { CPUState *env = next_cpu; qemu_clock_enable(vm_clock, (env->singlestep_enabled & SSTEP_NOTIMER) == 0); if (qemu_alarm_pending()) break; if (cpu_can_run(env)) { // qemu_cpu_exec 以 process mode 的路徑執行。 // cpu_x86_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c) // cpu_exec 執行完後會返回 exception_index 狀態,狀態定義在 cpu-defs.h。 if (qemu_cpu_exec(env) == EXCP_DEBUG) { break; } } else if (env->stop) { break; } } exit_request = 0; return any_cpu_has_work(); }
void main_loop_wait(int nonblocking) { nfds = -1; FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&xfds); QLIST_FOREACH(ioh, &io_handlers, next) { // 將欲處理的設備加入上述的 file set } // 根據 nonblocking 與否計算 select 等待時間 tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout % 1000) * 1000; // 將設備以 file descriptor 來處理 qemu_mutex_unlock_iothread(); // 用 select 由設備描述符中選擇一個能立即處理的設備 // select 參數代表的意義分別是: 欲處理的設備個數,要處理的輸入設備的檔案描述詞的集合,要處理的輸出設備的檔案描述詞的集合, // 有突發狀態發生的設備的檔案描述詞的集合和要求 select 等待的時間。 ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv); qemu_mutex_lock_iothread(); if (ret > 0) { IOHandlerRecord *pioh; QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) { /* 處理設備 */ } } qemu_run_all_timers(); /* Check bottom-halves last in case any of the earlier events triggered them. */ qemu_bh_poll(); }
QEMU 1.0 開啟 IO thread,無法關閉。仍舊以 qemu-system-i386 為例:
模擬虛擬 CPU 和虛擬外設分為不同的執行緒。開機時至少會看到兩個執行緒,主執行緒處理 IO,另一個則是模擬虛擬 CPU 的執行緒。模擬客戶機 CPU 的流程如下:
CPUX86State *cpu_x86_init(const char *cpu_model) { CPUX86State *env; static int inited; env = g_malloc0(sizeof(CPUX86State)); cpu_exec_init(env); env->cpu_model_str = cpu_model; /* init various static tables used in TCG mode */ if (tcg_enabled() && !inited) { inited = 1; optimize_flags_init(); #ifndef CONFIG_USER_ONLY prev_debug_excp_handler = cpu_set_debug_excp_handler(breakpoint_handler); #endif } if (cpu_x86_register(env, cpu_model) < 0) { cpu_x86_close(env); return NULL; } env->cpuid_apic_id = env->cpu_index; mce_init(env); qemu_init_vcpu(env); return env; }
void qemu_init_vcpu(void *_env) { CPUState *env = _env; env->nr_cores = smp_cores; env->nr_threads = smp_threads; env->stopped = 1; if (kvm_enabled()) { qemu_kvm_start_vcpu(env); } else { qemu_tcg_init_vcpu(env); } }
static void qemu_tcg_init_vcpu(void *_env) { CPUState *env = _env; /* share a single thread for all cpus with TCG */ if (!tcg_cpu_thread) { env->thread = g_malloc0(sizeof(QemuThread)); env->halt_cond = g_malloc0(sizeof(QemuCond)); qemu_cond_init(env->halt_cond); tcg_halt_cond = env->halt_cond; qemu_thread_create(env->thread, qemu_tcg_cpu_thread_fn, env, QEMU_THREAD_JOINABLE); #ifdef _WIN32 env->hThread = qemu_thread_get_handle(env->thread); #endif while (env->created == 0) { qemu_cond_wait(&qemu_cpu_cond, &qemu_global_mutex); } tcg_cpu_thread = env->thread; } else { env->thread = tcg_cpu_thread; env->halt_cond = tcg_halt_cond; } }
static void *qemu_tcg_cpu_thread_fn(void *arg) { CPUState *env = arg; qemu_tcg_init_cpu_signals(); qemu_thread_get_self(env->thread); /* signal CPU creation */ qemu_mutex_lock(&qemu_global_mutex); for (env = first_cpu; env != NULL; env = env->next_cpu) { env->thread_id = qemu_get_thread_id(); env->created = 1; } qemu_cond_signal(&qemu_cpu_cond); /* wait for initial kick-off after machine start */ while (first_cpu->stopped) { qemu_cond_wait(tcg_halt_cond, &qemu_global_mutex); } while (1) { tcg_exec_all(); if (use_icount && qemu_clock_deadline(vm_clock) <= 0) { qemu_notify_event(); } qemu_tcg_wait_io_event(); } return NULL; }
static void tcg_exec_all(void) { int r; /* Account partial waits to the vm_clock. */ qemu_clock_warp(vm_clock); if (next_cpu == NULL) { next_cpu = first_cpu; } for (; next_cpu != NULL && !exit_request; next_cpu = next_cpu->next_cpu) { CPUState *env = next_cpu; qemu_clock_enable(vm_clock, (env->singlestep_enabled & SSTEP_NOTIMER) == 0); if (cpu_can_run(env)) { r = tcg_cpu_exec(env); if (r == EXCP_DEBUG) { cpu_handle_guest_debug(env); break; } } else if (env->stop || env->stopped) { break; } } exit_request = 0; }
目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 posix-aio-compat.c worker thread 去處理。
cpu_exec_init_all(); /* open the virtual block devices */ if (snapshot) qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot, NULL, 0); if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func, &machine->use_scsi, 1) != 0) exit(1); qemu_init_cpu_loop(); // qemu_init_main_loop 呼叫 main_loop_init (main-loop.c) if (qemu_init_main_loop()) { fprintf(stderr, "qemu_init_main_loop failed\n"); exit(1); }
void qemu_init_cpu_loop(void) { qemu_init_sigbus(); qemu_cond_init(&qemu_cpu_cond); qemu_cond_init(&qemu_pause_cond); qemu_cond_init(&qemu_work_cond); qemu_cond_init(&qemu_io_proceeded_cond); qemu_mutex_init(&qemu_global_mutex); qemu_thread_get_self(&io_thread); }
int main_loop_init(void) { int ret; qemu_mutex_lock_iothread(); ret = qemu_signal_init(); if (ret) { return ret; } /* Note eventfd must be drained before signalfd handlers run */ ret = qemu_event_init(); if (ret) { return ret; } return 0; }
static void main_loop(void) { bool nonblocking; int last_io = 0; do { nonblocking = !kvm_enabled() && last_io > 0; last_io = main_loop_wait(nonblocking); } while (!main_loop_should_exit()); }
int main_loop_wait(int nonblocking) { fd_set rfds, wfds, xfds; int ret, nfds; struct timeval tv; int timeout; if (nonblocking) { timeout = 0; } else { timeout = qemu_calculate_timeout(); qemu_bh_update_timeout(&timeout); } os_host_main_loop_wait(&timeout); tv.tv_sec = timeout / 1000; tv.tv_usec = (timeout % 1000) * 1000; /* poll any events */ /* XXX: separate device handlers from system ones */ nfds = -1; FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&xfds); #ifdef CONFIG_SLIRP slirp_select_fill(&nfds, &rfds, &wfds, &xfds); #endif qemu_iohandler_fill(&nfds, &rfds, &wfds, &xfds); glib_select_fill(&nfds, &rfds, &wfds, &xfds, &tv); if (timeout > 0) { qemu_mutex_unlock_iothread(); } ret = select(nfds + 1, &rfds, &wfds, &xfds, &tv); if (timeout > 0) { qemu_mutex_lock_iothread(); } glib_select_poll(&rfds, &wfds, &xfds, (ret < 0)); qemu_iohandler_poll(&rfds, &wfds, &xfds, ret); #ifdef CONFIG_SLIRP slirp_select_poll(&rfds, &wfds, &xfds, (ret < 0)); #endif qemu_run_all_timers(); /* Check bottom-halves last in case any of the earlier events triggered them. */ qemu_bh_poll(); return ret; }
main (vl.c) → qemu_opts_foreach (qemu-option.c) → qemu_aio_wait (aio.c) → qemu_bh_poll (async.c) → spawn_thread_bh_fn (posix-aio-compat.c) → do_spawn_thread (posix-aio-compat.c)
aio_thread (posix-aio-compat.c) → cond_timedwait (posix-aio-compat.c)
底下腳本可以觀察 QEMU 本身。
$ vi command.gdb set breakpoint pending on file qemu handle SIGUSR2 noprint nostop break main_loop run linux-0.2.img -vnc 0.0.0.0:1 $ gdb -x command.gdb
虛擬機重啟 (reboot) 的時候,會重置 virtual cpu 的 reset vector,這樣 virtual cpu 才會跳至開機預設的位址執行。請在 cpu_reset 下斷點,並 reboot 虛擬機 1)。
(gdb) bt #0 cpu_reset (env=0x1251290) at /nfs_home/chenwj/work/svn/qemu-1.0/target-i386/helper.c:37 #1 0x0000000000638753 in pc_cpu_reset (opaque=0x1251290) at /nfs_home/chenwj/work/svn/qemu-1.0/hw/pc.c:928 #2 0x00000000004fe916 in qemu_system_reset (report=true) at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1381 #3 0x00000000004feb71 in main_loop_should_exit () at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1452 #4 0x00000000004fec48 in main_loop () at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:1485 #5 0x0000000000503864 in main (argc=4, argv=0x7fffffffe218, envp=0x7fffffffe240) at /nfs_home/chenwj/work/svn/qemu-1.0/vl.c:3485 (gdb)
void qemu_system_reset_request(void) { if (no_reboot) { shutdown_requested = 1; } else { reset_requested = 1; } cpu_stop_current(); qemu_notify_event(); }
static void main_loop(void) { bool nonblocking; int last_io = 0; do { nonblocking = !kvm_enabled() && last_io > 0; last_io = main_loop_wait(nonblocking); } while (!main_loop_should_exit()); }
static bool main_loop_should_exit(void) { RunState r; if (qemu_debug_requested()) { vm_stop(RUN_STATE_DEBUG); } if (qemu_shutdown_requested()) { qemu_kill_report(); monitor_protocol_event(QEVENT_SHUTDOWN, NULL); if (no_shutdown) { vm_stop(RUN_STATE_SHUTDOWN); } else { return true; } } if (qemu_reset_requested()) { // 返回 reset_requested pause_all_vcpus(); cpu_synchronize_all_states(); qemu_system_reset(VMRESET_REPORT); // 重啟系統 resume_all_vcpus(); if (runstate_check(RUN_STATE_INTERNAL_ERROR) || runstate_check(RUN_STATE_SHUTDOWN)) { runstate_set(RUN_STATE_PAUSED); } } if (qemu_powerdown_requested()) { monitor_protocol_event(QEVENT_POWERDOWN, NULL); qemu_irq_raise(qemu_system_powerdown); } if (qemu_vmstop_requested(&r)) { vm_stop(r); } return false; }
void qemu_system_reset(bool report) { QEMUResetEntry *re, *nre; /* reset all devices */ // 從 reset_handlers 抓出 device 重啟。之前就會用 qemu_register_reset 註冊各個裝置的 reset 回掉函式。 QTAILQ_FOREACH_SAFE(re, &reset_handlers, entry, nre) { re->func(re->opaque); } if (report) { monitor_protocol_event(QEVENT_RESET, NULL); } cpu_synchronize_all_post_reset(); }
static void pc_cpu_reset(void *opaque) { CPUState *env = opaque; cpu_reset(env); env->halted = !cpu_is_bsp(env); }
void cpu_reset(CPUX86State *env) { int i; if (qemu_loglevel_mask(CPU_LOG_RESET)) { qemu_log("CPU Reset (CPU %d)\n", env->cpu_index); log_cpu_state(env, X86_DUMP_FPU | X86_DUMP_CCOP); } memset(env, 0, offsetof(CPUX86State, breakpoints)); tlb_flush(env, 1); env->old_exception = -1; /* init to reset state */ #ifdef CONFIG_SOFTMMU env->hflags |= HF_SOFTMMU_MASK; #endif env->hflags2 |= HF2_GIF_MASK; cpu_x86_update_cr0(env, 0x60000010); env->a20_mask = ~0x0; env->smbase = 0x30000; env->idt.limit = 0xffff; env->gdt.limit = 0xffff; env->ldt.limit = 0xffff; env->ldt.flags = DESC_P_MASK | (2 << DESC_TYPE_SHIFT); env->tr.limit = 0xffff; env->tr.flags = DESC_P_MASK | (11 << DESC_TYPE_SHIFT); cpu_x86_load_seg_cache(env, R_CS, 0xf000, 0xffff0000, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_CS_MASK | DESC_R_MASK | DESC_A_MASK); cpu_x86_load_seg_cache(env, R_DS, 0, 0, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_W_MASK | DESC_A_MASK); cpu_x86_load_seg_cache(env, R_ES, 0, 0, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_W_MASK | DESC_A_MASK); cpu_x86_load_seg_cache(env, R_SS, 0, 0, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_W_MASK | DESC_A_MASK); cpu_x86_load_seg_cache(env, R_FS, 0, 0, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_W_MASK | DESC_A_MASK); cpu_x86_load_seg_cache(env, R_GS, 0, 0, 0xffff, DESC_P_MASK | DESC_S_MASK | DESC_W_MASK | DESC_A_MASK); env->eip = 0xfff0; env->regs[R_EDX] = env->cpuid_version; env->eflags = 0x2; /* FPU init */ for(i = 0;i < 8; i++) env->fptags[i] = 1; env->fpuc = 0x37f; env->mxcsr = 0x1f80; env->pat = 0x0007040600070406ULL; env->msr_ia32_misc_enable = MSR_IA32_MISC_ENABLE_DEFAULT; memset(env->dr, 0, sizeof(env->dr)); env->dr[6] = DR6_FIXED_1; env->dr[7] = DR7_FIXED_1; cpu_breakpoint_remove_all(env, BP_CPU); cpu_watchpoint_remove_all(env, BP_CPU); }
guest virtual addr (GVA) → guest physical addr (GPA) → host virtual addr (HVA)
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;
static TranslationBlock *tb_find_slow(target_ulong pc, ...) { /* find translated block using physical mappings */ 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) { /* check next page if needed */ 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: /* if no translated code available, then translate it now */ tb = tb_gen_code(env, pc, cs_base, flags, 0); found: /* we add the TB in the virtual pc hash table */ env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb; return tb; }
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); }
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; }
/* handle all cases except unaligned access which span two pages */ DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr, int mmu_idx) { // 先查找 TLB redo: // 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) { /* IO access */ // iotlb 緩存 IO 模擬函式 ioaddr = env->iotlb[mmu_idx][index]; } else if (((addr & ~TARGET_PAGE_MASK) + DATA_SIZE - 1) >= TARGET_PAGE_SIZE) { do_unaligned_access: /* slow unaligned access (it spans two pages) */ // 這裡會呼叫 slow_ldb_cmmu 做跨頁存取。 } else { /* unaligned/aligned access in the same page */ addend = env->tlb_table[mmu_idx][index].addend; res = glue(glue(ld, USUFFIX), _raw)((uint8_t *)(long)(addr+addend)); } } else { /* the page is not in the TLB : fill it */ // GETPC 包裝 __builtin_return_address,請見 http://gcc.gnu.org/onlinedocs/gcc/Return-Address.html。 // 其用途是取得此函式的 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; }
#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?。
/* try to fill the TLB and return an exception if error. If retaddr is NULL, it means that the function was called in C code (i.e. not from generated code or from helper.c) */ 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 被呼叫。 /* now we have a real cpu fault */ pc = (unsigned long)retaddr; tb = tb_find_pc(pc); if (tb) { /* the PC is inside the translated code. It means that we have a virtual CPU fault */ 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; }
uint8_t __ldb_mmu(target_ulong addr, int mmu_idx) { retaddr = ((void *)((unsigned long)__builtin_return_address(0) - 1)); tlb_fill(addr, 0, mmu_idx, retaddr); goto redo; }
/* return value: -1 = cannot handle fault 0 = nothing more to do // 頁面已在內存,填入適當 TLB 項目即可。 1 = generate PF fault // 頁面不在內存,產生頁缺失。 */ int cpu_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; }
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?。共有底下檔案:
底下檔案定義相關函式原型。
底下檔案生成相關函式體。
#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 }; /* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and EAX. It will be useful once fixed registers globals are less common. */ 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]); // 呼叫上述函式。 /* 略 */ }
// #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 /* !defined(CONFIG_USER_ONLY) */ #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.i uint8_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_t ldub_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); /* 略 */ }
#include softmmu_exec.h
,softmmu_exec.h 再利用 softmmu_header.h 生成 {ld,st}*_{kernel,user,etc} 函式。#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.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); // softmmu_template.h DATA_TYPE REGPARM glue(glue(__ld, SUFFIX), MMUSUFFIX)(target_ulong addr, int mmu_idx) { } // softmmu_header.h static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr) { /* 略 */ res = glue(glue(__ld, SUFFIX), MMUSUFFIX)(addr, mmu_idx); /* 略 */ }
#if !defined(CONFIG_USER_ONLY) #include "softmmu_exec.h" #endif /* !defined(CONFIG_USER_ONLY) */ 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; }
// 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) { }
#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))]
以 x86 為例,有幾種情況會呼叫 tlb_flush。
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)。Control registervoid tlb_flush(CPUState *env, int flush_global) { int i; /* must reset current TB so that interrupts cannot modify the links while we are modifying them */ 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++; }
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) { /* check next page if needed */ 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; } }
可參考底下文件,
int main(int argc, char **argv, char **envp) { // 初始化 TCG cpu_exec_init_all(0); // 初始化 CPUState env = cpu_init(cpu_model); // 傳遞環境變數和命令行參數給 guest program target_environ = envlist_to_environ(envlist, NULL); // 初始化 TaskState 数据结构 init_task_state(ts); /* build Task State */ 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; } /* flags setup : we activate the IRQs by default as in user mode */ env->eflags |= IF_MASK; /* linux register setup */ #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 // 設置中斷 /* linux interrupt setup */ #ifndef TARGET_ABI32 env->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); }
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。 }
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)); } }
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; }
// ret = loader_exec(filename, target_argv, target_environ, regs, // info, &bprm); int loader_exec(const char * filename, char ** argv, char ** envp, struct target_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) { /* success. Initialize important registers */ // 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; } }
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); } }
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; }
/* Construct the envp and argv tables on the target stack. */ 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) { /* FIXME - handle put_user() failures */ put_user_ual(stringp, argv); argv += n; stringp += target_strlen(stringp) + 1; } // 將環境變數寫至 guest 棧上。 while (envc-- > 0) { /* FIXME - handle put_user() failures */ put_user_ual(stringp, envp); envp += n; stringp += target_strlen(stringp) + 1; } }
// linux-user/syscall_defs.h 定義 TARGET_XXX static uint8_t host_to_target_signal_table[_NSIG] = { [SIGHUP] = TARGET_SIGHUP, } static uint8_t target_to_host_signal_table[_NSIG]; void signal_init(void) { /* generate signal conversion tables */ 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; } /* set all host signal handlers. ALL signals are blocked during the handlers to serialize them. */ 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 there's already a handler installed then something has gone horribly wrong, so don't even try to handle that case. */ /* Install some handlers for our own use. We need at least SIGSEGV and SIGBUS, to detect exceptions. We can not just trap all signals because it affects syscall interrupt behavior. But do trap all default-fatal signals. */ if (fatal_signal (i)) sigaction(host_sig, &act, NULL); } }
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); }
void cpu_loop(CPUX86State *env) { int trapnr; abi_ulong pc; target_siginfo_t info; for(;;) { trapnr = cpu_x86_exec(env); switch(trapnr) { case 0x80: /* linux syscall from int $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; /* different cases */ default: pc = env->segs[R_CS].base + env->eip; fprintf(stderr, "qemu: 0x%08lx: 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。
void cpu_loop(CPUX86State *env) { int trapnr; abi_ulong pc; target_siginfo_t info; for(;;) { trapnr = cpu_x86_exec(env); switch(trapnr) { case 0x80: /* linux syscall from int $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; ... 略 ... } ... 略 ... }
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; ... 略 ... } ... 略 ... }
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 為例,
#define EXCP_INTERRUPT 0x10000 /* async interruption */ #define EXCP_HLT 0x10001 /* hlt instruction reached */ #define EXCP_DEBUG 0x10002 /* cpu stopped after a breakpoint or singlestep */ #define EXCP_HALTED 0x10003 /* cpu is halted (waiting for external event) */
#define QEMU_NORETURN __attribute__ ((__noreturn__))
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; // 進行翻譯並執行的迴圈。 /* prepare setjmp context for exception handling */ for(;;) { if (setjmp(env->jmp_env) == 0) { // 正常流程。 next_tb = 0; /* force lookup of first TB */ for(;;) { } /* for(;;) */ } else { /* Reload env after longjmp - the compiler may have smashed all * local variables as longjmp is marked 'noreturn'. */ env = cpu_single_env; } } /* for(;;) */ }
for(;;) { if (setjmp(env->jmp_env) == 0) { /* if an exception is pending, we execute it here */ if (env->exception_index >= 0) { // exception_index 非 -1 代表有事要處理。 if (env->exception_index >= EXCP_INTERRUPT) { // 來自 cpu_exec 以外的例外。 /* exit request from the cpu execution loop */ ret = env->exception_index; if (ret == EXCP_DEBUG) { cpu_handle_debug_exception(env); } break; } else { #if defined(CONFIG_USER_ONLY) /* if user mode only, we simulate a fake exception which will be handled outside the cpu execution loop */ #if defined(TARGET_I386) do_interrupt(env); #endif ret = env->exception_index; break; #else } } } else { /* Reload env after longjmp - the compiler may have smashed all * local variables as longjmp is marked 'noreturn'. */ env = cpu_single_env; } } /* for(;;) */ /* 回復 env 中的欄位,清空 cpu_single_env,返回 cpu_loop 處理例外 */ }
void cpu_loop_exit(CPUState *env) { env->current_tb = NULL; longjmp(env->jmp_env, 1); }
/* exit the current TB from a signal handler. The host registers are restored in a state compatible with the CPU emulator */ #if defined(CONFIG_SOFTMMU) void cpu_resume_from_signal(CPUState *env, void *puc) { /* XXX: restore cpu registers saved in host registers */ env->exception_index = -1; longjmp(env->jmp_env, 1); } #endif
void do_interrupt(CPUState *env1) { CPUState *saved_env; saved_env = env; env = env1; #if defined(CONFIG_USER_ONLY) /* if user mode only, we simulate a fake exception which will be handled outside the cpu execution loop */ do_interrupt_user(env->exception_index, env->exception_is_int, env->error_code, env->exception_next_eip); /* successfully delivered */ env->old_exception = -1; #else /* simulate a real cpu exception. On i386, it can trigger new exceptions, but we do not handle double or triple faults yet. */ do_interrupt_all(env->exception_index, env->exception_is_int, env->error_code, env->exception_next_eip, 0); /* successfully delivered */ env->old_exception = -1; #endif env = saved_env; }
#if defined(CONFIG_USER_ONLY) /* fake user mode interrupt */ static void do_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; /* check privilege if software int */ if (is_int && dpl < cpl) raise_exception_err(EXCP0D_GPF, (intno << shift) + 2); // target-i386/cpu.h /* Since we emulate only user space, we cannot do more than exiting the emulation with the suitable exception and error code */ if (is_int) EIP = next_eip; // target-i386/cpu.h:#define EIP (env->eip) } #else
static void QEMU_NORETURN raise_exception_err(int exception_index, int error_code) { raise_interrupt(exception_index, 0, error_code, 0); }
/* * Signal an interruption. It is executed in the main CPU loop. * is_int is TRUE if coming from the int instruction. next_eip is the * EIP value AFTER the interrupt instruction. It is only relevant if * is_int is TRUE. */ 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); }
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, unsigned int 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; // 將用戶態資訊放至內核棧。 /* XXX: use SS segment size ? */ PUSHW(ssp, esp, 0xffff, compute_eflags()); PUSHW(ssp, esp, 0xffff, old_cs); PUSHW(ssp, esp, 0xffff, old_eip); // 將 env->eip 指向中斷向量。返回 cpu_exec 後便會翻譯中斷向量並執行。 /* update processor state */ 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) { /* 32 bits */ 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 { /* 16 bits */ 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; else eflags_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; /* force lookup of first TB */ 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; /* reset soft MMU for next block (it can currently only be set by a memory fault) */ } /* for(;;) */
Precise exception 要求當某一條 guest 指令發生例外,例如溢位或是頁缺失,此條指令之前的運算必須完成,此條指令之後的運算必須捨棄。當 QEMU 在 code cache 中執行翻譯過後的指令,其中發生例外時,QEMU 必須要能維護例外發生之前 guest 的暫存器和內存內容,並能反查出發生例外的 basic block 相對映開頭的 guest pc。從那個 guest pc 重新開始翻譯直到發生例外的位址,並把這個 guest pc 存回 CPUState,這是因為 QEMU 不會執行完一條 guest 指令就更新 guest pc。
QEMU 會用到底下定義在 translate-all.c 資料結構:
target_ulong gen_opc_pc[OPC_BUF_SIZE]; // 紀錄 guest pc。 uint16_t gen_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 判斷在哪裡發生例外,這用到 GCC Getting the Return or Frame Address of a Function 擴展。注意! QEMU 在兩個地方定義 GETPC,分別位於 exec-all.h 和 exec.c。
// __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
#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); }
#if !defined(CONFIG_USER_ONLY) /* try to fill the TLB and return an exception if error. If retaddr is NULL, it means that the function was called in C code (i.e. not from generated code or from helper.c) */ /* XXX: fix it to restore all registers */ 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) { /* now we have a real cpu fault */ pc = (unsigned long)retaddr; tb = tb_find_pc(pc); if (tb) { /* the PC is inside the translated code. It means that we have a virtual CPU fault */ cpu_restore_state(tb, env, pc); } } raise_exception_err(env->exception_index, env->error_code); } env = saved_env; } #endif
/* find the TB 'tb' such that tb[0].tc_ptr <= tc_ptr < tb[1].tc_ptr. Return NULL if not found */ 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 >= (unsigned long)code_gen_ptr) return NULL; /* binary search (cf Knuth) */ 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。 }
/* The cpu state corresponding to 'searched_pc' is restored. */ int cpu_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) { /* Reset the cycle counter to the start of the block. */ env->icount_decr.u16.low += tb->icount; /* Clear the IO flag. */ env->can_do_io = 0; } /* find opc index corresponding to search_pc */ // 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; /* now find start of instruction before */ 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; }
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) { lj++; while (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; /* we don't forget to fill the last values */ if (search_pc) { j = gen_opc_ptr - gen_opc_buf; lj++; while (lj <= j) gen_opc_instr_start[lj++] = 0; } }
/* Return the index of the micro operation such as the pc after is < offset bytes from the start of the TB. The contents of gen_code_buf must not be changed, though writing the same values is ok. Return -1 if not found. */ static inline int tcg_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; case INDEX_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++; } }
void restore_state_to_opc(CPUState *env, TranslationBlock *tb, int pc_pos) { int cc_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; }
static void QEMU_NORETURN raise_exception_err(int exception_index, int error_code) { raise_interrupt(exception_index, 0, error_code, 0); }
/* * Signal an interruption. It is executed in the main CPU loop. * is_int is TRUE if coming from the int instruction. next_eip is the * EIP value AFTER the interrupt instruction. It is only relevant if * is_int is TRUE. */ 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); }
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:7731 7731 { (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:7731 7731 { (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,%esp 0x0000e4c3: mov 0x8(%esp),%eax 0x0000e4c7: mov %al,(%esp) 0x0000e4ca: movzbl (%esp),%eax 0x0000e4ce: mov 0xc(%esp),%edx 0x0000e4d2: mov %al,%fs:(%edx) <--- 0x0000e4d5: add $0x4,%esp 0x0000e4d8: 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: 0000e4c0 0x0007: 0000e4c3 0x000d: 0000e4c7 0x0011: 0000e4ca 0x0015: 0000e4ce 0x001b: 0000e4d2 // spc 是觸發例外的 host binary 位址,等同 retaddr。 // eip 是觸發例外的 guest binary 位址。 spc=0x401255bf pc_pos=0x1b eip=0000e4d2 cs_base=0 Servicing hardware INT=0x20
0x40125577: mov %eax,%ebp // 保存 CPUState 0x40125579: mov %ebp,%ebx 0x4012557b: mov 0x84(%r14),%r12d 0x40125582: add %r12d,%ebx 0x40125585: mov (%r14),%r12d 0x40125588: mov %ebp,0x8(%r14) 0x4012558c: mov %ebx,%esi // 查詢 TLB 0x4012558e: mov %ebx,%edi 0x40125590: shr $0x7,%esi 0x40125593: and $0xfffff000,%edi 0x40125599: and $0x1fe0,%esi 0x4012559f: lea 0x34c(%r14,%rsi,1),%rsi 0x401255a7: cmp (%rsi),%edi 0x401255a9: mov %ebx,%edi 0x401255ab: jne 0x401255b6 // TLB 不命中,跳至 0x401255b6。 0x401255ad: add 0xc(%rsi),%rdi 0x401255b1: mov %r12b,(%rdi) 0x401255b4: jmp 0x401255c0 0x401255b6: mov %r12d,%esi // 不命中,呼叫 __stb_mmu 0x401255b9: xor %edx,%edx 0x401255bb: callq 0x54d38a // 執行 __stb_mmu 的時候,發生頁缺失例外。 0x401255c0: mov 0x10(%r14),%ebp
數個 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:7731 7731 { Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe76232c0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c:7731 7731 {
請閱讀以下文章:
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。8)
底下是 TCG 動態翻譯流程中會使用到的緩衝區:
uint16_t gen_opc_buf[OPC_BUF_SIZE]; // 放置 TCG opcode。 TCGArg gen_opparam_buf[OPPARAM_BUF_SIZE]; // 放置 TCG opcode 會用到的 operand。
#ifdef USE_STATIC_CODE_GEN_BUFFER static uint8_t static_code_gen_buffer[DEFAULT_CODE_GEN_BUFFER_SIZE] __attribute__((aligned (CODE_GEN_ALIGN))); #endif
// 根據不同平台,code_gen_section 用不同的 __attribute__ 修飾 code_gen_prologue。 uint8_t code_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)
void tcg_prologue_init(TCGContext *s) { /* init global prologue and epilogue */ 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 }
next_tb = 0; /* force lookup of first TB */ 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 = env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];
如果失敗,則呼叫 tb_find_slow 以 pc (cs + eip) 對映的物理位址尋找 TB。如果成功,則將該 TB 寫入 tb_jmp_cache; 若否,則進行翻譯。
not_found: /* if no translated code available, then translate it now */ tb = tb_gen_code(env, pc, cs_base, flags, 0); found: /* we add the TB in the virtual pc hash table */ env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)] = tb; return tb;
// 注意! 這裡會將虛擬位址轉成物理位址。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;
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
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; }
tcg_reg_alloc_start(s); s->code_buf = gen_code_buf; s->code_ptr = gen_code_buf; // host binary 會寫入 TCGContext s 的 code_ptr 所指向的緩衝區。
/* add in the physical hash table */ 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 的開頭。 /* add in the page list */ 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);
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); }
next_tb = tcg_qemu_tb_exec(tc_ptr);
// 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。
針對 self-modifying code 或是 JIT,在內存生成代碼之後通常需要清空快取9)。
請閱讀一下文章
為了盡量在 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) { /* as some TB could have been invalidated because of memory exceptions while generating the code, we must recompute the hash index here */ 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);
// block chaining 方向為: tb -> tb_next。n 用來指示 tb 條件分支的方向。 static inline void tb_add_jump(TranslationBlock *tb, int n, TranslationBlock *tb_next) { // jmp_next[0]/jmp_next[1] 代表 tb 條件分支的目標。 if (!tb->jmp_next[n]) { /* patch the native jump address */ 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)); } }
static inline void tb_set_jmp_target(TranslationBlock *tb, int n, unsigned long addr) { 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); }
#elif defined(__i386__) || defined(__x86_64__) static inline void tb_set_jmp_target1(unsigned long jmp_addr, unsigned long addr) { /* patch the branch destination */ *(uint32_t *)jmp_addr = addr - (jmp_addr + 4); // jmp 的參數為 jmp 下一條指令與目標地址的偏移量。 /* no need to flush icache explicitly */ }
由 guest binary → TCG IR 的過程中,gen_goto_tb 會做 block chaining 的準備。請見 Porting QEMU to Plan 9: QEMU Internals and Port Strategy 2.2.3 和 2.2.4 節。以 i386 為例: (target-i386/translate.c):
// tb_num 代表目前 tb block linking 分支情況。eip 代表跳轉目標。 static inline void gen_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 /* NOTE: we handle the case where the TB spans two pages here */ // 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)) { /* jump to same page: we can use a direct jump */ // 如果 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 { /* jump to another page: currently not optimized */ gen_jmp_im(eip); gen_eob(s); } }
static inline void tcg_gen_goto_tb(int idx) { tcg_gen_op1i(INDEX_op_goto_tb, idx); }
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) { /* direct jump method */ tcg_out8(s, OPC_JMP_long); /* jmp im */ // 紀錄將來要 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 { /* indirect jump method */ 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;
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 void gen_jmp(DisasContext *s, target_ulong eip) { // 注意! DisasContext 的 pc 值是 eip + cs_base。 // 直接跳轉至 eip。 gen_jmp_tb(s, eip, 0); }
請見,
當 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 /* NOTE: we handle the case where the TB spans two pages here */ if ((pc & TARGET_PAGE_MASK) == (tb->pc & TARGET_PAGE_MASK) || (pc & TARGET_PAGE_MASK) == ((s->pc - 1) & TARGET_PAGE_MASK)) { /* jump to same page: we can use a direct jump */ #endif tcg_gen_goto_tb(tb_num); gen_jmp_im(eip); tcg_gen_exit_tb((long)tb + tb_num); #if 0 } else { /* jump to another page: currently not optimized */ 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
底下幾種情況會做 block unchaining。請見以下討論,
void cpu_exit(CPUState *env) { env->exit_request = 1; // 拉起 env->exit_request。 cpu_unlink_tb(env); // 將 env 的 code cache 中的 tb unlink。 }
#ifdef CONFIG_IOTHREAD static void cpu_signal(int sig) { if (cpu_single_env) { // 某些情況下會把 env 備份到 cpu_single_env cpu_exit(cpu_single_env); } exit_request = 1; // 拉起 exit_request,之後會在 cpu_exec (cpu-exec.c) 中把 env->exit_request 拉起。 } #endif
if (unlikely(env->exit_request)) { env->exit_request = 0; env->exception_index = EXCP_INTERRUPT; // 設置 exception_index。longjmp 回 cpu_exec 開頭後會檢查 exception_index。 cpu_loop_exit(); // 將 env->current_tb 設為 NULL,longjmp 回 cpu_exec 開頭。 }
void cpu_interrupt(CPUState *env, int mask) { // 設置 interrupt_request。cpu_exec 會在內層迴圈處理 interrupt_request。 // 那時會設置 env->exception_index,並呼叫 cpu_loop_exit,longjmp 回 cpu_exec 開頭。 env->interrupt_request |= mask; cpu_unlink_tb(env); }
static void cpu_unlink_tb(CPUState *env) { /* FIXME: TB unchaining isn't SMP safe. For now just ignore the problem and hope the cpu will stop of its own accord. For userspace emulation this often isn't actually as bad as it sounds. Often signals are used primarily to interrupt blocking syscalls. */ TranslationBlock *tb; static spinlock_t interrupt_lock = SPIN_LOCK_UNLOCKED; spin_lock(&interrupt_lock); tb = env->current_tb; // current_tb 代表 env 目前正在執行的 tb /* if the cpu is currently executing code, we must unlink it and all the potentially executing TB */ if (tb) { env->current_tb = NULL; tb_reset_jump_recursive(tb); // 將 tb 的 block chaining 打斷 } spin_unlock(&interrupt_lock); }
static void tb_reset_jump_recursive(TranslationBlock *tb) { tb_reset_jump_recursive2(tb, 0); tb_reset_jump_recursive2(tb, 1); }
static inline void tb_reset_jump_recursive2(TranslationBlock *tb, int n) { TranslationBlock *tb1, *tb_next, **ptb; unsigned int n1; tb1 = tb->jmp_next[n]; // tb -> tb1。但有可能有其它 TB,tb2, 跳至 tb1。此時,tb->jmp_next 其值為 tb2。 if (tb1 != NULL) { /* find head of list */ for(;;) { n1 = (long)tb1 & 3; tb1 = (TranslationBlock *)((long)tb1 & ~3); if (n1 == 2) // 代表 tb 是唯一跳至 tb1 的 TB。 break; tb1 = tb1->jmp_next[n1]; // 代表有其它跳至 tb1 的 TB。繼續尋訪該串列。 } /* we are now sure now that tb jumps to tb1 */ tb_next = tb1; // 確定是 tb -> tb1。 /* remove tb from the jmp_first list */ ptb = &tb_next->jmp_first; // jmp_first 指向跳至 tb_next 的所有 TB。 for(;;) { tb1 = *ptb; n1 = (long)tb1 & 3; tb1 = (TranslationBlock *)((long)tb1 & ~3); if (n1 == n && tb1 == tb) // 在 jmp_first -> jmp_next 構成的串列中找到 tb break; ptb = &tb1->jmp_next[n1]; // 繼續在 jmp_first -> jmp_next 構成的串列中找尋 tb } *ptb = tb->jmp_next[n]; // 將 tb_next 的 jmp_first 的串列改以下一個 TB 為開頭 tb->jmp_next[n] = NULL; /* suppress the jump to next tb in generated code */ tb_reset_jump(tb, n); /* suppress jumps in the tb on which we could have jumped */ tb_reset_jump_recursive(tb_next); } }
/* reset the jump entry 'n' of a TB so that it is not chained to another TB */ static inline void tb_reset_jump(TranslationBlock *tb, int n) { tb_set_jmp_target(tb, n, (unsigned long)(tb->tc_ptr + tb->tb_next_offset[n])); }
void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr) { // 將該 tb 從 tb_phys_hash 中移除 phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣 h = tb_phys_hash_func(phys_pc); tb_remove(&tb_phys_hash[h], tb, offsetof(TranslationBlock, phys_hash_next)); // 將 tb 從相應的 PageDesc 中移除 if (tb->page_addr[0] != page_addr) { p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) { p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } tb_invalidated_flag = 1; // 將 tb 從 tb_jmp_cache 移除 h = tb_jmp_cache_hash_func(tb->pc); // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。 for(env = first_cpu; env != NULL; env = env->next_cpu) { if (env->tb_jmp_cache[h] == tb) env->tb_jmp_cache[h] = NULL; } // 處理 tb1 (tb -> tb1) tb_jmp_remove(tb, 0); tb_jmp_remove(tb, 1); // 處理 tb1 (tb1 -> tb) tb1 = tb->jmp_first; for(;;) { n1 = (long)tb1 & 3; if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb break; tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1 tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache) tb1->jmp_next[n1] = NULL; tb1 = tb2; } tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己 }
static inline void tb_jmp_remove(TranslationBlock *tb, int n) { ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向 tb1 = *ptb; // 處理 tb1 (tb -> tb1) if (tb1) { /* find tb(n) in circular list */ for(;;) { tb1 = *ptb; n1 = (long)tb1 & 3; // 取出 tb1 末兩位 tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1 if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb break; if (n1 == 2) { ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1 } else { ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) } } /* now we can suppress tb(n) from the list */ *ptb = tb->jmp_next[n]; tb->jmp_next[n] = NULL; } }
以 Linux 上的 self modifying code 為例 (請見 Self-modifying code),主要是利用 mprotect 修改頁面權限,修改其中代碼。QEMU 從兩個方向偵測 self-modifying code。
/* Modify the flags of a page and invalidate the code if necessary. The flag PAGE_WRITE_ORG is positioned automatically depending on PAGE_WRITE. The mmap_lock should already be held. */ void page_set_flags(target_ulong start, target_ulong end, int flags) { for (addr = start, len = end - start; len != 0; len -= TARGET_PAGE_SIZE, addr += TARGET_PAGE_SIZE) { // 反查該 guest pc 對映的頁面。 PageDesc *p = page_find_alloc(addr >> TARGET_PAGE_BITS, 1); /* If the write protection bit is set, then we invalidate the code inside. */ if (!(p->flags & PAGE_WRITE) && (flags & PAGE_WRITE) && p->first_tb) { tb_invalidate_phys_page(addr, 0, NULL); } p->flags = flags; } }
static inline void tb_invalidate_phys_page_fast(tb_page_addr_t start, int len) { PageDesc *p; int offset, b; p = page_find(start >> TARGET_PAGE_BITS); if (!p) return; if (p->code_bitmap) { offset = start & ~TARGET_PAGE_MASK; b = p->code_bitmap[offset >> 3] >> (offset & 7); if (b & ((1 << len) - 1)) goto do_invalidate; } else { do_invalidate: tb_invalidate_phys_page_range(start, start + len, 1); } }
#if !defined(CONFIG_SOFTMMU) static void tb_invalidate_phys_page(tb_page_addr_t addr, unsigned long pc, void *puc) { addr &= TARGET_PAGE_MASK; p = page_find(addr >> TARGET_PAGE_BITS); // 取得該 page 的第一個 tb。 // tb 末兩位如果是 01 (1),代表 tb 對應的 guest bianry 跨 page。 tb = p->first_tb; while (tb != NULL) { n = (long)tb & 3; // 取得 block chaing 的方向 tb = (TranslationBlock *)((long)tb & ~3); // 去掉末兩位的編碼,還原回真正的 tb tb_phys_invalidate(tb, addr); tb = tb->page_next[n]; // 取得 tb 所屬 page (或下一個 page) 的下一個 tb } p->first_tb = NULL; }
void tb_phys_invalidate(TranslationBlock *tb, tb_page_addr_t page_addr) { // 將該 tb 從 tb_phys_hash 中移除 phys_pc = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); // virtual addr 中 page offset 的部分和 physical addr 一樣 h = tb_phys_hash_func(phys_pc); tb_remove(&tb_phys_hash[h], tb, offsetof(TranslationBlock, phys_hash_next)); // 將 tb 從相應的 PageDesc 中移除 if (tb->page_addr[0] != page_addr) { p = page_find(tb->page_addr[0] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } if (tb->page_addr[1] != -1 && tb->page_addr[1] != page_addr) { p = page_find(tb->page_addr[1] >> TARGET_PAGE_BITS); tb_page_remove(&p->first_tb, tb); invalidate_page_bitmap(p); } tb_invalidated_flag = 1; // 將 tb 從 tb_jmp_cache 移除 h = tb_jmp_cache_hash_func(tb->pc); // 因為每一個 env 都有一份自己的 tb_jmp_cache,全部清除。 for(env = first_cpu; env != NULL; env = env->next_cpu) { if (env->tb_jmp_cache[h] == tb) env->tb_jmp_cache[h] = NULL; } // 處理 tb1 (tb -> tb1) tb_jmp_remove(tb, 0); tb_jmp_remove(tb, 1); // 處理 tb1 (tb1 -> tb) tb1 = tb->jmp_first; for(;;) { n1 = (long)tb1 & 3; if (n1 == 2) // tb1 末兩位如果為 10 (2),代表 tb1 沒有跳至其它 tb break; tb1 = (TranslationBlock *)((long)tb1 & ~3); // 還原回原本的 tb1 tb2 = tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) tb_reset_jump(tb1, n1); // 將 tb1 至其它的 tb 的 block chaining 打斷 (code cache) tb1->jmp_next[n1] = NULL; tb1 = tb2; } tb->jmp_first = (TranslationBlock *)((long)tb | 2); // 將 jmp_first 再次指向自己 }
static inline void tb_jmp_remove(TranslationBlock *tb, int n) { ptb = &tb->jmp_next[n]; // n (0 或 1) 指示 tb 下一個 block chaining 的方向 tb1 = *ptb; // 處理 tb1 (tb -> tb1) if (tb1) { /* find tb(n) in circular list */ for(;;) { tb1 = *ptb; n1 = (long)tb1 & 3; // 取出 tb1 末兩位 tb1 = (TranslationBlock *)((long)tb1 & ~3); 還原回原本的 tb1 if (n1 == n && tb1 == tb) // 代表 tb 沒有跳至其它 tb break; if (n1 == 2) { ptb = &tb1->jmp_first; // 代表沒有其它 tb 跳至 tb1 } else { ptb = &tb1->jmp_next[n1]; // 處理 tb2 (tb1 -> tb2) } } /* now we can suppress tb(n) from the list */ *ptb = tb->jmp_next[n]; tb->jmp_next[n] = NULL; } }
static inline int handle_cpu_signal(unsigned long pc, unsigned long address, ...) { if (is_write && page_unprotect(h2g(address), pc, puc)) { return 1; } }
int page_unprotect(target_ulong address, unsigned long pc, void *puc) { p = page_find(address >> TARGET_PAGE_BITS); /* if the page was really writable, then we change its protection back to writable */ if ((p->flags & PAGE_WRITE_ORG) && !(p->flags & PAGE_WRITE)) { host_start = address & qemu_host_page_mask; host_end = host_start + qemu_host_page_size; prot = 0; for (addr = host_start ; addr < host_end ; addr += TARGET_PAGE_SIZE) { p = page_find(addr >> TARGET_PAGE_BITS); p->flags |= PAGE_WRITE; prot |= p->flags; /* and since the content will be modified, we must invalidate the corresponding translated code. */ tb_invalidate_phys_page(addr, pc, puc); } mprotect((void *)g2h(host_start), qemu_host_page_size, prot & PAGE_BITS); return 1; } }
使用 GDB 的時候,會出現 host 發出的 SIGSEGV。
PAGE(0x8048000): cp <0x804854a>[0..57] <0x80485e1> Program received signal SIGSEGV, Segmentation fault. 0x00000000602296ac in static_code_gen_buffer () (gdb) c Continuing. Hello :-) No endless loop here! Program exited with code 052.
如果看 QEMU in_asm 的 log,會發現 injectHere 所在的區段被翻譯過兩次。如果 TARGET_HAS_PRECISE_SMC 被定義,會有額外的處理,這在 Qemu 0.5.4 被加入。x86 上針對 self-modifying code 會自動偵測並處理,無需程序員用特定指令將快取清空。TARGET_HAS_PRECISE_SMC 是針對某客戶機指令修改該指令所在客戶機內存區段的情況。
\_\_stl_mmu → io_writel → notdirty_mem_writel → tb_invalidate_phys_page_fast → tb_invalidate_phys_page_range
void tb_invalidate_phys_page_range(tb_page_addr_t start, tb_page_addr_t end, int is_cpu_write_access) { TranslationBlock *tb, *tb_next, *saved_tb; CPUState *env = cpu_single_env; tb_page_addr_t tb_start, tb_end; PageDesc *p; int n; #ifdef TARGET_HAS_PRECISE_SMC int current_tb_not_found = is_cpu_write_access; TranslationBlock *current_tb = NULL; int current_tb_modified = 0; target_ulong current_pc = 0; target_ulong current_cs_base = 0; int current_flags = 0; #endif /* TARGET_HAS_PRECISE_SMC */ p = page_find(start >> TARGET_PAGE_BITS); if (!p) return; if (!p->code_bitmap && ++p->code_write_count >= SMC_BITMAP_USE_THRESHOLD && is_cpu_write_access) { /* build code bitmap */ build_page_bitmap(p); } /* we remove all the TBs in the range [start, end[ */ /* XXX: see if in some cases it could be faster to invalidate all the code */ tb = p->first_tb; while (tb != NULL) { n = (long)tb & 3; tb = (TranslationBlock *)((long)tb & ~3); tb_next = tb->page_next[n]; /* NOTE: this is subtle as a TB may span two physical pages */ if (n == 0) { /* NOTE: tb_end may be after the end of the page, but it is not a problem */ tb_start = tb->page_addr[0] + (tb->pc & ~TARGET_PAGE_MASK); tb_end = tb_start + tb->size; } else { tb_start = tb->page_addr[1]; tb_end = tb_start + ((tb->pc + tb->size) & ~TARGET_PAGE_MASK); } if (!(tb_end <= start || tb_start >= end)) { #ifdef TARGET_HAS_PRECISE_SMC if (current_tb_not_found) { current_tb_not_found = 0; current_tb = NULL; if (env->mem_io_pc) { /* now we have a real cpu fault */ current_tb = tb_find_pc(env->mem_io_pc); } } if (current_tb == tb && (current_tb->cflags & CF_COUNT_MASK) != 1) { /* If we are modifying the current TB, we must stop its execution. We could be more precise by checking that the modification is after the current PC, but it would require a specialized function to partially restore the CPU state */ current_tb_modified = 1; cpu_restore_state(current_tb, env, env->mem_io_pc, NULL); cpu_get_tb_cpu_state(env, ¤t_pc, ¤t_cs_base, ¤t_flags); } #endif /* TARGET_HAS_PRECISE_SMC */ /* we need to do that to handle the case where a signal occurs while doing tb_phys_invalidate() */ saved_tb = NULL; if (env) { saved_tb = env->current_tb; env->current_tb = NULL; } tb_phys_invalidate(tb, -1); if (env) { env->current_tb = saved_tb; if (env->interrupt_request && env->current_tb) cpu_interrupt(env, env->interrupt_request); } } tb = tb_next; } #if !defined(CONFIG_USER_ONLY) /* if no code remaining, no need to continue to use slow writes */ if (!p->first_tb) { invalidate_page_bitmap(p); if (is_cpu_write_access) { tlb_unprotect_code_phys(env, start, env->mem_io_vaddr); } } #endif #ifdef TARGET_HAS_PRECISE_SMC if (current_tb_modified) { /* we generate a block containing just the instruction modifying the memory. It will ensure that it cannot modify itself */ env->current_tb = NULL; tb_gen_code(env, current_pc, current_cs_base, current_flags, 1); cpu_resume_from_signal(env, NULL); } #endif }
static inline void cpu_get_tb_cpu_state(CPUState *env, target_ulong *pc, target_ulong *cs_base, int *flags) { *cs_base = env->segs[R_CS].base; *pc = *cs_base + env->eip; *flags = env->hflags | (env->eflags & (IOPL_MASK | TF_MASK | RF_MASK | VM_MASK)); }
static inline DATA_TYPE glue(io_read, SUFFIX)(target_phys_addr_t physaddr, target_ulong addr, void *retaddr) { DATA_TYPE res; int index; index = (physaddr >> IO_MEM_SHIFT) & (IO_MEM_NB_ENTRIES - 1); physaddr = (physaddr & TARGET_PAGE_MASK) + addr; env->mem_io_pc = (unsigned long)retaddr; if (index > (IO_MEM_NOTDIRTY >> IO_MEM_SHIFT) && !can_do_io(env)) { cpu_io_recompile(env, retaddr); } ... 略 ... }
TCG IR 分為底下幾類:
mov %eax, 0x4(%ebx)
movi 0x8000000, 0x20(%r14) # env->eip = 0x8000000
OP: 右 (源) 至左 (目的)。 OUT_ASM: 左 (源) 至右 (目的)。
QEMU 用 typedef enum TCGOpcode 枚舉所有的 TCG Opcode。可以在 cpu-exec.i 看到宏展開之後的結果,例如: INDEX_op_add_i32。gen_opc_buf 指向存放 TCG opcode 的緩衝區,gen_opparam_buf 指向存放 TCG opcode 所需參數的緩衝區。Opcode end 是用來作為 gen_opc_buf 結尾的標記,Opcode exit_tb 代表 block 結尾,準備跳回 QEMU。
在 exec.c 的最後。
#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?。
static inline void tcg_gen_exit_tb(tcg_target_long val) { // tcg_gen_exit_tb((tcg_target_long)tb + tb_num); // 將 INDEX_op_exit_tb 寫入 gen_opc_buf; val 寫入 gen_opparam_buf。 tcg_gen_op1i(INDEX_op_exit_tb, val); }
static inline void tcg_out_op(TCGContext *s, int opc, const TCGArg *args, const int *const_args) { case INDEX_op_exit_tb: tcg_out_movi(s, TCG_TYPE_I32, TCG_REG_EAX, args[0]); // 將 val 寫進 EAX // e9 是 jmp 指令,後面的 operand 為相對偏移量,將會加上 eip。 // 總和效果使跳回 code_gen_prologue 中 prologue 以後的位置。 tcg_out8(s, 0xe9); /* jmp tb_ret_addr */ // tb_ret_addr 在 tcg_target_qemu_prologue 初始成指向 code_gen_prologue 中 prologue 以後的位置。 // 生成 host binary 的同時,s->code_ptr 會移向下一個 code buffer 的位址。 tcg_out32(s, tb_ret_addr - s->code_ptr - 4); break; }
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 { // move arg 至 ret tcg_out8(s, 0xb8 + ret); // 0xb8 為 move,ret 代表目的暫存器。0xb8 + ret 合成一個 opcode。 tcg_out32(s, arg); } }
static void tcg_out_modrm(TCGContext *s, int opc, int r, int rm) { tcg_out_opc(s, opc, r, rm, 0); tcg_out8(s, 0xc0 | (LOWREGMASK(r) << 3) | LOWREGMASK(rm)); }
static inline void tcg_gen_goto_tb(int idx) { tcg_gen_op1i(INDEX_op_goto_tb, idx); }
static void save_globals(TCGContext *s, TCGRegSet allocated_regs) { int i; for(i = 0; i < s->nb_globals; i++) { temp_save(s, i, allocated_regs); } }
// 用 addrlo_idx 索引 args 得到位址下半部,用 addrlo_idx + 1 索引 args 得到位址上半部。 // mem_index 用來索引 CPUState 中的 tlb_table[mem_index]。 // s_bits 是欲讀取資料大小以 2 為底的對數。 // which 是存取 CPUTLBEntry 其成員的偏移量,應該是 addr_read 或是 addr_write 的偏移。 static inline void tcg_out_tlb_load(TCGContext *s, int addrlo_idx, int mem_index, int s_bits, const TCGArg *args, uint8_t **label_ptr, int which) { const int addrlo = args[addrlo_idx]; // 索引 args 得到位址下半部。 const int r0 = tcg_target_call_iarg_regs[0]; // 取得參數傳遞所用的暫存器。 const int r1 = tcg_target_call_iarg_regs[1]; tcg_out_mov(s, type, r1, addrlo); // 分別複製參數 addrlo 至 r0 和 r1 tcg_out_mov(s, type, r0, addrlo); // 邏輯右移 // TARGET_PAGE_BITS 是 page size 以 2 為底的對數。 // CPU_TLB_ENTRY_BITS 是 CPUTLBEntry 以 2 為底的對數。 tcg_out_shifti(s, SHIFT_SHR + rexw, r1, TARGET_PAGE_BITS - CPU_TLB_ENTRY_BITS); // 取得該位址所在頁,利用 TARGET_PAGE_MASK。 tgen_arithi(s, ARITH_AND + rexw, r0, TARGET_PAGE_MASK | ((1 << s_bits) - 1), 0); // CPU_TLB_BITS 是 TLB 大小以 2 為底的對數。 tgen_arithi(s, ARITH_AND + rexw, r1, (CPU_TLB_SIZE - 1) << CPU_TLB_ENTRY_BITS, 0); tcg_out_modrm_sib_offset(s, OPC_LEA + P_REXW, r1, TCG_AREG0, r1, 0, offsetof(CPUState, tlb_table[mem_index][0]) + which); /* cmp 0(r1), r0 */ tcg_out_modrm_offset(s, OPC_CMP_GvEv + rexw, r0, r1, 0); tcg_out_mov(s, type, r0, addrlo); // r0 = addrlo /* jne label1 */ // TLB 缺失,跳至 label1 /* TLB Hit. */ /* add addend(r1), r0 */ tcg_out_modrm_offset(s, OPC_ADD_GvEv + P_REXW, r0, r1, offsetof(CPUTLBEntry, addend) - which); }
/* XXX: qemu_ld and qemu_st could be modified to clobber only EDX and EAX. It will be useful once fixed registers globals are less common. */ // // opc 代表 tcg_out_qemu_ld 是被 INDEX_op_qemu_ld8u (0),INDEX_op_qemu_ld16u (1),等等所呼叫。 // static void tcg_out_qemu_ld(TCGContext *s, const TCGArg *args, int opc) { addrlo_idx = 1; // TARGET_LONG_BITS 指被模擬的 guest。TCG_TARGET_REG_BITS 指宿主 host。 // mem_index 用來索引 CPUState 中的 tlb_table[mem_index]。 mem_index = args[addrlo_idx + 1 + (TARGET_LONG_BITS > TCG_TARGET_REG_BITS)]; // QEMU 會利用 opc 第 2 位指明是有號/無號數,這裡取末兩位作為欲讀取資料大小。 s_bits = opc & 3; // addrlo_idx=1, mem_index=0, s_bits=1, which=0 // 傳遞 label_ptr 給 tcg_out_tlb_load 生成 label。 // label_ptr[0] 為 TLB 命中,label_ptr[1] 為 TLB 缺失。 tcg_out_tlb_load(s, addrlo_idx, mem_index, s_bits, args, label_ptr, offsetof(CPUTLBEntry, addr_read)); /* TLB Hit. */ tcg_out_qemu_ld_direct(s, data_reg, data_reg2, tcg_target_call_iarg_regs[0], 0, opc); /* TLB Miss. */ // 準備參數。 tcg_out_movi(s, TCG_TYPE_I32, tcg_target_call_iarg_regs[arg_idx], mem_index); // 呼叫 helper function。 tcg_out_calli(s, (tcg_target_long)qemu_ld_helpers[s_bits]); // 視情況擴展。 switch(opc) { } /* label2: */ *label_ptr[2] = s->code_ptr - label_ptr[2] - 1; }
---- 0xe86c8 mov_i32 tmp2,edi qemu_ld8u tmp0,tmp2,$0x0 ext8u_i32 tmp12,tmp0 movi_i32 tmp13,$0xffffff00 and_i32 edx,edx,tmp13 or_i32 edx,edx,tmp12 OUT: [size=172] 0x40000ce0: mov 0x1c(%r14),%ebp 0x40000ce4: mov %ebp,%esi // tcg_out_mov(s, type, r1, addrlo); 0x40000ce6: mov %ebp,%edi 0x40000ce8: shr $0x7,%esi // tcg_out_shifti 0x40000ceb: and $0xfffff000,%edi // tgen_arithi,取得目標位址所在頁 0x40000cf1: and $0x1fe0,%esi // tgen_arithi,取得 TLB entry index 0x40000cf7: lea 0x348(%r14,%rsi,1),%rsi // tcg_out_modrm_sib_offset,取得 TLB entry 位址 0x40000cff: cmp (%rsi),%edi // tcg_out_modrm_offset,將 TLB entry 位址其內容和目標位址所在頁加以比較 0x40000d01: mov %ebp,%edi // edi = ebp 0x40000d03: jne 0x40000d0e // TLB 缺失,跳至 0x40000d0e 0x40000d05: add 0x10(%rsi),%rdi // TLB 命中。tcg_out_modrm_offset。將 addend (0x10(%rsi)) 加上 %rdi。 0x40000d09: movzbl (%rdi),%ebp // ebp 是欲讀取的值? 0x40000d0c: jmp 0x40000d18 0x40000d0e: xor %esi,%esi // TLB 缺失。 0x40000d10: callq 0x54cf8e // \_\_ldb_mmu 0x40000d15: movzbl %al,%ebp // 視狀況擴展。 0x40000d18: movzbl %bpl,%ebp 0x40000d1c: mov 0x8(%r14),%ebx
IN: 0xc014198d: test %ebp,%ebp 0xc014198f: je 0xc0141999 OP: ---- 0xc014198d mov_i32 tmp0,ebp mov_i32 tmp1,ebp discard cc_src // 捨棄 cc_src 暫存器的內容。 and_i32 cc_dst,tmp0,tmp1 ---- 0xc014198f movi_i32 cc_op,$0x18 // 藉由之前計算出的 codition code 決定分支方向。 movi_i32 tmp12,$0x0 brcond_i32 cc_dst,tmp12,eq,$0x0 goto_tb $0x0 // 為 if 預留空間。tcg_gen_goto_tb(tb_num); movi_i32 tmp4,$0xc0141991 // if 分支跳躍目標,0xc0141991。gen_jmp_im(eip); st_i32 tmp4,env,$0x20 // 將該目標寫入 guest pc exit_tb $0x7f042bc5c070 // tcg_gen_exit_tb((tcg_target_long)tb + tb_num); set_label $0x0 goto_tb $0x1 // 為 else 預留空間 movi_i32 tmp4,$0xc0141999 // else 分支跳躍目標,0xc0141999 st_i32 tmp4,env,$0x20 // 將該目標寫入 guest pc exit_tb $0x7f042bc5c071 OUT: [size=89] 0x40b3cee0: mov 0x14(%r14),%ebp 0x40b3cee4: mov 0x14(%r14),%ebx 0x40b3cee8: and %ebx,%ebp 0x40b3ceea: mov $0x18,%ebx 0x40b3ceef: mov %ebx,0x30(%r14) 0x40b3cef3: mov %ebp,0x2c(%r14) 0x40b3cef7: test %ebp,%ebp 0x40b3cef9: je 0x40b3cf1c 0x40b3ceff: jmpq 0x40b3cf04 // 預留將來 block linking patch 的點 0x40b3cf04: mov $0xc0141991,%ebp // if 分支跳躍目標,0xc0141991 0x40b3cf09: mov %ebp,0x20(%r14) // 將該目標寫入 guest pc 0x40b3cf0d: mov $0x7f042bc5c070,%rax // if (藉由後 2 個 bit) 0x40b3cf17: jmpq 0x10eadae // 返回 prologue/epilogue 0x40b3cf1c: jmpq 0x40b3cf21 // 預留將來 block linking patch 的點 0x40b3cf21: mov $0xc0141999,%ebp // else 分支跳躍目標,0xc0141999 0x40b3cf26: mov %ebp,0x20(%r14) // 將該目標寫入 guest pc 0x40b3cf2a: mov $0x7f042bc5c071,%rax // else (藉由後 2 個 bit) 0x40b3cf34: jmpq 0x10eadae // 返回 prologue/epilogue
struct TCGOpDef
用來定義各個 TCG Op 的相關性質。在不同宿主機上,暫存器分配有不同限制,由 strutc TCGArgConstraint
規範。 typedef struct TCGArgConstraint { uint16_t ct; uint8_t alias_index; union { // 視平台有多少個暫存器,TCGRegSet 被 typedef 成 uint32_t 或是 uint64_t。 // 用來代表宿主機上的暫存器組。 TCGRegSet regs; } u; } TCGArgConstraint /* Bits for TCGOpDef->flags, 8 bits available. */ enum { /* Instruction defines the end of a basic block. */ TCG_OPF_BB_END = 0x01, /* Instruction clobbers call registers and potentially update globals. */ TCG_OPF_CALL_CLOBBER = 0x02, /* Instruction has side effects: it cannot be removed if its outputs are not used. */ TCG_OPF_SIDE_EFFECTS = 0x04, /* Instruction operands are 64-bits (otherwise 32-bits). */ TCG_OPF_64BIT = 0x08, /* Instruction is optional and not implemented by the host. */ TCG_OPF_NOT_PRESENT = 0x10, }; typedef struct TCGOpDef { const char *name; // 此 TCG Op 的輸出參數,輸入參數,常數參數和參數個數。 uint8_t nb_oargs, nb_iargs, nb_cargs, nb_args; uint8_t flags; TCGArgConstraint *args_ct; int *sorted_args; } TCGOpDef;
tcg/i386/tcg-target.c
有其規範。static const TCGTargetOpDef x86_op_defs[] = { { INDEX_op_exit_tb, { } }, { INDEX_op_goto_tb, { } }, { INDEX_op_call, { "ri" } }, { INDEX_op_jmp, { "ri" } }, ... 略 ... } /* parse target specific constraints */ static int target_parse_constraint(TCGArgConstraint *ct, const char **pct_str) { switch(ct_str[0]) { case 'a': ct->ct |= TCG_CT_REG; tcg_regset_set_reg(ct->u.regs, TCG_REG_EAX); break; ... 略 ... default: return -1; } ct_str++; *pct_str = ct_str; return 0; }
tcg/tcg-opc.h
定義各個 TCG Op 的相關性質。 /* * DEF(name, oargs, iargs, cargs, flags) */ DEF(qemu_ld8u, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS) DEF(qemu_ld8s, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS) DEF(qemu_ld16u, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS) DEF(qemu_ld16s, 1, 1, 1, TCG_OPF_CALL_CLOBBER | TCG_OPF_SIDE_EFFECTS)
struct TCGContext
為暫存器分配的中樞。typedef struct TCGTemp { TCGType base_type; TCGType type; int val_type; int reg; tcg_target_long val; int mem_reg; tcg_target_long mem_offset; unsigned int fixed_reg:1; unsigned int mem_coherent:1; unsigned int mem_allocated:1; unsigned int temp_local:1; /* If true, the temp is saved across basic blocks. Otherwise, it is not preserved across basic blocks. */ unsigned int temp_allocated:1; /* never used for code gen */ /* index of next free temp of same base type, -1 if end */ int next_free_temp; const char *name; } TCGTemp; struct TCGContext { uint8_t *pool_cur, *pool_end; TCGPool *pool_first, *pool_current, *pool_first_large; TCGLabel *labels; int nb_labels; TCGTemp *temps; /* globals first, temps after */ ... 略 ... };
tcg_gen_code_common
(tcg/tcg.c
) 檢視 TCG IR。TCGOpDef tcg_op_defs[] = { #define DEF(s, oargs, iargs, cargs, flags) { #s, oargs, iargs, cargs, iargs + oargs + cargs, flags }, #include "tcg-opc.h" #undef DEF }; static inline int tcg_gen_code_common(TCGContext *s, uint8_t *gen_code_buf, long search_pc) { unsigned int dead_args; tcg_reg_alloc_start(s); for(;;) { opc = gen_opc_buf[op_index]; def = &tcg_op_defs[opc]; switch(opc) { ... 略 ... default: /* Sanity check that we've not introduced any unhandled opcodes. */ if (def->flags & TCG_OPF_NOT_PRESENT) { tcg_abort(); } /* Note: in order to speed up the code, it would be much faster to have specialized register allocator functions for some common argument patterns */ dead_args = s->op_dead_args[op_index]; tcg_reg_alloc_op(s, def, opc, args, dead_args); break; } args += def->nb_args; next: if (search_pc >= 0 && search_pc < s->code_ptr - gen_code_buf) { return op_index; } op_index++; } the_end: return -1; } }
tcg_reg_alloc_op
(tcg/tcg.c
) 分配暫存器並呼叫 tcg_out_op
生成 host binary。static void tcg_reg_alloc_op(TCGContext *s, const TCGOpDef *def, TCGOpcode opc, const TCGArg *args, unsigned int dead_args) { TCGRegSet allocated_regs; int i, k, nb_iargs, nb_oargs, reg; TCGArg arg; const TCGArgConstraint *arg_ct; TCGTemp *ts; TCGArg new_args[TCG_MAX_OP_ARGS]; int const_args[TCG_MAX_OP_ARGS]; // 取得該 TCG Op 其輸出和輸入參數個數。 nb_oargs = def->nb_oargs; nb_iargs = def->nb_iargs; /* copy constants */ // TCGArg 依序放置輸出參數、輸入參數和常數參數。 memcpy(new_args + nb_oargs + nb_iargs, args + nb_oargs + nb_iargs, sizeof(TCGArg) * def->nb_cargs); // 分配暫存器給輸入參數。 // 將 global 存回內存。 // 分配暫存器給輸出參數。 // 生成 host binary。 /* emit instruction */ tcg_out_op(s, opc, new_args, const_args); /* move the outputs in the correct register if needed */ for(i = 0; i < nb_oargs; i++) { ts = &s->temps[args[i]]; reg = new_args[i]; if (ts->fixed_reg && ts->reg != reg) { tcg_out_mov(s, ts->type, ts->reg, reg); } } }
icount 只有在 system mode 下有作用。以 x86 為例,
typedef struct icount_decr_u16 { uint16_t low; uint16_t high; } icount_decr_u16; #define CPU_COMMON \ int64_t icount_extra; /* Instructions until next timer event. */ \ /* Number of cycles left, with interrupt flag in high bit. \ This allows a single read-compare-cbranch-write sequence to test \ for both decrementer underflow and exceptions. */ \ union { \ uint32_t u32; \ icount_decr_u16 u16; \ } icount_decr;
static inline void gen_intermediate_code_internal(CPUState *env, ...) { gen_icount_start(); for(;;) { pc_ptr = disas_insn(dc, pc_ptr); num_insns++; } if (tb->cflags & CF_LAST_IO) gen_io_end(); gen_icount_end(tb, num_insns); }
static inline void gen_icount_start(void) { TCGv_i32 count; if (!use_icount) return; icount_label = gen_new_label(); count = tcg_temp_local_new_i32(); tcg_gen_ld_i32(count, cpu_env, offsetof(CPUState, icount_decr.u32)); /* This is a horrid hack to allow fixing up the value later. */ icount_arg = gen_opparam_ptr + 1; tcg_gen_subi_i32(count, count, 0xdeadbeef); // count -= 0xdeadbeef; tcg_gen_brcondi_i32(TCG_COND_LT, count, 0, icount_label); // if count < 0 goto icount_label; tcg_gen_st16_i32(count, cpu_env, offsetof(CPUState, icount_decr.u16.low)); // else count = icount_decr.u16.low tcg_temp_free_i32(count); }
static void gen_icount_end(TranslationBlock *tb, int num_insns) { if (use_icount) { *icount_arg = num_insns; gen_set_label(icount_label); // 設置 label,做為 block 的開頭。如果 counter 小於零,跳至此 label。 tcg_gen_exit_tb((tcg_target_long)tb + 2); // 返回 QEMU,末兩位設為 2 做為返回值。 } }
if (likely(!env->exit_request)) { tc_ptr = tb->tc_ptr; /* execute the generated code */ next_tb = tcg_qemu_tb_exec(env, tc_ptr); // 只有當 icount 開啟且 counter expire,next_tb 末兩位才會被設成 2。 if ((next_tb & 3) == 2) { /* Instruction counter expired. */ int insns_left; tb = (TranslationBlock *)(long)(next_tb & ~3); /* Restore PC. */ cpu_pc_from_tb(env, tb); // env->eip = tb->pc - tb->cs_base; insns_left = env->icount_decr.u32; if (env->icount_extra && insns_left >= 0) { /* Refill decrementer and continue execution. */ env->icount_extra += insns_left; if (env->icount_extra > 0xffff) { insns_left = 0xffff; } else { insns_left = env->icount_extra; } env->icount_extra -= insns_left; env->icount_decr.u16.low = insns_left; } else { if (insns_left > 0) { /* Execute remaining instructions. */ cpu_exec_nocache(env, insns_left, tb); } env->exception_index = EXCP_INTERRUPT; next_tb = 0; cpu_loop_exit(env); } }
/* Execute the code without caching the generated code. An interpreter could be used if available. */ static void cpu_exec_nocache(CPUState *env, int max_cycles, TranslationBlock *orig_tb) { unsigned long next_tb; TranslationBlock *tb; /* Should never happen. We only end up here when an existing TB is too long. */ if (max_cycles > CF_COUNT_MASK) max_cycles = CF_COUNT_MASK; tb = tb_gen_code(env, orig_tb->pc, orig_tb->cs_base, orig_tb->flags, max_cycles); env->current_tb = tb; /* execute the generated code */ next_tb = tcg_qemu_tb_exec(env, tb->tc_ptr); env->current_tb = NULL; if ((next_tb & 3) == 2) { /* Restore PC. This may happen if async event occurs before the TB starts executing. */ cpu_pc_from_tb(env, tb); } tb_phys_invalidate(tb, -1); tb_free(tb); }
TCG 不支援向量指令,guest 向量指令需透過 helper function 實現。此外,考慮 guest 和 host 有大小端的問題,一般只能以 scalar 處理 guest 向量指令,無法直接使用 host 向量指令實現。
// target-arm/neon_helper.c uint32_t HELPER(neon_add_u8)(uint32_t a, uint32_t b) { uint32_t mask; mask = (a ^ b) & 0x80808080u; a &= ~0x80808080u; b &= ~0x80808080u; return (a + b) ^ mask; }
static inline int gen_neon_add(int size, TCGv t0, TCGv t1) { switch (size) { case 0: gen_helper_neon_add_u8(t0, t0, t1); break; case 1: gen_helper_neon_add_u16(t0, t0, t1); break; case 2: tcg_gen_add_i32(t0, t0, t1); break; default: return 1; } return 0; }
static int disas_neon_data_insn(CPUState * env, DisasContext *s, uint32_t insn) { // 視情況將 128/64 bit vector operation 拆成 4/2 個 helper function call (一次處理 32 bit)。 for (pass = 0; pass < (q ? 4 : 2); pass++) { case 16: if (!u) { /* VADD */ if (gen_neon_add(size, tmp, tmp2)) return 1; } else { /* VSUB */ switch (size) { case 0: gen_helper_neon_sub_u8(tmp, tmp, tmp2); break; case 1: gen_helper_neon_sub_u16(tmp, tmp, tmp2); break; case 2: tcg_gen_sub_i32(tmp, tmp, tmp2); break; default: return 1; } } break; } }
Coroutine 在 QEMU 上主要是為了做到多執行緒的效果,又不需要付出多執行緒所需要的開銷12)。某些函式在 QEMU 中被標記成 Coroutine,此類函式無法從一般的函式被呼叫。
static void coroutine_fn foo(void) { ... }
QEMU 會使用 Coroutine 記下函式每一次的進入點,下一次呼叫會從上一次的返回點開始執行。
目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 worker thread 去處理。每一個 VCPU 均有對應的 VCPU 執行緒運行。
int main(int argc, char **argv, char **envp) { ... 略 ... qemu_init_cpu_loop(); // qemu_init_main_loop 呼叫 main_loop_init (main-loop.c) if (qemu_init_main_loop()) { fprintf(stderr, "qemu_init_main_loop failed\n"); exit(1); } ... 略 ... cpu_exec_init_all(); bdrv_init_with_whitelist(); blk_mig_init(); /* open the virtual block devices */ if (snapshot) qemu_opts_foreach(qemu_find_opts("drive"), drive_enable_snapshot, NULL, 0); if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func, &machine->use_scsi, 1) != 0) exit(1); ... 略 ... resume_all_vcpus(); main_loop(); bdrv_close_all(); pause_all_vcpus(); net_cleanup(); res_free(); return 0; }
void qemu_init_cpu_loop(void) { qemu_init_sigbus(); qemu_cond_init(&qemu_cpu_cond); qemu_cond_init(&qemu_pause_cond); qemu_cond_init(&qemu_work_cond); qemu_cond_init(&qemu_io_proceeded_cond); qemu_mutex_init(&qemu_global_mutex); qemu_thread_get_self(&io_thread); }
qemu_init_main_loop
(vl.c
) 呼叫 main_loop_init
(main-loop.c
)。int main_loop_init(void) { int ret; qemu_mutex_lock_iothread(); ret = qemu_signal_init(); if (ret) { return ret; } /* Note eventfd must be drained before signalfd handlers run */ ret = qemu_event_init(); if (ret) { return ret; } return 0; }
static void main_loop(void) { bool nonblocking; int last_io = 0; do { nonblocking = !kvm_enabled() && last_io > 0; last_io = main_loop_wait(nonblocking); } while (!main_loop_should_exit()); }
main_loop_wait
(main-loop.c
) 等待 work thread 完成任務。int main_loop_wait(int nonblocking) { int ret; uint32_t timeout = UINT32_MAX; if (nonblocking) { timeout = 0; } else { qemu_bh_update_timeout(&timeout); } /* poll any events */ /* XXX: separate device handlers from system ones */ nfds = -1; FD_ZERO(&rfds); FD_ZERO(&wfds); FD_ZERO(&xfds); qemu_iohandler_fill(&nfds, &rfds, &wfds, &xfds); // 1. Waits for file descriptors to become readable or writable. ret = os_host_main_loop_wait(timeout); // fd 已便備,處理 IO。 qemu_iohandler_poll(&rfds, &wfds, &xfds, ret); qemu_run_all_timers(); /* Check bottom-halves last in case any of the earlier events triggered them. */ qemu_bh_poll(); return ret; }
qemu_iohandler_poll
(main-loop.c
)。void qemu_iohandler_poll(fd_set *readfds, fd_set *writefds, fd_set *xfds, int ret) { if (ret > 0) { IOHandlerRecord *pioh, *ioh; QLIST_FOREACH_SAFE(ioh, &io_handlers, next, pioh) { if (!ioh->deleted && ioh->fd_read && FD_ISSET(ioh->fd, readfds)) { ioh->fd_read(ioh->opaque); } if (!ioh->deleted && ioh->fd_write && FD_ISSET(ioh->fd, writefds)) { ioh->fd_write(ioh->opaque); } /* Do this last in case read/write handlers marked it for deletion */ if (ioh->deleted) { QLIST_REMOVE(ioh, next); g_free(ioh); } } } }
qemu_bh_poll
(async.c
) 處理 bh。struct QEMUBH { QEMUBHFunc *cb; void *opaque; QEMUBH *next; bool scheduled; bool idle; bool deleted; }; int qemu_bh_poll(void) { QEMUBH *bh, **bhp, *next; ... 略 ... // 有需要的裝置透過 qemu_bh_new (async.c) 將自己的 handler 加進 BH 等待調用。 // 這裡調用排定好的 bh handler。 for (bh = first_bh; bh; bh = next) { next = bh->next; if (!bh->deleted && bh->scheduled) { bh->scheduled = 0; if (!bh->idle) ret = 1; bh->idle = 0; bh->cb(bh->opaque); } } ... 略 ... }
struct QEMUTimer { QEMUClock *clock; // 使用特定的 QEMUClock 計時 int64_t expire_time; QEMUTimerCB *cb; // callback function pointer void *opaque; // 傳給 callback function 的參數 struct QEMUTimer *next; };
QEMUClock 有底下幾種,請見 qemu-timer.h:
rtc_clock 會選擇上述其中一種 clock。
請見 cpu-all.h,基本上有四類通用中斷:
另外留下 CPU_INTERRUPT_TGT_EXT_* 和 CPU_INTERRUPT_TGT_INT_* 給各個 CPU 自行運用。例如: target-i386/cpu.h。
請見 QEMU's new device model qdev 和 QEMU's device model qdev: Where do we go from here?。docs/qdev-device-use.txt。
QOM (Qemu Object Model) 用來取代 QDev 13)。
虛擬外設發出的 IRQ 以 IRQState 包裝。在 QEMU 中,所有的设备包括总线,桥,一般设备都对应一个设备结构。總線,如 PCI 總線,在 QEMU 中包裝成 PCIBus; 橋,如 PCI 橋,在 QEMU 中包裝成 PCIBridge。PCIDeviceInfo。
/* PC hardware initialisation */ static void pc_init1(ram_addr_t ram_size, ...) { /* 初始化 CPU */ /* 初始化內存 */ /* 初始化 PIC */ if (!xen_enabled()) { cpu_irq = pc_allocate_cpu_irq(); i8259 = i8259_init(cpu_irq[0]); } else { i8259 = xen_interrupt_controller_init(); } /* 初始化 ISA */ isa_irq_state = qemu_mallocz(sizeof(*isa_irq_state)); isa_irq_state->i8259 = i8259; /* 初始化 IOAPIC */ if (pci_enabled) { ioapic_init(isa_irq_state); // sysbus_get_default 會創建 main-system-bus } /* 初始化 PCI bus,之後即可將外設掛上 PCI bus */ if (pci_enabled) { pci_bus = i440fx_init(&i440fx_state, &piix3_devfn, isa_irq, ram_size); } else { pci_bus = NULL; i440fx_state = NULL; isa_bus_new(NULL); } /* 初始化其它外設 */ }
qemu_irq *i8259_init(qemu_irq parent_irq) { PicState2 *s; s = qemu_mallocz(sizeof(PicState2)); pic_init1(0x20, 0x4d0, &s->pics[0]); // Master IO port 為 0x20 pic_init1(0xa0, 0x4d1, &s->pics[1]); // Slave IO port 為 0xa0 s->pics[0].elcr_mask = 0xf8; s->pics[1].elcr_mask = 0xde; s->parent_irq = parent_irq; s->pics[0].pics_state = s; s->pics[1].pics_state = s; isa_pic = s; return qemu_allocate_irqs(i8259_set_irq, s, 16); }
static PCIBus *i440fx_common_init(const char *device_name, ...) { DeviceState *dev; PCIBus *b; PCIDevice *d; I440FXState *s; // 北橋 PIIX3State *piix3; // 南橋 (PCI-ISA) /* 创建 PCI 主总线设备 */ dev = qdev_create(NULL, "i440FX-pcihost"); s = FROM_SYSBUS(I440FXState, sysbus_from_qdev(dev)); // 請見 hw/sysbus.h 和 osdep.h /* 创建我们真正的 PCI 总线 */ b = pci_bus_new(&s->busdev.qdev, NULL, 0); s->bus = b; /* 初始化主总线设备 */ qdev_init_nofail(dev); /* 创建主桥 */ d = pci_create_simple(b, 0, device_name); *pi440fx_state = DO_UPCAST(PCII440FXState, dev, d); /* 创建 ISA 桥 (南橋) */ piix3 = DO_UPCAST(PIIX3State, dev, pci_create_simple_multifunction(b, -1, true, "PIIX3")); pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3, PIIX_NUM_PIRQS); /* 连接 8259 中断控制器,IOAPIC 貌似也和在一起 */ piix3->pic = pic; (*pi440fx_state)->piix3 = piix3; *piix3_devfn = piix3->dev.devfn; ram_size = ram_size / 8 / 1024 / 1024; if (ram_size > 255) ram_size = 255; (*pi440fx_state)->dev.config[0x57]=ram_size; return b; /* 此後可將外設掛在這個 PCI bus */ }
以 i8259 為例:
static void i8259_set_irq(void *opaque, int irq, int level) { pic_set_irq1(&s->pics[irq >> 3], irq & 7, level); pic_update_irq(s); }
QEMU 與 KVM 的協作請見 What is the difference between KVM and QEMU? 和 Kernel-based Virtual Machine Technology。
include/qemu/object.h
。
watch_mem_{read, write}
。static uint64_t watch_mem_read(void *opaque, target_phys_addr_t addr, unsigned size) { check_watchpoint(addr & ~TARGET_PAGE_MASK, ~(size - 1), BP_MEM_READ); switch (size) { case 1: return ldub_phys(addr); case 2: return lduw_phys(addr); case 4: return ldl_phys(addr); default: abort(); } } static const MemoryRegionOps watch_mem_ops = { .read = watch_mem_read, .write = watch_mem_write, .endianness = DEVICE_NATIVE_ENDIAN, };
cpu_watchpoint_insert
用來插入 watchpoint。qemu_add_vm_change_state_handler
用來這註冊當 QEMU 狀態有變化時會調用的函式。io_mem_init
。static void io_mem_init(void) { memory_region_init_io(&io_mem_ram, &error_mem_ops, NULL, "ram", UINT64_MAX); memory_region_init_io(&io_mem_rom, &rom_mem_ops, NULL, "rom", UINT64_MAX); memory_region_init_io(&io_mem_unassigned, &unassigned_mem_ops, NULL, "unassigned", UINT64_MAX); memory_region_init_io(&io_mem_notdirty, ¬dirty_mem_ops, NULL, "notdirty", UINT64_MAX); memory_region_init_io(&io_mem_subpage_ram, &subpage_ram_ops, NULL, "subpage-ram", UINT64_MAX); memory_region_init_io(&io_mem_watch, &watch_mem_ops, NULL, "watch", UINT64_MAX); }
check_watchpoint
。/* Generate a debug exception if a watchpoint has been hit. */ static void check_watchpoint(int offset, int len_mask, int flags) { ... 略 ... // check_watchpoint 在 watch_mem_read 中被第一次呼叫時,env->watchpoint_hit 為 NULL。 if (env->watchpoint_hit) { /* We re-entered the check after replacing the TB. Now raise * the debug interrupt so that is will trigger after the * current instruction. */ // 重新執行觸發 watchpoint 的指令,會來到這裡。 // env->interrupt_request 被設為 CPU_INTERRUPT_DEBUG,接著再返回 cpu_exec。(1.b) cpu_interrupt(env, CPU_INTERRUPT_DEBUG); return; } vaddr = (env->mem_io_vaddr & TARGET_PAGE_MASK) + offset; // 查詢目前存取的內存位址是否有被監控。 QTAILQ_FOREACH(wp, &env->watchpoints, entry) { if ((vaddr == (wp->vaddr & len_mask) || (vaddr & wp->len_mask) == wp->vaddr) && (wp->flags & flags)) { wp->flags |= BP_WATCHPOINT_HIT; // 第一次進到 check_watchpoint,env->watchpoint_hit 為 NULL。 if (!env->watchpoint_hit) { env->watchpoint_hit = wp; tb = tb_find_pc(env->mem_io_pc); if (!tb) { cpu_abort(env, "check_watchpoint: could not find TB for " "pc=%p", (void *)env->mem_io_pc); } cpu_restore_state(tb, env, env->mem_io_pc); tb_phys_invalidate(tb, -1); if (wp->flags & BP_STOP_BEFORE_ACCESS) { env->exception_index = EXCP_DEBUG; cpu_loop_exit(env); } else { // 重新翻譯觸發 watchpoint 的 TB,從觸發 watchpoint 的那一條指令開始翻起。 // 返回至 cpu_exec 從觸發 watchpoint 的那一條指令開始執行。(1.a) cpu_get_tb_cpu_state(env, &pc, &cs_base, &cpu_flags); tb_gen_code(env, pc, cs_base, cpu_flags, 1); cpu_resume_from_signal(env, NULL); } } } else { wp->flags &= ~BP_WATCHPOINT_HIT; } } }
cpu_exec
。int cpu_exec(CPUArchState *env) { ... 略 ... for(;;) { if (setjmp(env->jmp_env) == 0) { /* if an exception is pending, we execute it here */ if (env->exception_index >= 0) { if (env->exception_index >= EXCP_INTERRUPT) { /* exit request from the cpu execution loop */ ret = env->exception_index; if (ret == EXCP_DEBUG) { cpu_handle_debug_exception(env); // 處理 watchpoint。(1.b) } break; } else { do_interrupt(env); env->exception_index = -1; } } next_tb = 0; /* force lookup of first TB */ for(;;) { interrupt_request = env->interrupt_request; if (unlikely(interrupt_request)) { ... 略 ... if (interrupt_request & CPU_INTERRUPT_DEBUG) { env->interrupt_request &= ~CPU_INTERRUPT_DEBUG; env->exception_index = EXCP_DEBUG; cpu_loop_exit(env); // 返回 cpu_exec 外層迴圈。(1.a) } ... 略 ... } } } ... 略 ... }
static void notdirty_mem_write(void *opaque, target_phys_addr_t ram_addr, uint64_t val, unsigned size) { int dirty_flags; dirty_flags = cpu_physical_memory_get_dirty_flags(ram_addr); if (!(dirty_flags & CODE_DIRTY_FLAG)) { #if !defined(CONFIG_USER_ONLY) tb_invalidate_phys_page_fast(ram_addr, size); dirty_flags = cpu_physical_memory_get_dirty_flags(ram_addr); #endif } switch (size) { case 1: stb_p(qemu_get_ram_ptr(ram_addr), val); // ram_addr 是 GPA,qemu_get_ram_ptr 將其轉成對應的 HVA。 break; case 2: stw_p(qemu_get_ram_ptr(ram_addr), val); break; case 4: stl_p(qemu_get_ram_ptr(ram_addr), val); break; default: abort(); } dirty_flags |= (0xff & ~CODE_DIRTY_FLAG); cpu_physical_memory_set_dirty_flags(ram_addr, dirty_flags); /* we remove the notdirty callback only if the code has been flushed */ if (dirty_flags == 0xff) tlb_set_dirty(cpu_single_env, cpu_single_env->mem_io_vaddr); }
stl_phys_notdirty
(exec.c
) 寫入 PTE。void stl_phys_notdirty(target_phys_addr_t addr, uint32_t val) { uint8_t *ptr; MemoryRegionSection *section; section = phys_page_find(addr >> TARGET_PAGE_BITS); if (!memory_region_is_ram(section->mr) || section->readonly) { addr = memory_region_section_addr(section, addr); if (memory_region_is_ram(section->mr)) { section = &phys_sections[phys_section_rom]; } io_mem_write(section->mr, addr, val, 4); } else { unsigned long addr1 = (memory_region_get_ram_addr(section->mr) & TARGET_PAGE_MASK) + memory_region_section_addr(section, addr); ptr = qemu_get_ram_ptr(addr1); stl_p(ptr, val); if (unlikely(in_migration)) { if (!cpu_physical_memory_is_dirty(addr1)) { /* invalidate code */ tb_invalidate_phys_page_range(addr1, addr1 + 4, 0); /* set dirty bit */ cpu_physical_memory_set_dirty_flags( addr1, (0xff & ~CODE_DIRTY_FLAG)); } } } }
static int gdb_handle_packet(GDBState *s, const char *line_buf) { }
// target-i386/cpu.h #define cpu_exec cpu_x86_exe
register struct CPUX86State *env asm(AREG0);
# 修改 linux-user/main.c 裡面的 #define DEBUG_LOGFILE "/tmp/qemu.log" # 修改 exec.c 裡面的 logfilename $ qemu-i386 -d in_asm,out_asm,op hello $ less /tmp/qemu.log
$ grep -r qemu_log qemu-0.14.2/*
#define CPU_LOG_IBTC (1 << 10) #define CPU_LOG_TB_FIND (1 << 11)
const CPULogItem cpu_log_items[] = { { CPU_LOG_IBTC, "ibtc", "print ibtc return address" }, }
#ifdef DEBUG_DISAS if (qemu_loglevel_mask(CPU_LOG_IBTC)) { qemu_log("%lu\t%p\n", guest_eip, next_tb->tc_ptr); } #endif
// 傳入 helper function 的參數請見 def-helper.h,也請參考 tcg/README。 // DEF_HELPER_FLAGS_? 中的 ? 代表參數個數。參數的意義分別是: helper function 的名稱,修飾子 (TCG_CALL_CONST 表示 // 該 helper function 是否會修改到全域變數),回傳執型別和參數型別。 // 相對應的函式: void *helper_lookup_ibtc(target_ulong guest_eip, CPUState *env) // 可用 gen_helper_lookup_ibtc(ibtc_host_eip, cpu_T[0]) 產生呼叫該 helper function 的 TCG IR。 DEF_HELPER_FLAGS_2(lookup_ibtc, TCG_CALL_CONST, ptr, tl, env)
# configure 中有 echo "TARGET_DIRS=$target_list" >> $config_host_mak # $target_list 即為 --target-list=i386-linux-user 中的 i386-linux-user。 include config-host.mak # 將 $(TARGET_DIRS) 中的 % 替換成 subdir-%。 SUBDIR_RULES=$(patsubst %,subdir-%, $(TARGET_DIRS)) # $(GENERATED_HEADERS) 是編譯時才產生的標頭檔。 subdir-%: $(GENERATED_HEADERS) $(call quiet-command,$(MAKE) $(SUBDIR_MAKEFLAGS) -C $* V="$(V)" TARGET_DIR="$*/" all,) ifneq ($(wildcard config-host.mak),) include $(SRC_PATH)/Makefile.objs endif $(universal-obj-y) $(common-obj-y): $(GENERATED_HEADERS) subdir-libcacard: $(oslib-obj-y) $(trace-obj-y) qemu-timer-common.o # 從 $(SUBDIR_RULES) 濾出 %-softmmu,% 代表任意長度的字串。 $(filter %-softmmu,$(SUBDIR_RULES)): $(universal-obj-y) $(trace-obj-y) $(common-obj-y) subdir-libdis $(filter %-user,$(SUBDIR_RULES)): $(GENERATED_HEADERS) $(universal-obj-y) $(trace-obj-y) subdir-libdis-user subdir-libuser
QEMU_PROG
即是最後生成的執行檔。一般我們會在 $BUILD
目錄底下編譯,與 $SRC
目錄區隔。######################################################### # Linux user emulator target ifdef CONFIG_LINUX_USER # call 負責將參數,在此為 $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR), # 傳遞給表達式 set-vpath。rules.mak 定義 set-vpath。 $(call set-vpath, $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR)) # 如果 --target-list=i386-linux-user,TARGET_I386 會設成 y,最後成為 obj-y += vm86.o。 # 可以把自己的 *.o 加在 obj-y 之後。 obj-$(TARGET_I386) += vm86.o
# 由 *.c 生成 *.o 檔。$@ 代表欲生成的 *.o 檔,@< 代表輸入的檔案,在此為 *.c 檔。 # 在此可以新增條件,用 clang 生成 LLVM 的 *.bc 檔。 %.o: %.c $(call quiet-command,$(CC) $(QEMU_INCLUDES) $(QEMU_CFLAGS) $(QEMU_DGFLAGS) $(CFLAGS) -c -o $@ $<," CC $(TARGET_DIR)$@") # V 即為 `make V=1` 中的 V。此時會將執行的命令印在螢幕上,否則 @ 會使得執行的命令不顯示在螢幕上。 # $1 即為 $(CC) $(QEMU_INCLUDES) $(QEMU_CFLAGS) $(QEMU_DGFLAGS) $(CFLAGS) -c -o $@ $< # $2 即為 " CC $(TARGET_DIR)$@" quiet-command = $(if $(V),$1,$(if $(2),@echo $2 && $1, @$1)) VPATH_SUFFIXES = %.c %.h %.S %.m %.mak %.texi %.sh # set-vpath 設定 VPATH。VPATH 是變量,vpath 是關鍵字。 # VPATH 是變量,告知 make 在 $BUILD 目錄底下若找不到相應的檔案,應該要再找那些路徑。 # foreach 是將 $(VPATH_SUFFIXES) 中的變數逐一放至 PATTERN,再執行 $(eval vpath $(PATTERN) $1)。 # vpath 為符合 $(PATTERN) 的文件指定搜索目錄 $1,在此即為 $(SRC_PATH)/linux-user:$(SRC_PATH)/linux-user/$(TARGET_ABI_DIR))。 set-vpath = $(if $1,$(foreach PATTERN,$(VPATH_SUFFIXES),$(eval vpath $(PATTERN) $1)))
/* get abs value */ static inline int8_t mipsdsp_sat_abs_u8(uint8_t a) { int8_t temp; temp = a; if (a == 0x80) { set_DSPControl_overflow_flag(1, 20); temp = 0x7f; } else { if ((a & 0x80) == 0x80) temp = -temp; } return temp; }
請見 Git。
$ wget http://wiki.qemu.org/download/linux-user-test-0.3.tar.gz; tar xvf linux-user-test-0.3.tar.gz $ cd linux-user-test-0.3 $ vim qemu-linux-user.sh $ make test