QEMU 的簡介請見 QEMU internals

Internal

QEMU 的簡介請見 QEMU internals

QEMU 中的 target 有兩種意義,

  1. 描述被模擬的硬體
  2. 對 TCG 而言,target 描述產生何種宿主硬體代碼

QEMU 0.9 版以前使用 dyngen,對於 dyngen 的描述可以參考以下文件。QEMU 0.10 以後改採 TCG。可以從下載 QEMU Source Archive 源碼。

請先閱讀 Documentation/GettingStartedDevelopers

  • QEMU does not have a high level design description document - only the source code tells the full story 8-)
  • HACKING 、CODING_STYLE 和 tcg/README。
  • QEMU 使用宏展開。編譯時加上 –extra-cflags="-save-temps" 可以得到宏展開之後的檔案 *.i。
  • 有些註解是 QEMU 0.9 以前的殘留。
  • qemu patchlist 保留早期 QEMU 的 log。

簡單的 patch 請送到 ,請見 Contribute/TrivialPatches

源碼目錄概觀

  • target-ARCH/
    • 定義被模擬硬體,反匯編
  • OS-user/
    • 作業系統相關
  • tcg/
    • 定義如何生成宿主平台指令

Memory

QEMU 1.0 將有變動。請見 HACKING、docs/memory.txt 或 Planning/1.0

請見 memory.[ch]。

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。
  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。
  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。
  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。

以宿主機作業系統的角度來看,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 分配給客戶機的內存是以 RAMBlockRAMList 來管理。內存主要分為底下幾類 (cpu-common.h):

  • IO_MEM_RAM: 一般內存。
  • IO_MEM_ROM: ROM。
  • IO_MEM_UNASSIGNED: 初始未指定。
  • IO_MEM_ROMD: 讀的時候視作為 ROM,寫的時候視作為裝置。
  1. 透過 qemu_ram_alloc (exec.c) 申請空間。
    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 的偏移量。
  2. 透過 cpu_register_physical_memory → cpu_register_physical_memory_offset 註冊該 RAMBlock 的資訊 (跟 QEMU 註冊客戶機物理內存)。target_phys_addr_t 代表客戶機物理內存空間; ram_addr_t 代表宿主機虛擬內存空間。
    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);
           }
        }
    }
  3. 使用 MMIO 的裝置會先呼叫 cpu_register_io_memory 註冊 IO 模擬函式。cpu_register_io_memory 返回 io_mem_write/io_mem_read 的索引。該索引被當作 phys_offset 傳給 cpu_register_physical_memory。
    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

PhysPageDesc 用來描述客戶機物理頁面和宿主機虛擬頁面的對映。有一個二級頁表 l1_phys_map 存放 PhysPageDesc。phys_page_find_alloc 用客戶機物理位址查詢 l1_phys_map 取得 PhysPageDesc,視情況配置新的 PhysPageDesc。

  • cpu_register_physical_memory_offset → phys_page_find → phys_page_find_alloc。
    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

PageDesc 維護 TB 和虛擬頁面/客戶機物理頁面之間的關係 (視 process/system mode 而定)。同樣有一個二級頁表 l1_map 存放 PageDesc。page_find_alloc 查詢 l1_map 取得 PageDesc,視情況配置新的 PageDesc。

  • tb_find_slow → tb_gen_code → tb_link_page → tb_alloc_page → page_find_alloc。
    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 清掉。

  • stl_mmu (softmmu_template.h) → io_writel (softmmu_template.h) → notdirty_mem_writel (exec.c) → notdirty_mem_writel → tb_invalidate_phys_page_fast (exec.c) → tb_invalidate_phys_page_range (exec.c) → tb_phys_invalidate (exec.c) 會將屬於某虛擬頁面/客戶機物理頁面的 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) {
        }
    }
  • tb_invalidate_phys_page (exec.c) → tb_phys_invalidate (exec.c)。tb_invalidate_phys_page 僅在 process mode 有定義,用來處理 SMC。
    #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;
     
    }
  • 最終會呼叫到 tb_phys_invalidate。
    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 再次指向自己
    }
    • tb_jmp_remove 將該 tb 移出 circular lists‎。
      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;
          }
      }
    • cpu_exec (cpu-exec.c) 會用到 tb_invalidated_flag。
      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;
      }

MemoryRegion

請見 memory.[ch]doc/memory.txt

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。
  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。
  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。
  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。
  • 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;
    };

System Mode

Before QEMU 1.0

