qemu原始碼架構

CopperDong發表於2017-09-26

http://blog.chinaunix.net/uid-26941022-id-3510672.html
原文地址:qemu原始碼架構 作者:cywcdwxjf

前言:本文主要概括了QEMU的程式碼結構,特別從程式碼翻譯的角度分析了QEMU是如何將客戶機程式碼翻譯成TCG程式碼和主機程式碼並且最終執行的過程。並且在最後描述了QEMU和KVM之間聯絡的紐帶。

申明:本文前面部分從qemu detailed study第七章翻譯而來。

 

1.程式碼結構

如我們所知,QEMU是一個模擬器,它能夠動態模擬特定架構的CPU指令,如X86,PPC,ARM等等。QEMU模擬的架構叫目標架構,執行 QEMU的系統架構叫主機架構,QEMU中有一個模組叫做微型程式碼生成器(TCG),它用來將目的碼翻譯成主機程式碼。如下圖所示。

    


 

我們也可以將執行在虛擬cpu上的程式碼叫做客戶機程式碼,QEMU的主要功能就是不斷提取客戶機程式碼並且轉化成主機指定架構的程式碼。整個翻譯任務分為兩個部分:第一個部分是將做目的碼(TB)轉化成TCG中間程式碼,然後再將中間程式碼轉化成主機程式碼。

QEMU的程式碼結構非常清晰但是內容非常複雜,這裡先簡單分析一下總體的結構

1. 開始執行:

主要比較重要的c檔案有:/vl.c,/cpus.c, /exec-all.c, /exec.c, /cpu-exec.c.

QEMU的main函式定義在/vl.c中,它也是執行的起點,這個函式的功能主要是建立一個虛擬的硬體環境。它通過引數的解析,將初始化記憶體,需要的模擬的裝置初始化,CPU引數,初始化KVM等等。接著程式就跳轉到其他的執行分支檔案如:/cpus.c, /exec-all.c, /exec.c, /cpu-exec.c.

2. 硬體模擬

所有的硬體裝置都在/hw/ 目錄下面,所有的裝置都有獨自的檔案,包括匯流排,串列埠,網路卡,滑鼠等等。它們通過裝置模組串在一起,在vl.c中的machine _init中初始化。這裡就不講每種裝置是怎麼實現的了。

3.目標機器

現在QEMU模擬的CPU架構有:Alpha, ARM, Cris, i386, M68K, PPC, Sparc, Mips, MicroBlaze, S390X and SH4.

我們在QEMU中使用./configure 可以配置執行的架構,這個指令碼會自動讀取本機真實機器的CPU架構,並且編譯的時候就編譯對應架構的程式碼。對於不同的QEMU做的事情都不同,所以不同架 構下的程式碼在不同的目錄下面。/target-arch/目錄就對應了相應架構的程式碼,如/target-i386/就對應了x86系列的程式碼部分。雖然 不同架構做法不同,但是都是為了實現將對應客戶機CPU架構的TBs轉化成TCG的中間程式碼。這個就是TCG的前半部分。

4.主機

這個部分就是使用TCG程式碼生成主機的程式碼,這部分程式碼在/tcg/裡面,在這個目錄裡面也對應了不同的架構,分別在不同的子目錄裡面,如i386就在/tcg/i386中。整個生成主機程式碼的過程也可以教TCG的後半部分。

5.檔案總結和補充:

/vl.c:                                     最主要的模擬迴圈,虛擬機器機器環境初始化,和CPU的執行。

/target-arch/translate.c    將客戶機程式碼轉化成不同架構的TCG操作碼。

/tcg/tcg.c                              主要的TCG程式碼。

/tcg/arch/tcg-target.c         將TCG程式碼轉化生成主機程式碼

/cpu-exec.c                          其中的cpu-exec()函式主要尋找下一個TB(翻譯程式碼塊),如果沒找到就請求得到下一個TB,並且操作生成的程式碼塊。

2. TCG - 動態翻譯

