2017年11月22日星期三

Useful information from wiki

Memory:
以宿主機作業系統的角度來看,QEMU 就是一般的使用者進程。QEMU 會在自己的虛擬位址空間分配內存給客戶機作業系統。以客戶機作業系統的角度來看,該塊內存即是客戶機作業系統的物理內存。該物理內存分成一般使用的內存和內存映射 IO。透過 cpu_register_physical_memory_offset (exec.c) 註冊。hw/* 會依序呼叫 cpu_register_io_memory 註冊 IO 模擬的函式和 cpu_register_physical_memory。
QEMU 分配給客戶機的內存是以 RAMBlock 和 RAMList 來管理。內存主要分為底下幾類 (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. (模拟硬件MMU里的TLB)
  • 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;
      }

Find Fast - tb_find_fast (cpu-exec.c)
Fins Slow - tb_find_slow (cpu-exec.c)
Build -  tb_gen_code (exec.c)
Flush - tb_flush (exec.c)
Chain - tb_add_jump (exec-all.h)
Excute - tcg_qemu_tb_exec (tcg/tcg.h)
Invalidate - tb_phys_invalidate (exec.c)
Unchain - cpu_unlink_tb (exec.c)
Restore - cpu_restore_state (translate-all.c

TCG (Tiny Code Generator)gen_intermediate_code (target-i386/translate.c)
tcg_gen_code (tcg/tcg.c)
CC (Code Cache)
gen_opc_buf and gen_opparam_buf (translate-all.c)
static_code_gen_buffer (exec.c)
TBD (TB Descriptor)
TranslationBlock (exec-all.h)
TBDA (TB Descriptor Array)
TranslationBlock *tbs (exec.c)
TBHT (TB Hash Table)
TranslationBlock *tb_phys_hash (exec.c)
MPD (Memory Page Descriptor)
PageDesc (exec.c)


MemoryRegion

  • 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
  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 的偏移量。
    }
  • 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;
        }
    }


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(;;) */



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。
  • 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) 請見 PicState2 和 PicState。請見 8259 PIC 和 Intel 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 440FX 和 PCI 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)))

没有评论:

发表评论