以 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)

  • main_loop (vl.c) → qemu_main_loop_start (cpus.c) → cpu_exec_all (cpus.c) → main_loop_wait (vl.c)
    • cpu_exec_all (cpus.c) → qemu_clock_enable (qemu-timer.c) → qemu_alarm_pending (qemu-timer.c) → any_cpu_has_work (cpus.c)
    • cpu_exec_all (cpus.c) → qemu_cpu_exec (cpus.c) → cpu_x86_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c)
      • tb_find_slow (cpu-exec.c) → get_page_addr_code (exec-all.h)
      • tb_find_slow (cpu-exec.c) → tb_gen_code (exec.c) → cpu_gen_code (translate-all.c) → gen_intermediate_code (target-i386/translate.c) → tcg_gen_code (tcg/tcg.c) → tcg_gen_code_common (tcg/tcg.c)
  • main_loop_wait (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。

  1. 進入點為 main.c (vl.c)。初始化環境。
    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;
    }
    • dynticks_start_timer 所註冊的 SIGALRM 的信號句柄是 host_alarm_handler。當宿主機作業系統發出 SIGALRM 時,host_alarm_handler 視情況會呼叫 qemu_notify_event。qemu_notify_event 用 cpu_exit 將 QEMU 從當前 code cache 中拉出來檢查 IO。關於 clock 請見 [Qemu-devel] Question on kvm_clock working ...
    • cpu_exec_init_all 的代碼如下:
      /* 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_init_pci (hw/pc_piix.c) 呼叫 pc_init1 (hw/pc_piix.c) 進行 PC 機器的初始化。
      /* 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);
      }
      • Features/RamAPI
        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);
        }
  2. main_loop (vl.c) 是主要的執行迴圈。
    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(); // 暫無作用
    }
  3. 翻譯並執行客戶端代碼是由 cpu_exec_all (cpus.c) 負責。
    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();
    }
    • qemu_cpu_exec 基本上只額外多做計數。
  4. 處理 IO 是由 main_loop_wait (vl.c) 負責。How to use the select(), an I/O Multiplexer
    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();
    }

After QEMU 1.0

QEMU 1.0 開啟 IO thread,無法關閉。仍舊以 qemu-system-i386 為例:

模擬虛擬 CPU 和虛擬外設分為不同的執行緒。開機時至少會看到兩個執行緒,主執行緒處理 IO,另一個則是模擬虛擬 CPU 的執行緒。模擬客戶機 CPU 的流程如下:

  1. cpu_init/cpu_x86_init (target-i386/helper.c) 在初始化虛擬 CPU 時,會呼叫 qemu_init_vcpu
    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;
    }
  2. qemu_init_vcpu (cpus.c)
    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);
        }
    }
  3. qemu_tcg_init_vcpu (cpus.c)
    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;
        }
    }
  4. qemu_tcg_cpu_thread_fn (cpus.c)
    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;
    }
  5. tcg_exec_all (cpus.c) 執行所有的虛擬 CPU。
    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_tcg_cpu_thread_fn (cpus.c) → tcg_exec_all (cpus.c) → tcg_cpu_exec (cpus.c) → cpu_x86_exec (cpu-exec.c)

目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 posix-aio-compat.c worker thread 去處理。

  1. main (vl.c)
        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);
        }
    • qemu_init_cpu_loop (cpus.c)
      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);
      }
    • 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;
      }
  2. main_loop (vl.c) 是主要的執行迴圈,IO thread。
    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());
    }
  3. main_loop_wait (main-loop.c)
    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

虛擬機重啟 (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)
  1. 當有 reboot (reset) 的需要時,會呼叫 qemu_system_reset_request (vl.c) 拉起 reset_requested。
    void qemu_system_reset_request(void)
    {
        if (no_reboot) {
            shutdown_requested = 1;
        } else {
            reset_requested = 1;
        }
        cpu_stop_current();
        qemu_notify_event();
    }
    • 以 i386 為例,大約有以下幾處會呼叫 qemu_system_reset_request。前兩者都是當出現 Triple fault 的時候重啟系統,後者是拉起 port 92。
      1. target-i386/op_helper.c
      2. target-i386/helper.c
      3. hw/pc.c
  2. 在 main_loop (vl.c) 中會呼叫 main_loop_should_exit 判斷是否需要跳離主迴圈。
    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());
    }
  3. 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;
    }
  4. 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();
    }
  5. pc_cpu_reset 呼叫 cpu_reset (target-i386/helper.c)。
    static void pc_cpu_reset(void *opaque)
    {
        CPUState *env = opaque;
     
        cpu_reset(env);
        env->halted = !cpu_is_bsp(env);
    }
  6. cpu_reset (target-i386/helper.c) 開機或是重啟時會將 CPU 狀態重置。
    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);
    }

Software MMU

  • target_phys_addr_t (targphys.h) 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。
  • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。
  • ram_addr_t (cpu-common.h) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。
  • tb_page_addr_t (exec-all.h) 在 system mode 中被 typedef 成 ram_addr_t; 在 process mode 中被 typedef 成 abi_ulong,abi_ulong 又被 typedef 成 target_ulong。

guest virtual addr (GVA) → guest physical addr (GPA) → host virtual addr (HVA)

  1. GVA → GPA 由客戶機作業系統負責; GPA → HVA 由 QEMU 負責。HVA → HPA 由宿主機作業系統負責
      • GVA → HVA。存放 GVA 相對於 HVA 的偏移量。轉換 GVA 到 HVA 的過程中,會先搜尋 TLB。如果命中,則將 GVA 加上該偏移量得到 HVA。若否,則需搜尋 l1_phys_map 並將 PhysPageDesc 填入 TLB。
    1. guest virtual addr → guest physical addr
    2. 搜尋 l1_phys_map 得到 PhysPageDesc。
    3. 將 phys_ram_base 加上 PhysPageDesc.phys_offset,得到 host virtual addr (physical addr → host virtual addr)
  typedef struct CPUTLBEntry {
    // 以下存放 GVA,同時也代表該頁面的權限。tlb_set_page (exec.c) 填入新的 TLB 項目時會做設置。
    target_ulong addr_read; // 可讀
    target_ulong addr_write; // 可寫
    target_ulong addr_code; // 可執行
    // HVA 相對於 GVA 的偏移量。
    unsigned long addend;
} CPUTLBEntry;
  1. tb_find_slow 是利用 guest pc (GVA) 對映的 guest physical address (GPA) 查找該 guest pc 的 TB。
    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;
    }
  2. get_page_addr_code (exec.c) 先查找 TLB。process mode 情況有所不同,此時沒有所謂的 GPA,直接返回 addr。注意! get_page_addr_code 是被 tb_find_slow (cpu-exec.c) 或是 tb_gen_code (exec.c) 這兩個函式呼叫,get_page_addr_code 中的 code 代表存取的地址是一段 code。因此,皆是呼叫到 ld*_code 或是 ldb_cmmu。強烈建議查看 i386-softmmu/exec.i。
    static inline tb_page_addr_t get_page_addr_code(CPUState *env1, target_ulong addr)
    {
        page_index = (addr >> TARGET_PAGE_BITS) & (CPU_TLB_SIZE - 1); // 計算 GVA 對映的 TLB 索引
         mmu_idx = cpu_mmu_index(env1);
        // TLB 不命中
        if (unlikely(env1->tlb_table[mmu_idx][page_index].addr_code !=
                     (addr & TARGET_PAGE_MASK))) {
            ldub_code(addr);
        }
        // TLB 命中。檢查欲執行的位址屬於 RAM 之後,計算 GVA 對映的 HVA。
        p = (void *)(unsigned long)addr
            + env1->tlb_table[mmu_idx][page_index].addend;
        // 返回 HVA 在 RAM 中的偏移量。 
        return qemu_ram_addr_from_host(p);
    }
  3. TLB 不命中。ldub_code (softmmu_header.h) 是個透過宏展開的函式。
    static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr)
    {
        if (unlikely(env->tlb_table[mmu_idx][page_index].ADDR_READ !=
                     (addr & (TARGET_PAGE_MASK | (DATA_SIZE - 1))))) {
            // ADDR_READ 會視情況被替換成 addr_code 或是 addr_read。這裡因為存取的是 code,
             // ADDR_READ 被替換成 addr_code。 
             res = glue(glue(__ld, SUFFIX), MMUSUFFIX)(addr, mmu_idx);
        } else {
            physaddr = addr + env->tlb_table[mmu_idx][page_index].addend;
            res = glue(glue(ld, USUFFIX), _raw)((uint8_t *)physaddr);
        }
        return res;
    }
  4. ldb_cmmu (softmmu_template.h),其中的 cmmu 代表存取的是 code。如果是 mmu,代表存取的是 data。
    /* 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;
    }
    • 在 exec.c 的最後定義如下的宏,並 include softmmu_template.h 將其中的宏展開。請見 exec.i。
      #define MMUSUFFIX _cmmu // load code
      #define GETPC() NULL // tb_find_slow -> get_page_addr_code -> ldub_code -> __ldb_cmmu
      #define env cpu_single_env
      #define SOFTMMU_CODE_ACCESS
       
      #define SHIFT 0
      #include "softmmu_template.h"
       
      #define SHIFT 1
      #include "softmmu_template.h"
       
      #define SHIFT 2
      #include "softmmu_template.h"
       
      #define SHIFT 3
      #include "softmmu_template.h"
       
      #undef env

      請見 [Qemu-devel] When the tlb_fill will be called from generated code?

  5. tlb_fill (target-i386/op_helper.c)。查找頁表,如果頁存在,將該頁帶進 TLB; 如果頁不存在,發出頁缺失中斷。
    /* 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;
    }
    • retaddr 非 NULL 代表 tlb_fill 並非從 get_page_addr_code 呼叫。例如,target-i386/op_helper.c 或是 code cache。請見 op_helper.i。
      uint8_t __ldb_mmu(target_ulong addr, int mmu_idx)
      {
              retaddr = ((void *)((unsigned long)__builtin_return_address(0) - 1));
       
              tlb_fill(addr, 0, mmu_idx, retaddr);
              goto redo;
      }
  6. cpu_x86_handle_mmu_fault (target-i386/helper.c) 查找頁表。如果該頁已在內存,呼叫 tlb_set_page 將該頁寫入 TLB。
    /* 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;
    }
  7. tlb_set_page (exec.c) 填入 TLB 項。
    void tlb_set_page(CPUState *env, target_ulong vaddr, ...)
    {
        CPUTLBEntry *te;
     
        // 回傳 host virtual address
        addend = (unsigned long)qemu_get_ram_ptr(pd & TARGET_PAGE_MASK);
        // 更新 TLB 項目
         te = &env->tlb_table[mmu_idx][index];
        te->addend = addend - vaddr; // host virtual address 與 guest virtual address 的偏移量。
    }

請見 [Qemu-devel] When the tlb_fill will be called from generated code?。共有底下檔案:

  • softmmu-semi.h
  • softmmu_defs.h: 宣告 \_\_{ld,st}* 函式原型。
  • softmmu_exec.h: 利用 softmmu_header.h 生成 {ld,st}_{user,kernel,etc} 函式。{ld,st}_{user,kernel,etc} 函式又會呼叫到 \_\_{ld,st}* 函式。
  • softmmu_header.h: 生成 {ld,st}* 函式。{ld,st}* 函式又會呼叫到 \_\_{ld,st}* 函式。
  • softmmu_template.h: 生成 \_\_{ld,st}* 函式。

底下檔案定義相關函式原型。

  • softmmu_defs.h: 宣告給 TCG IR qemu_ld/qemu_st 使用的 \_\_{ld,st}* 函式原型。被 softmmu_exec.h, tcg/xxx/tcg-target.c 和 exec-all.h 所 #include。

底下檔案生成相關函式體。

  • softmmu_template.h: exec.c 和 target-*/op_helper.c 使用 softmmu_template.h 生成 \_\_{ld,st}* 函式。tcg_out_qemu_ld (tcg/xxx/tcg-target.c) 在為 qemu_ld/qemu_st 產生 host binary 時,會呼叫到 softmmu_template.h 生成 \_\_{ld,st}* 函式。
    1. tcg_out_qemu_ld (tcg/i386/tcg-target.c)。
      #include "../../softmmu_defs.h"
      // softmmu_defs.h
      // uint8_t REGPARM __ldb_mmu(target_ulong addr, int mmu_idx);
      // void REGPARM __stb_mmu(target_ulong addr, uint8_t val, int mmu_idx);
       
      // 內存讀指令。會將該指令指定的虛擬位址透過 software MMU 轉換成物理位址。
      // softmmu_template.h 會透過宏展開定義相對應的函式。  
      static void *qemu_ld_helpers[4] = {
          __ldb_mmu, // load byte
          __ldw_mmu, // load word 
          __ldl_mmu, // load long word
          __ldq_mmu, // load quad word
      };
       
      /* 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]); // 呼叫上述函式。
       
          /* 略 */
      }
    2. switch_tss (target-i386/op_helper.c)
      // #define MMU_MODE0_SUFFIX _kernel
      // #define MMU_MODE1_SUFFIX _user
      #include "cpu.h"
       
      // softmmu_exec.h 生成 {ld,st}*_{kernel,user,etc} 函式。
      // #define ACCESS_TYPE 0
      // #define MEMSUFFIX MMU_MODE0_SUFFIX
      // #define DATA_SIZE 1
      // #include "softmmu_header.h"
      #if !defined(CONFIG_USER_ONLY)
      #include "softmmu_exec.h"
      #endif /* !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);
       
          /* 略 */   
      }
  • softmmu_exec.h: target-*/op_helper.c #include softmmu_exec.h,softmmu_exec.h 再利用 softmmu_header.h 生成 {ld,st}*_{kernel,user,etc} 函式。
    • softmmu_exec.h #include softmmu_defs.h,softmmu_defs.h 定義函式原型 \_\_{ld,st},其函式體由 softmmu_template.h 實現。softmmu_exec.h 也 #include softmmu_header.h,softmmu_header.h 定義巨集生成 {ld,st}*_{kernel,user,etc} 函式。
      // softmmu_defs.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);
       
              /* 略 */
      }
    • helper_fldt (target-i386/op_helper.c)
      #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;
      }
  • softmmu_header.h: 定義巨集。被 softmmu_exec.h 和 exec-all.h #include 進而展開巨集,根據 MMU mode 和 data size 定義 inli ne ld/st 函式。
    • softmmu_exec.h: 被 target-xxx/op_helper.c 所 #include。根據 MMU mode (user/kernel) 和 data size 生成 inline ld/st 函式。
      // target-xxx/cpu.h 自行定義 MMU_MODE?_SUFFIX。
      // 以 i386 為例: _kernel,_user。
      #define MEMSUFFIX MMU_MODE1_SUFFIX
      // target-i386/op_helper.c
      #include "cpu.h"
      #include "softmmu_exec.h"
       
      // softmmu_exec.h
      #include "softmmu_defs.h"
       
      #define ACCESS_TYPE 0
      #define MEMSUFFIX MMU_MODE0_SUFFIX
      #define DATA_SIZE 1
      #include "softmmu_header.h"
       
      // softmmu_header.h
      // op_helper.i -> ldub_kernel
      // ld/st 最後會呼叫到 __ld/__st
      // kernel mode: env->tlb_table[0]
      // user mode: env->tlb_table[1]
      // data: env->tlb_table[(cpu_mmu_index(env))]
      static inline RES_TYPE glue(glue(ld, USUFFIX), MEMSUFFIX)(target_ulong ptr)
      {
      }
  1. exec-all.h。定義給 code cache 使用的 softmmu 函式。
    #include "softmmu_defs.h"
     
    // uint64_t REGPARM __ldq_mmu(target_ulong addr, int mmu_idx);
    // void REGPARM __stq_mmu(target_ulong addr, uint64_t val, int mmu_idx);
    //
    // uint8_t REGPARM __ldb_cmmu(target_ulong addr, int mmu_idx);
    // void REGPARM __stb_cmmu(target_ulong addr, uint8_t val, int mmu_idx);
     
    #define ACCESS_TYPE (NB_MMU_MODES + 1)
    #define MEMSUFFIX _code
    #define env cpu_single_env
     
    #define DATA_SIZE 1
    #include "softmmu_header.h"
     
    // softmmu_header.h
    #elif ACCESS_TYPE == (NB_MMU_MODES + 1)
     
    #define CPU_MMU_INDEX (cpu_mmu_index(env))
    #define MMUSUFFIX _cmmu
     
    #else
     
    // 生成 ldub_cmmu -> __ldb_cmmu
    // env->tlb_table[(cpu_mmu_index(env))]
  • cpu-exec.i: {ld, st}{sb, ub}_{kernel, user, data, p} p: 直接讀。