QEMU在 0.9.1版本之前使用DynGen翻譯c程式碼.當我們需要的時候TCG會動態的轉變程式碼,這個想法的目的是用更多的時間去執行我們生成的程式碼。當新的代 碼從TB中生成以後, 將會被儲存到一個cache中,因為很多相同的TB會被反覆的進行操作,所以這樣類似於記憶體的cache,能夠提高使用效率。而 cache的重新整理使用LRU演算法。

 

 

編譯器在執行器會從原始碼中產生目的碼,像GCC這種編譯器,它為了產生像函式呼叫目的碼會產生一些特殊的彙編目的碼,他們能夠讓編譯器需要知道在呼叫函式。需要什麼,以及函式呼叫以後需要返回什麼,這些特殊的彙編程式碼產生過程就叫做函式的Prologue和Epilogue,這裡就叫前端和後段吧。我在其他文章中也分析過彙編呼叫函式的過程,至於彙編裡面函式呼叫過程中暫存器是如何變化的,在本文中就不再描述了。

函式的後端會恢復前端的狀態,主要做下面2點:

1. 恢復堆疊的指標,包括棧頂和基地址。

2. 修改cs和ip,程式回到之前的前端記錄點。

TCG就如編譯器一樣可以產生目的碼,程式碼會儲存在緩衝區中,當進入前端和後端的時候就會將TCG生成的緩衝程式碼插入到目的碼中。

接下來我們就來看下如何翻譯程式碼的:

客戶機程式碼

 

TCG中間程式碼


主機程式碼

 

 

3. TB鏈

在QEMU中,從程式碼cache到靜態程式碼再回到程式碼cache,這個過程比較耗時,所以在QEMU中涉及了一個TB鏈將所有TB連在一起,可以讓一個TB執行完以後直接跳到下一個TB,而不用每次都返回到靜態程式碼部分。具體過程如下圖:

 

4. QEMU的TCG程式碼分析

接下來來看看QEMU程式碼中中到底怎麼來執行這個TCG的,看看它是如何生成主機程式碼的。

main_loop(...){/vl.c} : 

函式main_loop 初始化qemu_main_loop_start()然後進入無限迴圈cpu_exec_all() , 這個是QEMU的一個主要迴圈,在裡面會不斷的判斷一些條件,如虛擬機器的關機斷電之類的。

qemu_main_loop_start(...){/cpus.c} :

函式設定系統變數 qemu_system_ready = 1並且重啟所有的執行緒並且等待一個條件變數。 

cpu_exec_all(...){/cpus.c} :

它是cpu迴圈,QEMU能夠啟動256個cpu核,但是這些核將會分時執行,然後執行qemu_cpu_exec() 。

struct CPUState{/target-xyz/cpu.h} :

它是CPU狀態結構體,關於cpu的各種狀態,不同架構下面還有不同。

 

cpu_exec(...){/cpu-exec.c}:

這個函式是主要的執行迴圈,這裡第一次翻譯之前說道德TB,TB被初始化為(TranslationBlock *tb) ,然後不停的執行異常處理。其中巢狀了兩個無限迴圈 find tb_find_fast() 和tcg_qemu_tb_exec().

cantb_find_fast()為客戶機初始化查詢下一個TB,並且生成主機程式碼。

tcg_qemu_tb_exec()執行生成的主機程式碼 

struct TranslationBlock {/exec-all.h}:

結構體TranslationBlock包含下面的成員:PC, CS_BASE, Flags (表明TB), tc_ptr (指向這個TB翻譯程式碼的指標), tb_next_offset[2], tb_jmp_offset[2] (接下去的Tb), *jmp_next[2], *jmp_first (之前的TB).

 

tb_find_fast(...){/cpu-exec.c} :

函式通過呼叫獲得程式指標計數器,然後傳到一個雜湊函式從 tb_jmp_cache[] (一個雜湊表)得到TB的所以,所以使用tb_jmp_cache可以找到下一個TB。如果沒有找到下一個TB,則使用tb_find_slow。

 tb_find_slow(...){/cpu-exec.c}:

這個是在快速查詢失敗以後試圖去訪問實體記憶體,尋找TB。

tb_gen_code(...){/exec.c}:

開始分配一個新的TB,TB的PC是剛剛從CPUstate裡面通過using get_page_addr_code()找到的