QEMU softmmu 有幾處可以加速2)3)4)

System Call

TLB

以 x86 為例,有幾種情況會呼叫 tlb_flush。

  1. cpu_x86_update_crN。在 target-i386/translate.c 中,遇到 mov reg, crN 或是 mov crN, reg 會呼叫 helper_write_crN (target-i386/op_helper.c),helper_write_crN 再視情況呼叫 cpu_x86_update_crN (target-i386/helper.c)。Control register
  2. cpu_register_physical_memory_log
  3. cpu_reset
  4. cpu_x86_set_a20
  1. tlb_flush (exec.c)。
    void 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++;
    }
  2. 這時 QEMU 被迫以 tb_find_slow 改以 GPA 查找是否有以翻譯過的 TranslationBlock,同時會進行額外檢查。注意! 這時候有可能會重翻!
    static TranslationBlock *tb_find_slow(CPUState *env, ...)
    {
        for(;;) {
            tb = *ptb1;
            if (!tb)
                goto not_found;
            if (tb->pc == pc &&
                tb->page_addr[0] == phys_page1 &&
                tb->cs_base == cs_base &&
                tb->flags == flags) {
                /* 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;
        }
    }

User Mode

可參考底下文件,

  1. main (linux-user/main.c) 為進入點。
    int main(int argc, char **argv, char **envp)
    {
        // 初始化 TCG
        cpu_exec_init_all(0);
     
        // 初始化 CPUState
        env = cpu_init(cpu_model);
     
        // 傳遞環境變數和命令行參數給 guest 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);
    }
    • cpu_exec_init_all 初始化 TCG。
      void cpu_exec_init_all(unsigned long tb_size)
      {
          // 呼叫 tcg_context_init (tcg/tcg.c) 初始化 TCG。tcg_context_init
          // 再呼叫 tcg_target_init (tcg/i386/tcg-target.c) 針對宿主機做相應的設置。
          cpu_gen_init();
          code_gen_alloc(tb_size); // 使用 mmap 將 code_gen_prologue 和 code_gen_buffer 設為可讀、可寫和可執行。
          code_gen_ptr = code_gen_buffer;
          page_init(); // 扫描 /proc/self/maps。針對每個頁調用 page_set_flags。
      }
    • page_init (exec.c)。
      static void page_init(void)
      {
              last_brk = (unsigned long)sbrk(0); // 取得目前 QEMU brk 指針,brk 指向堆 (heap) 的頂端,代表程序資料段的結尾。
       
              f = fopen("/compat/linux/proc/self/maps", "r"); // 取得 QEMU 虛擬內存的映象。
              if (f) {
                  mmap_lock();
       
                  do {
                      // 掃描 maps 的內容,透過 h2g (cpu-all.h) 將 host addr 轉換成 guest addr,透過減去 guest addr base。
                         // 並檢查 guest addr 是否落在 guest addr space。
       
                          // page_set_flags 檢視該頁面是否被寫入。若被寫入,則清空所有與該頁面有關的 TB。這裡只是要建該頁面的 PageDesc。
                          page_set_flags(startaddr, endaddr, PAGE_RESERVED); 
                  } while (!feof(f));
             }
      }
  2. cpu_init 依據不同的 guest CPU 型號進行初始化。如 x86,則呼叫 cpu_x86_init (target-i386/helper.c)。
  3. init_task_state (linux-user/main.c) 每個 guest process 都有一個 TaskState
    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;
    }
  4. loader_exec (linux-user/linuxload.c) 載入 guest program。檢查其執行檔格式和權限。
    • image_info
      // 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;
          }
      }
    • load_elf_binary (linux-user/elfload.c) 將 guest binary 載入成為 image。
      • Dynamic Linking
        int load_elf_binary(struct linux_binprm * bprm, struct target_pt_regs * regs,
                            struct image_info * info)
        {
            // 將 guest binary 載入成為 image。
            load_elf_image(bprm->filename, bprm->fd, info,
                           &elf_interpreter, bprm->buf);
         
            // 如果該 guest binary 為動態連結,載入 dynamic linker。透過檢查 guest binary 的 program header 是否帶有 PT_INTERP。
            if (elf_interpreter) {
                load_elf_interp(elf_interpreter, &interp_info, bprm->buf);
            }
         
            bprm->p = create_elf_tables(bprm->p, bprm->argc, bprm->envc, &elf_ex,
                                        info, (elf_interpreter ? &interp_info : NULL));
            info->start_stack = bprm->p;
         
            // 將進入點設為 dynamic linker。 
            if (elf_interpreter) {
                info->load_bias = interp_info.load_bias;
                info->entry = interp_info.entry;
                free(elf_interpreter);
            }
        }
    • create_elf_tables (linux-user/elfload.c)
      static abi_ulong create_elf_tables(abi_ulong p, int argc, int envc,
                                         struct elfhdr *exec,
                                         struct image_info *info,
                                         struct image_info *interp_info)
      {
          info->saved_auxv = sp;
       
          sp = loader_build_argptr(envc, argc, sp, p, 0);
          return sp;
      }
    • loader_build_argptr (linux-user/linuxload.c) 在 guest (target) 的棧上放置環境變數和命令行參數。put_user_ual (linux-user/qemu.h) 用來將資料搬移至 guest 內存。
      /* 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;
          }
       
      }
  5. syscall_init (linux-user/syscall.c) IOCTLEntry
     
  6. signal_init (linux-user/signal.c) 會處理 guest 跟 host 之間 signal 如何對映和處理。
    // 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);
        }
    }
  7. 設定 virtual CPU 暫存器、中斷描述符表、全域描述符表和段描述符。
    static uint64_t *idt_table;
     
    static void set_gate(void *ptr, unsigned int type, unsigned int dpl,
                         uint32_t addr, unsigned int sel)
    {
        uint32_t *p, e1, e2;
        e1 = (addr & 0xffff) | (sel << 16);
        e2 = (addr & 0xffff0000) | 0x8000 | (dpl << 13) | (type << 8);
        p = ptr;
        p[0] = tswap32(e1);
        p[1] = tswap32(e2);
    }
     
    static void set_idt(int n, unsigned int dpl)
    {
        set_gate(idt_table + n, 0, dpl, 0, 0);
    }
  8. cpu_loop (linux-user/main.c) 為主要執行迴圈,呼叫 cpu_x86_exec 進行 guest binary → TCG → host binary 並執行的流程。cpu_x86_exec/cpu_exec (cpu-exec.c) 會返回例外號,cpu_loop 視例外號的不同做相對應的處理,之後處理訊號。不同架構定義不同的 cpu_loop。以 x86 為例,
    void cpu_loop(CPUX86State *env)
    {
        int trapnr;
        abi_ulong pc;
        target_siginfo_t info;
     
        for(;;) {
            trapnr = cpu_x86_exec(env);
            switch(trapnr) {
            case 0x80:
                /* 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
        }
    }

System Call

QEMU user mode 在處理系統呼叫時,不像 system mode 需要翻譯 system call handler。

  1. cpu_loop (linux-user/main.c)。
    void cpu_loop(CPUX86State *env)
    {
        int trapnr;
        abi_ulong pc;
        target_siginfo_t info;
     
        for(;;) {
            trapnr = cpu_x86_exec(env);
            switch(trapnr) {
            case 0x80:
                /* 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;
     
             ... 略 ...
         }
     
         ... 略 ...
     
    }
  2. do_syscall (linux-user/syscall.c) 將客戶 system call 做些包裝之後,轉交給宿主 system call。
    abi_long do_syscall(void *cpu_env, int num, abi_long arg1,
                        abi_long arg2, abi_long arg3, abi_long arg4,
                        abi_long arg5, abi_long arg6, abi_long arg7,
                        abi_long arg8)
    {
        switch(num) {
     
        ... 略 ...
     
        case TARGET_NR_open:
            if (!(p = lock_user_string(arg1)))
                goto efault;
            ret = get_errno(do_open(cpu_env, p,
                                    target_to_host_bitmask(arg2, fcntl_flags_tbl),
                                    arg3));
            unlock_user(p, arg1, 0);
            break;
     
        ... 略 ...
     
        }
     
        ... 略 ...
    }
  3. do_open (linux-user/syscall.c)。
    static int do_open(void *cpu_env, const char *pathname, int flags, mode_t mode)
    {
        // 前置處理。
     
        // 呼叫 host system call。
        return get_errno(open(path(pathname), flags, mode));
    }

Interrupt & Exception Handling

以 x86 為例,

  • cpu-defs.h 定義例外號。
    #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) */
  • 有些函式前面會加上 QEMU_NORETURN (compiler.h),這代表該函式不會返回。
    #define QEMU_NORETURN __attribute__ ((__noreturn__))
  1. cpu_exec (cpu-exec.c)
    int cpu_exec(CPUState *env)
    {
        if (env->halted) { // system mode 才會拉起 env->halted。
            if (!cpu_has_work(env)) {
                return EXCP_HALTED;
            }
     
            env->halted = 0;
        }
     
        cpu_single_env = env; // 保存當前 env。待 lonjmp 時,可以用 cpu_single_env 回復 env。
     
        if (unlikely(exit_request)) {
            env->exit_request = 1;
        }
     
    // 不同架構會有不同前置處理。
    #if defined(TARGET_I386)
        CC_SRC = env->eflags & (CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);
        DF = 1 - (2 * ((env->eflags >> 10) & 1));
        CC_OP = CC_OP_EFLAGS;
        env->eflags &= ~(DF_MASK | CC_O | CC_S | CC_Z | CC_A | CC_P | CC_C);
    #else
    #error unsupported target CPU
    #endif
        // cpu_exec 返回值即為 env->exception_index。以 process mode 為例,cpu_loop 在呼叫 cpu_exec 之後,會檢視其返回值並做相應處理。 
        env->exception_index = -1;
     
        // 進行翻譯並執行的迴圈。
        /* 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(;;) */
    }
    • QEMU 支持 precise exception。當例外發生時,執行流程會將舊的 env (cpu_single_env) 存回 env。
  2. 外層迴圈。cpu_exec 利用 setjmp/longjmp 處理例外。cpu_loop_exit (cpu-exec.c) 和 cpu_resume_from_signal (cpu-exec.c) 會呼叫 longjmp 回到 setjmp 設定的例外處理分支。5)
        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 處理例外 */
    }
    • cpu_loop_exit (cpu-exec.c)
      void cpu_loop_exit(CPUState *env)
      {
          env->current_tb = NULL;
          longjmp(env->jmp_env, 1);
      }
    • cpu_resume_from_signal (cpu-exec.c)
      /* 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
  3. user mode 和 system mode 有不同處置。
    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;
    }
    • user mode。
      • General protection fault
        #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
      • raise_exception_err (target-i386/op_helper.c) 轉呼叫 raise_interrupt (target-i386/op_helper.c)。
        static void QEMU_NORETURN raise_exception_err(int exception_index,
                                                      int error_code)
        {
            raise_interrupt(exception_index, 0, error_code, 0);
        }
      • raise_interrupt (target-i386/op_helper.c) 最後呼叫 cpu_loop_exit (cpu-exec.c) longjmp 回外層迴圈 else 分支。
        /*
         * 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);
        }
    • system mode。
      • do_interrupt_all (target-i386/op_helper.c)。
        static void do_interrupt_all(int intno, int is_int, int error_code,
                                     target_ulong next_eip, int is_hw)
        {
            ... 略 ...
         
            // 檢查當前處於何種模式,交由相對應的函式處理。如果客戶機是 64 bit,還有 do_interrupt_64。
            if (env->cr[0] & CR0_PE_MASK) {
                if (env->hflags & HF_SVMI_MASK)
                    handle_even_inj(intno, is_int, error_code, is_hw, 0);
                {
                    do_interrupt_protected(intno, is_int, error_code, next_eip, is_hw);
                }
            } else {
                do_interrupt_real(intno, is_int, error_code, next_eip);
            }
         
            ... 略 ...
        }
      • 內核態與用戶態分別有內核棧和用戶棧。
      • do_interrupt_real
        static void do_interrupt_real(int intno, int is_int, int error_code,
                                      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;
        }
  4. 內層迴圈。
                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

Precise exception 要求當某一條 guest 指令發生例外,例如溢位或是頁缺失,此條指令之前的運算必須完成,此條指令之後的運算必須捨棄。當 QEMU 在 code cache 中執行翻譯過後的指令,其中發生例外時,QEMU 必須要能維護例外發生之前 guest 的暫存器和內存內容,並能反查出發生例外的 basic block 相對映開頭的 guest pc。從那個 guest pc 重新開始翻譯直到發生例外的位址,並把這個 guest pc 存回 CPUState,這是因為 QEMU 不會執行完一條 guest 指令就更新 guest pc。

  1. 暫存器: 在有可能發生例外的運算之前,如: guest load/store 或是呼叫 helper function,QEMU 會把 dirty CPUState 寫回內存。
  2. 內存: QEMU 不會將原本指令亂序。

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。

  1. exec-all.h。定義給 code cache 使用的 softmmu 函式。
    // __builtin_return_address 返回當前函式 (參數 0) 的返回位址。
    #else
    # define GETPC() ((void *)((unsigned long)__builtin_return_address(0) - 1))
    #endif
     
    #include "softmmu_defs.h"
     
    #define ACCESS_TYPE (NB_MMU_MODES + 1)
    #define MEMSUFFIX _code
    #define env cpu_single_env
     
    #define DATA_SIZE 1
    #include "softmmu_header.h"
     
    #define DATA_SIZE 2
    #include "softmmu_header.h"
     
    #define DATA_SIZE 4
    #include "softmmu_header.h"
     
    #define DATA_SIZE 8
    #include "softmmu_header.h"
     
    #undef ACCESS_TYPE
    #undef MEMSUFFIX
    #undef env
     
    #endif
  2. exec.c。定義給一般 C 函式使用的 softmmu 函式。
    #define MMUSUFFIX _cmmu
    #undef GETPC
    #define GETPC() NULL
    #define env cpu_single_env
    #define SOFTMMU_CODE_ACCESS
     
    #define SHIFT 0
    #include "softmmu_template.h"
     
    #define SHIFT 1
    #include "softmmu_template.h"
     
    #define SHIFT 2
    #include "softmmu_template.h"
     
    #define SHIFT 3
    #include "softmmu_template.h"
     
    #undef env
     
    #endif
     
    static inline void svm_load_seg(target_phys_addr_t addr, SegmentCache *sc)
    {
        unsigned int flags;
     
        sc->selector = lduw_phys(addr + offsetof(struct vmcb_seg, selector));
        sc->base = ldq_phys(addr + offsetof(struct vmcb_seg, base));
        sc->limit = ldl_phys(addr + offsetof(struct vmcb_seg, limit));
        flags = lduw_phys(addr + offsetof(struct vmcb_seg, attrib));
        sc->flags = ((flags & 0xff) << 8) | ((flags & 0x0f00) << 12);
    }
  1. tlb_fill。env 是 dyngen-exec.h 中定義的全域變數,是一個指向 CPUState 的指針,該指針存放在宿主機特定的暫存器 7)
    #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
    • tb_find_slow → get_page_addr_code → ldub_code → \_\_ldb_cmmu → tlb_fill。此時,retaddr 為 NULL,代表是從一般 C 函式呼叫到 tlb_fill。
    • code cache → \_\_ldl_mmu → tlb_fill。此時,retaddr 不為 NULL,代表是從 code cache 呼叫到 tlb_fill,guest binary 通常在做 guest memory operation。
  2. 透過 tb_find_pc 以發生例外 host binary (retaddr) 位址反查 TranslationBlock。
    /* 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。
    }
  3. cpu_restore_state (translate-all.c) 重翻該 TranslationBlock (guest binary → TCG IR → host binary),同時附帶上 guest pc 資訊。searched_pc 為發生例外的 host binary 位址。
    /* 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;
    }
    1. gen_intermediate_code_pc (target-i386/translate.c) 轉呼叫 gen_intermediate_code_internal,search_pc 為真。將 guest binary 翻成 TCG IR 的同時,紀錄下 guest pc。
      static inline void gen_intermediate_code_internal(CPUState *env,
                                                        TranslationBlock *tb,
                                                        int search_pc)
      {
          for(;;) {
              if (search_pc) {
                  // gen_opc_ptr 為 TCG opcode buffer 目前位址,gen_opc_buf 為 TCG opcode buffer。
                  j = gen_opc_ptr - gen_opc_buf;
                  if (lj < j) {
                      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;
          }
      }
    2. tcg_gen_code_search_pc (tcg/tcg.c) 轉呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 翻成 host binary。search_pc 應該改叫 offset。
      /* 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++;
          }
      }
    3. restore_state_to_opc (target-i386/translate.c)
      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;
      }
  4. raise_exception_err (target-i386/op_helper.c)
    static void QEMU_NORETURN raise_exception_err(int exception_index,
                                                  int error_code)
    {
        raise_interrupt(exception_index, 0, error_code, 0);
    }
  5. raise_interrupt (target-i386/op_helper.c)
    /*
     * 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);
    }
    • 這裡操作的 env 是 global env,在 tlb_fill 已經同步成當前 env。
  6. helper_svm_check_intercept_param (target-i386/op_helper.c)
  7. cpu_loop_exit (cpu-exec.c)
    void cpu_loop_exit(CPUState *env)
    {
        env->current_tb = NULL;
        longjmp(env->jmp_env, 1);
    }

以 linux-0.11 為例,

(gdb) b gen_intermediate_code_internal if tb->pc == 0xe4c0
(gdb) r -m 1024M -boot a -fda linux-0.11/Image -hda rootfs/hdc-0.11-new.img -vnc 0.0.0.0:1
  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
  2. ls0x4011809f 位在 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
  • 執行 \_\_stb_mmu 的時候,會呼叫 tlb_fill。tlb_fill 呼叫 cpu_x86_handle_mmu_fault 查找客戶機頁表,若該頁存在,將其頁表項填入 tlb; 否則觸發頁缺失例外。

數個 tb 可能會相對應 (不同進程的) guest pc。

Breakpoint 1, gen_intermediate_code_internal (env=0x110e5d0, tb=0x7fffe75d2eb0, search_pc=0) at /tmp/chenwj/qemu-0.13.0/target-i386/translate.c: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 Flow

請閱讀以下文章:

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 動態翻譯流程中會使用到的緩衝區:

  1. gen_opc_buf 和 gen_opparam_buf (translate-all.c) 是 TCG IR 所在位置。
    uint16_t gen_opc_buf[OPC_BUF_SIZE]; // 放置 TCG opcode。
    TCGArg gen_opparam_buf[OPPARAM_BUF_SIZE]; // 放置 TCG opcode 會用到的 operand。
  2. 如果使用靜態配置的緩衝區,static_code_gen_buffer (exec.c) 是 translation block code cache (host binary) 所在位置。
    #ifdef USE_STATIC_CODE_GEN_BUFFER
    static uint8_t static_code_gen_buffer[DEFAULT_CODE_GEN_BUFFER_SIZE]
                   __attribute__((aligned (CODE_GEN_ALIGN)));
    #endif
  3. 在跳入/出 code cache 執行之前/後,要執行 prologue/epilogue。
    // 根據不同平台,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)

  • tcg_prologue_init (tcg/tcg.c) → tcg_target_qemu_prologue (tcg/i386/tcg-target.c)
    • 進 translation block code cache 之前和之後,需要執行 prologue 和 epilogue。請參考 Calling conventionWhich x86 registers to save in C functions?
      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
      }
  • cpu_loop (linux-user/main.c) → cpu_x86_exec/cpu_exec (cpu-exec.c) → tb_find_fast (cpu-exec.c)
    • tb_find_fast (cpu-exec.c) → tb_find_slow (cpu-exec.c) → tb_gen_code (exec.c) → cpu_gen_code (translate-all.c) → gen_intermediate_code (target-i386/translate.c) → tcg_gen_code (tcg/tcg.c) → tcg_gen_code_common (tcg/tcg.c)
    • tcg_gen_code_common (tcg/tcg.c) → tcg_reg_alloc_op (tcg/tcg.c) → tcg_out_op (tcg/i386/tcg-target.c)
  1. cpu_exec (cpu-exec.c) 為主要執行迴圈。cpu_exec_nocache 是在 system mode 啟用 icount 的時候才會被用到
    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_find_fast 會以 pc (虛擬位址)試圖尋找已翻譯過的 translation block。
      tb = env->tb_jmp_cache[tb_jmp_cache_hash_func(pc)];

      如果失敗,則呼叫 tb_find_slow 以 pc (cs + eip) 對映的物理位址尋找 TB。如果成功,則將該 TB 寫入 tb_jmp_cache; 若否,則進行翻譯。

      not_found:
        /* 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;
  2. tb_gen_code 配置內存給 TB,再交由 cpu_gen_code。
    // 注意! 這裡會將虛擬位址轉成物理位址。phys_pc 將交給之後的 tb_link_page 使用。
    // get_page_addr_code 在 process mode 直接返回 pc; system mode 則透過查找
    // env 的 tlb_table 返回 GPA 在客戶機內存中的偏移量。
    phys_pc = get_page_addr_code(env, pc);
    tb = tb_alloc(pc);
    if (!tb) {
      // 清空 code cache
    }
     
    // 初始 tb
     
    cpu_gen_code(env, tb, &code_gen_size); // 開始 guest binary -> TCG IR -> host binary 的翻譯。
     
    // 將 tb 加入 tb_phys_hash 和二級頁表 l1_map。
    // phys_pc 和 phys_page2 分別代表 tb (guest pc) 對映的物理位址和所屬的第二個頁面 (如果 tb 代表的 guest binary 跨頁面的話)。
    tb_link_page(tb, phys_pc, phys_page2);
    return tb; 
    • cpu_gen_code 負責 guest binary → TCG IR → host binary 的翻譯。
      tcg_func_start(s); // 初始 gen_opc_ptr 和 gen_opparam_ptr  
       
      gen_intermediate_code(env, tb); // 呼叫 gen_intermediate_code_internal 產生 TCG IR
       
      gen_code_size = tcg_gen_code(s, gen_code_buf); // TCG IR -> host binary
      • tcg_func_start (tcg/tcg.c)
        void tcg_func_start(TCGContext *s)
        {
            int i;
            tcg_pool_reset(s);
            s->nb_temps = s->nb_globals;
            for(i = 0; i < (TCG_TYPE_COUNT * 2); i++)
                s->first_free_temp[i] = -1;
            s->labels = tcg_malloc(sizeof(TCGLabel) * TCG_MAX_LABELS);
            s->nb_labels = 0;
            s->current_frame_offset = s->frame_start;
         
            gen_opc_ptr = gen_opc_buf;
            gen_opparam_ptr = gen_opparam_buf;
        }
    • gen_intermediate_code_internal (target-*/translate.c) 初始化並呼叫 disas_insn 反組譯 guest binary 成 TCG IR。
      • disas_insn 呼叫 tcg_gen_xxx (tcg/tcg-op.h) 產生 TCG IR。分別將 opcode 寫入 gen_opc_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opc_buf); operand 寫入 gen_opparam_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opparam_buf )。
    • tcg_gen_code (tcg/tcg.c) 呼叫 tcg_gen_code_common (tcg/tcg.c) 將 TCG IR 轉成 host binary。
      tcg_reg_alloc_start(s);
       
      s->code_buf = gen_code_buf;
      s->code_ptr = gen_code_buf; // host binary 會寫入 TCGContext s 的 code_ptr 所指向的緩衝區。  
    • tb_link_page (exec.c) 把新的 TB 加進 tb_phys_hash 和 l1_map 二級頁表。tb_find_slow 會用 pc 對映的物理位址的哈希值索引 tb_phys_hash (存放 TranslationBlock *) 取得下一個 TB。
      /* 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);
      • tb_alloc_page 建立新的 PageDesc。(exec.c)。
        static inline void tb_alloc_page(TranslationBlock *tb,
                                         unsigned int n, tb_page_addr_t page_addr)
        {
          // 代表 tb (guest binary) 所屬頁面。
          tb->page_addr[n] = page_addr;
          // 在 l1_map 中配置一個 PageDesc,返回該 PageDesc。
          p = page_find_alloc(page_addr >> TARGET_PAGE_BITS, 1);
          tb->page_next[n] = p->first_tb; // 將該頁面目前第一個 TB 串接到此 TB。將來有需要將某頁面所屬所有 TB 清空。
          // n 為 1 代表 tb 對應的 guest binary 跨 page。
          p->first_tb = (TranslationBlock *)((long)tb | n);
          invalidate_page_bitmap(p);
        }
      • tb_reset_jump 呼叫 tb_set_jmp_target 使得該 TB 不會鏈結到其它 TB。根據是否使用 direct jump 做 block chaining 與否,tb_set_jmp_target 直接修改 TB (code cache) 跳躍目標或是 TB 的 tb_next。
  3. tb_find_fast/slow 傳回 translation block 後,交給 tcg_qemu_tb_exec 執行。
    next_tb = tcg_qemu_tb_exec(tc_ptr);
    • tcg_qemu_tb_exec 被定義在 tcg/tcg.h。
      // code_gen_prologue(tb_ptr) is casted to a function with one parameter,
      // in such a way, execute host machine code stored in code_gen_prologue[]
      #define tcg_qemu_tb_exec(tb_ptr) ((long REGPARM (*)(void *))code_gen_prologue)(tb_ptr)

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

有幾種情況會需要打斷 code cache 的執行,將控制權將還給 QEMU。

  • cpu_interrupt: 虛擬外設會發出中斷,最後透過 apic_local_deliver (hw/apic.c) 呼叫 cpu_interrupt 發出中斷給虛擬 CPU。QEMU 0.15 將system mode 和 process mode 的 cpu_interrupt 分離,請見 cpu-all.h 和 exec.c。
  • cpu_exit: system mode 會註冊 host alarm signal handler,該 handler 會呼叫 cpu_exit。其它諸如 gdb stub,DMA,IO thread 都會呼叫到 cpu_exit。

針對 self-modifying code 或是 JIT,在內存生成代碼之後通常需要清空快取9)

Block Chaining

請閱讀一下文章

為了盡量在 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);
  • tb_add_jump 呼叫 tb_set_jmp_target 做 block chaining 的 patch。另外,會利用 tb 的 jmp_next 和 tb_next 的 jmp_first 把 block chaining 中的 tb 串成一個 circular list。這邊要注意到,QEMU 利用 TranslatonBlock 位址後兩位必為零的結果做了一些手腳。(exec-all.h)。
    // block chaining 方向為: tb -> tb_next。n 用來指示 tb 條件分支的方向。
    static 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));
        }
    }
  • 依據是否採用 direct jump,tb_set_jmp_target (exec-all.h) 有不同做法。採用 direct jump 的話,tb_set_jmp_target 會根據 host 呼叫不同的 tb_set_jmp_target1。tb_set_jmp_target1 會用到 TB 的 tb_jmp_offset。如果不採用 direct jump 做 block chaining,tb_set_jmp_target 會直接修改 TB 的 tb_next。
    • tb_set_jmp_target (exec-all.h)。
      static inline void tb_set_jmp_target(TranslationBlock *tb,
                                           int n, unsigned 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);
      }
    • tb_set_jmp_target1 (exec-all.h)。
      #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);
    }
}
  • tcg_gen_goto_tb 生成 TCG IR。
    static inline void tcg_gen_goto_tb(int idx)
    {   
        tcg_gen_op1i(INDEX_op_goto_tb, idx);
    }
  • tcg/i386/tcg-target.c 將 TCG IR 翻成 host binary。注意! 這邊利用 patch jmp 跳轉位址達成 block linking。
    static inline void tcg_out_op(TCGContext *s, TCGOpcode opc,
                                  const TCGArg *args, const int *const_args)
    {
        case INDEX_op_goto_tb:
            if (s->tb_jmp_offset) {
                /* 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;
  • 遇到 guest binary 中的條件分支和直接跳轉都會呼叫 gen_goto_tb (target-i386/translate.c)。以 i386 直接跳轉為例:
    static void gen_jmp_tb(DisasContext *s, target_ulong eip, int tb_num)
    {
        if (s->jmp_opt) { // 使用 direct jump 實現 block chaining
            gen_update_cc_op(s);
            gen_goto_tb(s, tb_num, eip); // tb_num 指示目前 tb 分支方向,eip 是下一個 tb 位址。
            s->is_jmp = DISAS_TB_JUMP;
        } else {
            gen_jmp_im(eip);
            gen_eob(s);
        }
    }
     
    static 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

底下幾種情況會做 block unchaining。請見以下討論,

  1. cpu_exit (exec.c) → cpu_unlink_tb(exec.c)。當某些情況需要 QEMU 從 code cache 中跳離出來處理某些事情時,會呼叫到 cpu_exit。例如: Host SIGALRM, DMA, IO Thread, Single step。之所以要將 tb unlink,是不希望一直在 code cache 中執行,需要早點離開 code cache 處理事情。
    void cpu_exit(CPUState *env)
    {
        env->exit_request = 1; // 拉起 env->exit_request。
        cpu_unlink_tb(env); // 將 env 的 code cache 中的 tb unlink。
    }
    • cpu_signal。IOThread 完成 IO 之後,透過 cpu_signal 通知 QEMU。
      #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
    • 在 cpu_exec (cpu-exec.c) 中會檢查 env→exit_request。
      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 開頭。
      }
  2. cpu_interrupt (exec.c) → cpu_unlink_tb (exec.c)。虛擬外設透過 cpu_interrupt (exec.c) 發出中斷。QEMU 0.15 改叫 tcg_handle_interrupt。
    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);
    }
    • cpu_unlink_tb (exec.c) → tb_reset_jump_recursive。
      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);
      }
    • tb_reset_jump_recursive 呼叫 tb_reset_jump_recursive2 打斷 tb 的 block chaining。
      static void tb_reset_jump_recursive(TranslationBlock *tb)
      {
          tb_reset_jump_recursive2(tb, 0);
          tb_reset_jump_recursive2(tb, 1);
      }
    • tb_reset_jump_recursive2 清除 jmp_first list (TranslationBlock),再呼叫 tb_reset_jump 和 tb_reset_jump_recursive 重設 code cache 中的 jmp address,打斷 block chaining。
      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]));
      }
  3. tb_phys_invalidate 會呼叫 tb_jmp_remove 做 unchain。
    • tb_phys_invalidate。
      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 再次指向自己
      }
    • tb_jmp_remove 將該 tb (TranslationBlock) 移出 circular list。
      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;
          }
      }

Self-modifying Code

以 Linux 上的 self modifying code 為例 (請見 Self-modifying code),主要是利用 mprotect 修改頁面權限,修改其中代碼。QEMU 從兩個方向偵測 self-modifying code。

  1. 在呼叫 do_syscall 執行 mprotect 系統呼叫時進行相應的檢查。
    • main → cpu_loop → do_syscall → target_mprotect → page_set_flags (如果遇到 SMC,把相應的 tb 沖掉) → tb_invalidate_phys_page (只有 process mode 有定義此函式) → tb_phys_invalidate
      /* 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;
          }
      }
  2. tb_invalidate_phys_page_fast (exec.c) 會先檢查客戶機頁面是否有 bitmap。
    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);
        }
    }
  3. tb_invalidate_phys_page (exec.c)。
    #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;
     
    }
    • 最終會呼叫到 tb_phys_invalidate。
      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 再次指向自己
      }
    • tb_jmp_remove 將該 tb 移出 circular lists‎。
      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;
          }
      }
  4. 當宿主機發出 SIGSEGV 給 QEMU 時,QEMU 會檢視該 signal 並做相應處理。
    • host_signal_handler (linux-user/signal.c) → cpu_x86_signal_handler (user-exec.c) → handle_cpu_signal (user-exec.c) → page_unprotect (exec.c) → tb_invalidate_phys_page (exec.c)
      static inline int handle_cpu_signal(unsigned long pc, unsigned long address, ...)
      {
          if (is_write && page_unprotect(h2g(address), pc, puc)) {
              return 1;
          }
      }
    • page_unprotect 將該 guest page 相應的 tb 清掉,將該內存區段設成可寫。
      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

  1. \_\_stl_mmu 中存取 IO 的路徑不完全是 MMIO10)
     
  2. 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, &current_pc, &current_cs_base,
                                         &current_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
    }
  • cpu_get_tb_cpu_state (target-i386/cpu.h)。
    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));
    }
  • mem_io_pc (cpu-defs.h)。
    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

TCG IR 分為底下幾類:

  • 暫存器移動: mov, movi
  • 邏輯運算: and, or, xor, shl, shr, …
  • 算術運算: add, sub, mul, div, …
  • 內存操作: qemu_ld, qemu_st。客戶代碼中的內存操作,這裡會透過 mmu (tlb) 做 guest virtual addr 到 guest physical addr 的轉換。Re: [Qemu-devel] When the tlb_fill will be called from generated code?
    mov %eax, 0x4(%ebx)
  • QEMU 內部內存操作: ld, st。QEMU 存取 CPUState 之用。
    movi 0x8000000, 0x20(%r14)          # env->eip = 0x8000000
  • 分支指令: jmp, br, brcond
  • Helper function: call。呼叫 helper function。
  • 其它: exit_tb, end。

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。

  1. tcg/tcg.c 定義了最基本的函式供 tcg/xxx/tcg-target.c 使用。例如 tcg_out8 是將其參數 (8 bit) 寫入 host binary 緩衝區。
  2. tcg/xxx/tcg-target.c 定義利用 tcg/tcg.c 提供的基本函式客製化自己的 tcg_out_xxx。
  3. 在 TCG 裡提到的 target 都是指宿主機 11)

在 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?

  1. softmmu_exec.h: target-*/exec.h 使用 softmmu_exec.h 生成 {ld,st}*_{kernel,user,etc} 函式。
  2. softmmu_template.h: exec.c 和 target-*/op_helper.c 使用 softmmu_template.h 生成 __{ld,st}* 函式。
  1. tcg_gen_exit_tb 呼叫 tcg_gen_op1i 生成 TCG IR,其 op 為 INDEX_op_exit_tb,operand 為 val。
    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);
    }
  2. tcg/xxx/tcg-target.c 根據 TCG IR 產生對應 host binary。以 i386 為例:
    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;
    }
    • tcg_out_movi 將 arg 移至 ret 代表的暫存器。
      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);
          }
      }
    • tcg_out_modrm 是 x86 上對 opcode 的 extension。
      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));
      }
  1. tcg_gen_goto_tb。
    static inline void tcg_gen_goto_tb(int idx)
    {
        tcg_gen_op1i(INDEX_op_goto_tb, idx);
    }
  1. tcg_gen_code_common 會依據 TCG IR 呼叫不同的函式分配暫存器,tcg_reg_alloc_op (tcg/tcg.c) 是其中之一。
     
  2. 不論 TLB 命中與否,都會呼叫 save_globals (tcg/tcg.c) 將 CPUState 寫回。
    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);
        }
    }
  3. tcg_out_tlb_load 查詢 guest virtual pc 是否在 TLB 內已有項目。
    // 用 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);
    }
  4. /* 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

Register Allocation

  • 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;
    • 在不同宿主機上,暫存器分配有不同限制。以 x86 為例,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 */
     
        ... 略 ...
    };
  1. 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;
    }
    }
  2. 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

icount 只有在 system mode 下有作用。以 x86 為例,

  1. cpu-defs.h 定義 icount 相關資料結構。
    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;                                  
  2. gen_intermediate_code_internal (target-i386/translate.c) 在翻譯 guest binary 的前後呼叫 gen_icount_start 和 gen_icount_end 插入 icount 相關的 TCG IR,兩者只有在開啟 icount 的情況下才有作用。
    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);
    }
    • gen_icount_start (gen-icount.h)
      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);
      }
    • gen_icount_end (gen-icount.h)
      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 做為返回值。
          }
      }
  3. cpu_exec (cpu-exec.c)
                    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);
                            }
                        }
    • cpu_exec_nocache (cpu-exec.c) 執行完 tb 之後就會把它清空。
      /* 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

Coroutine 在 QEMU 上主要是為了做到多執行緒的效果,又不需要付出多執行緒所需要的開銷12)。某些函式在 QEMU 中被標記成 Coroutine,此類函式無法從一般的函式被呼叫。

static void coroutine_fn foo(void) {
  ...
}

QEMU 會使用 Coroutine 記下函式每一次的進入點,下一次呼叫會從上一次的返回點開始執行。

I/O

目前 QEMU 本身即為 IO thread 執行 main_loop_wait,當遇到 block IO 時,會 fork 出 worker thread 去處理。每一個 VCPU 均有對應的 VCPU 執行緒運行。

  1. main (vl.c)
    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;
    }
    • qemu_init_cpu_loop (cpus.c)
      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;
      }
  2. main_loop (vl.c) 是主要的執行迴圈,即 IO thread。
    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());
    }
  3. 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);
              }
          }
       
          ... 略 ...
      }
  • ioport.[ch]: port IO 不用做位址轉換
  • MMIO 需要做位址轉換: env→iotlb
  • DMA 使用物理位址。

Timer

QEMUTimer

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:

  1. rt_clock: 只有不會改變虛擬機的事物才能使用 rt_clock,這是因為 rt_clock 即使在虛擬機停止的情況下仍會運作。
  2. vm_clock: vm_clock 只有在虛擬機運行時才會運作。
  3. host_clock: 用來產生 real time source 的虛擬設備使用 host_clock。

rtc_clock 會選擇上述其中一種 clock。

外設與中斷

請見 cpu-all.h,基本上有四類通用中斷:

  1. CPU_INTERRUPT_HARD: 虛擬外設發出的中斷。
  2. CPU_INTERRUPT_EXITTB: 用於某些外設改變其內存映射時,如: A20 line change。要求虛擬 CPU 離開目前的 TB。
  3. CPU_INTERRUPT_HALT: 停止當前的虛擬 CPU。
  4. CPU_INTERRUPT_DEBUG: 除錯之用。

另外留下 CPU_INTERRUPT_TGT_EXT_* 和 CPU_INTERRUPT_TGT_INT_* 給各個 CPU 自行運用。例如: target-i386/cpu.h。

請見 QEMU's new device model qdevQEMU's device model qdev: Where do we go from here?。docs/qdev-device-use.txt。

  • hw/pc.c 一般 PC 周邊。
  • hw/irq.* 中斷之用。
  • hw/apic.c 模擬 APIC,負責發出中斷 (cpu_interrupt)。
  • hw/i8259.c 模擬 PIC。
  • hw/i8254.c 模擬時鐘。

QOM (Qemu Object Model) 用來取代 QDev 13)

虛擬外設發出的 IRQ 以 IRQState 包裝。在 QEMU 中,所有的设备包括总线,桥,一般设备都对应一个设备结构。總線,如 PCI 總線,在 QEMU 中包裝成 PCIBus; 橋,如 PCI 橋,在 QEMU 中包裝成 PCIBridgePCIDeviceInfo

  • pc_init_pci (hw/pc_piix.c) 呼叫 pc_init1 (hw/pc_piix.c) 進行 PC 機器的初始化。
    /* 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);
        }
     
        /* 初始化其它外設 */
    }
    • i8259 (PIC) 請見 PicState2PicState。請見 8259 PICIntel 8259
      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);
      }
    • i440fx_init → i440fx_common_init。請見 Intel 440FXPCI IDE ISA Xcelerator
      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);
}
  1. 最後由 apic_local_deliver (Local APIC) 呼叫 cpu_interrupt 送出中斷給 virtual CPU。