phys_pc = get_page_addr_code(env, pc);

tb = tb_alloc(pc);

ph當呼叫cpu_gen_code() 以後,接著會呼叫tb_link_page()它將增加一個新的TB,並且指向它的物理頁表。

cpu_gen_code(...){translate-all.c}:

函式初始化真正的程式碼生成,在這個函式裡面有下面的函式呼叫:

gen_intermediate_code(){/target-arch/translate.c}->gen_intermediate_code_internal(){/target-arch/translate.c }->disas_insn(){/target-arch/translate.c}

disas_insn(){/target-arch/translate.c}   

      //9833 disas_arm_insn   --> printf("insn(%d):0x%x\n", ++copper_counter, insn); //copper

                                             --> arm_ldl_code(cpu.h,740)->insn = cpu_ldl_code

      // 4448 disas_neon_data_insn  //copper float

      ./include/exec/cpu-all.h:268:#define cpu_ldl_code(env1, p) ldl_raw(p)      

      ./include/exec/cpu-all.h:253:#define ldl(p) ldl_raw(p)

      ./roms/qemu-palcode/pal.h:142:#define ldl_phw_ldl/p

       disas_thumb_insn

函式disas_insn() 真正的實現將客戶機程式碼翻譯成TCG程式碼,它通過一長串的switch case,將不同的指令做不同的翻譯,最後呼叫tcg_gen_code。

 

tcg_gen_code(...){/tcg/tcg.c}:

這個函式將TCG的程式碼轉化成主機程式碼,這個就不細細說明了,和前面類似。

 

#define tcg_qemu_tb_exec(...){/tcg/tcg.g}:

通過上面的步驟,當TB生成以後就通過這個函式進行執行.

next_tb = tcg_qemu_tb_exec(tc_ptr) :

extern uint8_t code_gen_prologue[];

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

 

通過上面的步驟我們就解析了QEMU是如何將客戶機程式碼翻譯成主機程式碼的,瞭解了TCG的工作原理。接下來看看QEMU與KVM是怎麼聯絡的。

5. QEMU中的IOCTL

在QEMU-KVM中,使用者空間的QEMU是通過IOCTL與核心空間的KVM模組進行通訊的。

1. 建立KVM

在/vl.c中通過kvm_init()將會建立各種KVM的結構體變數,並且通過IOCTL與已經初始化好的KVM模組進行通訊,建立虛擬機器。然後建立VCPU,等等。

2. KVM_RUN

這個IOCTL是使用最頻繁的,整個KVM執行就不停在執行這個IOCTL,當KVM需要QEMU處理一些指令和IO等等的時候就會退出通過這個IOCTL退回到QEMU進行處理,不然就會一直在KVM中執行。

它的初始化過程:

vl.c中呼叫machine->init初始化硬體裝置接著呼叫pc_init_pci,然後再呼叫pc_init1。

接著通過下面的呼叫初始化KVM的主迴圈,以及CPU迴圈。在CPU迴圈的過程中不斷的執行KVM_RUN與KVM進行互動。

pc_init1->pc_cpus_init->pc_new_cpu->cpu_x86_init->qemu_init_vcpu->kvm_init_vcpu->ap_main_loop->kvm_main_loop_cpu->kvm_cpu_exec->kvm_run

3.KVM_IRQ_LINE

這個IOCTL和KVM_RUN是不同步的,它也是個頻率非常高的呼叫,它就是一般中斷裝置的中斷注入入口。當裝置有中斷就通過這個IOCTL最終 呼叫KVM裡面的kvm_set_irq將中斷注入到虛擬的中斷控制器。在kvm中會進一步判斷屬於什麼中斷型別,然後在合適的時機寫入vmcs。當然在 KVM_RUN中會不斷的同步虛擬中斷控制器,來獲取需要注入的中斷,這些中斷包括QEMU和KVM本身的,並在重新進入客戶機之前注入中斷。

 

總結: 通過這篇文章能夠大概的瞭解QEMU的程式碼結構,其中主要包括TCG翻譯程式碼的過程以及QEMU和KVM的互動過程。

相關文章