Floppy

KVM

QOM

Watchpoint

  1. 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 狀態有變化時會調用的函式。
  2. 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, &notdirty_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);
    }
  3. 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;
            }
        }
    }
  4. 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);
}
  1. 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));
                }
            }
        }
    }

GDB Stub

  • gdb_handle_packet 處理 client 送來的 request。
    static int gdb_handle_packet(GDBState *s, const char *line_buf)
    {
    }

重要檔案

  • linux-user/main.c - qemu user main
  • exec.c - virtual page mapping and translated block handling
  • cpu-exec.c- i386 emulator main execution loop
  • target-i386/translate.c - i386 translation
  • tcg/tcg.c - Tiny Code Generator for QEMU
    • tcg_out_xxx 負責將參數 (host binary) 寫入 TCGContext 所指的 code cache。
  • tcg/tcg-op.h - 提供產生 TCG IR 的函式,opcode 寫入 gen_opc_ptr 指向的緩衝區 (translate-all.c 裡的 gen_opc_buf); operand 寫入 gen_opparam_ptr 指向的緩衝區。
    • tcg_gen_xxx 產生 TCG IR。
  • target-i386/op_helper.c - Code snippets called from TCG generated code. Implement more complex operations that gcc gets better than TCG.
  • target-i386/helper.c - Helper functions specific to the CPU, but called from multiple places around QEMU. For example the MMU code belongs here.
  • target-i386/cpu.h 會引用 cpu-defs.h 定義個平台共用的資料結構
  • cpu_x86_exec/cpu_exec 代表呼叫的是 cpu_x86_exec,實際上被呼叫的是 cpu_exec。
    // target-i386/cpu.h
    #define cpu_exec cpu_x86_exe
  1. 當中斷發生時,translation block 必須 unchain。
    • cpu_interrupt (exec.c)→ cpu_unlink_tb (exec.c)
  2. 不同 ISA 有不同的中斷處理函式。請見 target-xxx/* 中的 do_interrupt_xxx。

重要資料結構

      • exception_index 存放中斷號。hw,syscall 都會賦值給 exception_index。QEMU 利用 setjmp/longjmp 處理中斷,jmp_env 用來存放上下文。
      • interrupt_request
      • exit_request。全域變數 exit_request 只有在開啟 IO 執行緒的情況下才會被 cpu_signal 賦值。
      • CPUState *env 會被保存在特定一個 host 暫存器,以加速存取 CPUState。請見 exec.h。
        register struct CPUX86State *env asm(AREG0);
      • CPUState 已用 QOM 改寫。[Qemu-devel] [PULL] QOM CPUState for i386
    • 用來記錄 TB 所需訊息。tc_ptr 指向 translated code cache。TB 和客戶機物理頁面有所聯繫。
      • page_addr 紀錄 TB 所屬頁面。
      • page_next
    • 用來查找頁中的 TB
    • 反組譯 guest binary 所用的結構
    • 存放 TCG IR 所用的結構。
    • static_temps 用來存放運算過程中的中間值。
    • IOHandler 是 callback 函式
    • 段暫存器快取。x86 載入 TR 和 IDTR 其選擇符的時候,會一併把其段基地址、段限長度和描述符屬性載入。cpu_x86_load_seg_cache (target-i386/cpu.h) 負責填充 SegmentCache。

其它

  1. in_asm 代表的是對 guest 的反組譯。out_asm 代表的是針對目標機器生成的組語。op 代表的是 QEMU 自己的 IR。
    # 修改 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
  2. 得知 qemu 在什麼地方做 log。
    $ grep -r qemu_log qemu-0.14.2/*
  3. 加入額外的 log 選項。
    1. cpu-all.h 中加入
      #define CPU_LOG_IBTC       (1 << 10)
      #define CPU_LOG_TB_FIND    (1 << 11)
    2. 打印出新增的選項。
      const CPULogItem cpu_log_items[] = {
         { CPU_LOG_IBTC, "ibtc",
           "print ibtc return address" },
      }
    3. 在代碼中使用新增的選項。
      #ifdef DEBUG_DISAS
        if (qemu_loglevel_mask(CPU_LOG_IBTC)) {
            qemu_log("%lu\t%p\n", guest_eip, next_tb->tc_ptr);
        }
      #endif
  4. 定義自己的 helper function。例如要為 i386 guest 新增 helper function
    1. target-i386/helper.h
      // 傳入 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)
  5. 因為 QEMU 將跳入/出 code cache 當作函式呼叫,它會把 guest call instruction 翻譯成 jmp instruction。如果不這樣做的話,code cache 中的 longjmp (回 QEMU) 會破壞 call stack。
  6. 修改 monitor.[ch] 增加 QEMU Monitor 的功能。
  7. 查看 $BUILD/config-host.mak 得知設定參數。

Makefile

  • Makefile。
    # 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
  • Makefile.target。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
  • rules.mak 14)
    # 由 *.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)))

HowTo

  1. 對照第 72 頁 ABSQ_S.QB 中的 Operation 小節。
    /* 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;
    }

Bug

Submitted Patch

Q & A

  1. QEMU softmmu 做什麼?
    softmmu 負責 GVA → GPA 的對映,但 QEMU 會用 TLB 做 GVA → HVA 的對映,這是用來加速15)。HVA 之後交由宿主機作業系統利用頁表對映至 HPA。
  2. tb_find_fast 和 tb_find_slow 的區別?
    tb_find_fast 用 virtual pc 查找,tb_find_slow 則是用 virtual pc 對應的 physical addr 查找 16)。這是因為不同程序會有相同的 virtual pc,如果 tb_find_fast 用 virtual pc 查找得到的 tb 不屬於當前進程,則改用 tb_find_slow 查找。
  3. QEMU 處理 host physical address 嗎?
    不。QEMU 內部最多用 TLB 存放 GVA → HVA 的對映。
  4. CPUState 中的 iotlb 有什麼用途?
    Re: How can I understand iotlb (IOMMU)
  5. Block chaining 為何要有限制?
    以 qemu-i386 為例,有兩處對 block chaining 有所限制17)
  6. 可以用 GDB 監看客戶機作業系統的物理位址嗎? GDB 只能監看虛擬位址,但有其它作法18)。Bochs 可以指定監看虛擬或物理位址。
  7. 多執行緒程序的支援? 還不周全。
  8. target_phys_addr_t 和 ram_addr_t 分別指的是?
    請見 [Qemu-devel] target_phys_addr_t vs ram_addr_t
    • target_phys_addr_t 代表客戶機物理地址空間。如果客戶機是 x86 開啟 PAE 的話,target_phys_addr_t 為 64 bit。
    • target_ulong 代表客戶機暫存器大小和虛擬地址空間。如果客戶機是 x86 開啟 PAE 的話,target_ulong 為 32 bit。
    • ram_addr_t (uintptr_t) 代表宿主機虛擬地址空間。如果宿主機是 x86 的話,ram_addr_t 為 32 bit。
  9. PageDesc 和 PhysPageDesc 的區別?
    注意,QEMU 看不到 HPA,所以註解中所提到的 physical 總是指 'guest physical'。PageDesc 和 PhysPageDesc 各有自己的兩層頁表 l1_map 和 l1_phys_map。l1_map 是用來查找某個客戶虛擬頁面中相對應的 TB 和其權限。l1_phys_map 用來查找 GPA → HVA 的對映 19)
  10. 執行 code cache 發生頁缺失的時候,會呼叫 tb_find_pc 反查指向該塊 code cache 的 guest pc。
  11. block unchaing 即使在 single thread process mode 和 system mode 都有潛在問題。[Qemu-devel] linux-user crashing in multi-threaded programs
  12. 只用 pc 和 cs_base 來區分不同 process 相同 virtual pc 是不夠的20)

相關文章

外部連結

登录