在前面的章節中,我們介紹了一些關於管理程序的基本概念,並簡要介紹了x86虛擬化的不同技術:使用二進制翻譯的全虛擬化,超虛擬化和硬件虛擬化。今天,我們回深入研究全虛擬化,特別是早期版本的VMWare Workstation如何成功將虛擬化帶回到x86中,不管缺少虛擬化及時支持和架構的深度復雜性。
- 深入解析虛擬化(一)—— 虛擬化簡介 [譯文]
- 深入解析虛擬化(二)——VMWare和使用二進制翻譯的全虛擬化
- 深入解析虛擬化(三)——Xen和超虛擬化
在我們進一步討論前,我想強調,本章將討論的內容是專門設計用於在引入64位擴展或硬件支持虛擬化( VT-x和AMD-v )[2006] 之前的虛擬化x86架構。VMware當前的市面上虛擬機管理程序(VMM)與原始設計明顯不同。不過,你將學的知識將擴展您對虛擬化和底層概念的理解。
關於VMWare的一些話
VMWare從兩種管理程序解決方案開始:Workstation 和 ESX。VMWare Workstation的第一個發布版可追溯到1999年(發布版本歷史)。ESX在2001年出現(發布版本歷史)。Workstation被認為是宿主架構(類型1),然而ESX是運行在裸金屬架構(類型2)。在這篇文章中,我們將專注於VMWare Workstation。
如果你想查看VMM(虛擬機管理程序),從此處下載安裝程序,將其安裝到Windows XP VM中,安裝后,在ProgramFiles目錄中找到 vmware.exe,使用PE資源編輯器(如CFF Explorer)打開它,並轉儲二進制文件,VMM是一個ELF文件。
VMWare Workstation的宿主架構
正如我們在第一篇文章中看到的,宿主架構允許將虛擬化插入到現有的操作系統中。VMWare打包為一個正常的應用程序,其中包含一系列的驅動和可執行/dll文件。作為正常應用程序運行有很多好處。首先,VMWare依靠主機圖形用戶界面,以至於每個虛擬姐屏幕的內容可以自然的出現在一個特別的窗口中,這將是很好的用戶體驗。另一方面,每個虛擬機實例以進程(vmware-vmx.exe)的形式運行在主機操作系統,可以獨立啟動,監控,終止。該進程在本章中會被標記為VMX。
除此之外,在主機OS上運行有助於 I/O 設備模擬。由於操作系統可以使用自己的設備驅動和 I/O 設備通訊,因此VMWare支持通過標准系統調用到主機操作系統來模擬設備。例如,它會讀寫宿主機文件系統來模擬虛擬磁盤設備,或者在宿主機的桌面窗口中繪畫來模擬顯卡。只要宿主機操作系統中有合適的驅動程序,VMWare就可以在其上運行虛擬機。
然而,正常的應用程序沒有VMM的必要API或工具來復用CPU和內存資源。因此,VMWare似乎只運行在當前操作系統的頂端,實際上,它的VMM可以在系統級運行,完全控制硬件。事實上,主機操作系統恰當地假設它一直在控制硬件資源。但是,VMM實際上在一段有限的時間內控制硬件,在這段時間內主機操作系統被從虛擬和線性內存中短暫移除。
從上圖中可以看出,在任何時候,每個CPU可以位於:
- 操作系統完全控制的宿主機OS上下文,或者;
- VMM完全控制的VMM上下文
VMM和宿主機操作系統的上下文切換又稱為世界切換(world switch)。每個上下文有自己的地址空間,中斷描述符表,堆棧,執行上下文。駐留在宿主機的VMM驅動程序實現了一系列操作,包括鎖定物理內存頁,轉發中斷以及調用世界切換原語。就主機操縱系統而言,設備驅動是標准的可加載的內核模塊。但不是驅動某些硬件設備,而是驅動VMM並將其從宿主機操作系統完全隱藏。
當一個設備產生中斷時,CPU可能在主機上下文和VMM上下文中運行。在第一種情況下,CPU通過中斷描述符表(Interrupt Descriptor Table ,IDT)將控制權交給主機操作系統。在任何VMM上下文中發生中斷的第二種情況下,涉及步驟(i)-(v):
- i:VMM被CPU中斷,並觸發VMM外部中斷處理程序執行。
- ii:中斷處理程序立刻觸發世界切換回主機操作系統上下文,idtr恢復為指向主機操作系統中斷表。譯者注:idtr,內存管理寄存器之一。
- iii:內核駐留 驅動將控制權交給由主機操作系統指定的中斷處理程序。
- iv: 這只需通過發出一個 int <vector>指令實現,其中<vector>指令對應於原始的外部中斷。主機操作系統的中斷處理程序隨后正常運行,就像在VMX進程中,當VMM驅動正在處理ioctl時發生了外部I/O中斷一樣。
- v:VMM驅動程序隨后將控制權返還給用戶級的VMX進程,從而主機操作系統有機會決定優先調度。
處理物理中斷的一部分,這個插圖展示出VMWare如何依靠VMs發出I/O請求,所有這些虛擬I/O請求都是使用VMM和VMX進程間的RPC調用來執行的,VMX進程隨后執行正常對主機操作OS的系統調用。為了允許虛擬機和它自己掛起的I/O請求重疊執行,VMX進程運行不同的線程:
- 模擬器線程(Emulator thread)處理執行VM指令的主循環,並模擬設備前端,作為RPC調用處理的一部分。
- 其他線程異步IO(Asychrounous IO,AIO)負責所有可能阻塞操作的執行。
現在返回到世界切換,它與之前可能遇到的傳統上下文切換非常相似(如在內核空間和用戶空間之間,或者在調試器和調試對象之間),提供了加載並執行虛擬機上下文的低級VMM機制,以及恢復主機操作系統上下文的反向機制。
上圖指明了世界切換程序如何將主機切換到VMM上下文,反之亦然。VMM將離開前4MB空間。交叉頁面(cross page)是單頁內存,以非常特殊的方式使用,是世界切換的重點。交叉頁面由內核駐留驅動分配到主機操作系統的內核地址空間。由於驅動程序使用標准API進行分配,因此主機操作系統決定了交叉頁面的地址。
緊接着,在每次世界切換前后,交叉頁面也映射到VMM地址空間。交叉頁面包含世界切換的代碼和數據結構。下面是雙向執行的指令反匯編:
VMX進程表示主機上的虛擬機。它的作用是分配,鎖定和最終釋放所有內存資源。此外,它以文件映射(映射到自己的地址空間)的方式管理VM的物理內存(Linux使用mmap或者Windows使用文件映射(file mapping)API)。虛擬設備DMA的模擬是通過VMX對映射文件對應部分的簡單bcopy,read或write操作實現的。VMX和駐留在內核的驅動程序協同工作來為客戶機物理地址(Guest Physical Address,gPA)鎖定的頁面提供機器物理地址(Machine Physical Address,mPA)。
在Windows上顯示鎖定頁面的屏幕截圖。
虛擬機監視器
既然我們對VMWare的整體托管架構有了一個概念,現在讓我們轉到VMM本身以及它的運作方式。我們之前已經看到,VMM的主要功能是虛擬化CPU和內存。我們也討論了虛擬機通常使用稱為陷阱和模擬(trap-and-emulate)的方法運行。在陷阱和模擬方式的VMM中,客戶機代碼直接運行在CPU上,但減少了權限(reduced privilege)。當客戶機嘗試讀或修改特權狀態時,處理器會生成一個將控制權交給VMM的陷阱。VMM隨后使用解釋器模擬指令並在下一條指令恢復客戶機代碼的直接執行。我們說過x86不能使用陷阱和模擬,因為許多如敏感非特權指令(sensitive non-privileged instructions)的阻礙。那么如何繼續?
一種方法是使用動態二進制翻譯來運行完整的系統模擬,如 Qemu 那樣做。然而,這回產生顯著的性能開銷。如果你運行的是Windows,你可以從這里下載Qemu,並自己動手嘗試。在Linux中,你可以檢查這個 鏈接 ,當然,你不應該使用 KVM 來運行它,因為 Qemu 有一個對 KVM 加速虛擬化的模式,我們將會在之后的章節中討論。
VWWare提供了一個由二進制翻譯(BT)和直接執行(DE)相結合的方案。DE意味着你可以直接在CPU上執行匯編指令
BT將輸入的可執行指令序列轉換成可以在目標系統自身上執行的第二二進制指令序列。動態二進制翻譯器在運行時通過存儲目標序列到稱為翻譯緩存的緩沖區中來執行翻譯。VMWare使用DE運行客戶機用戶模式應用以及BT運行客戶機系統代碼(內核)。將BT和DE結合限制了客戶機花費運行內核代碼的翻譯器時間開銷,這通常是總執行時間的一小部分。與僅依賴二進制翻譯的系統相比,這樣做可以顯著提高性能,因為它允許直接使用所有的硬件組件。
保護VMM
VMM必須為自己保留部分客戶機虛擬地址(VA)空間。盡管VMM的指令和數據結構可能使用大量的客戶機VA空間,但VMM可以完整地在客戶機VA空間內運行。或者,VMM可以運行在單獨的地址空間中,但這種情況下,VMM也必須使用少量的客戶機VA空間用來管理客戶機軟件和VMM之間轉換的控制結構(如IDT和GDT)。無論如何,VMM必須阻止客戶機訪問VMM正在使用的客戶機VA空間的那些部分。否則,如果客戶機能夠寫入那些部分或者客戶機可以讀取它們(內存泄漏),VMM的完整性會受到影響。
VMWare VMM與VM共享相同的地址空間,我們需要保證該部分內容對用戶透明,並用最小的性能開銷來完成。x86支持兩種保護機制:分頁和分段。可以使用其中一個或者兩者都使用,VMWare使用分段來保護VMM免受來自客戶機的影響。
客戶機用戶模式應用在ring 3正常運行,然而,被用來運行在( ring 0 )的客戶機內核代碼被降權而在ring 1層或 %cpl = 1 上的二進制翻譯下運行。虛擬機段(segments)被VMM截斷來確保它們不會與VMM自身重疊。任何嘗試從VM訪問VMM段的行為會觸發會被VMM正確處理的通常保護錯誤。用戶模式的程序運行在截斷的段中,並且受到自身操作系統的保護限制訪問使用分頁 pte.us 的客戶機內核區域。在實際頁表中 pet.us 標志和原始客戶機頁表中的相同。客戶機應用代碼被硬件限制,只能訪問 pte.us = 1 的頁面。客戶機內核代碼,在二進制翻譯機制下運行在 %cpl = 1,沒有限制。
二進制翻譯引入了新的特別的挑戰,是被翻譯的代碼包含混合了需要訪問VMM區域(訪問用來支持VMM的數據結構s)的指令和原始指令.解決方案是預留一個段寄存器,%gs,用來始終指向VMM區域。二進制翻譯器保證(在翻譯時)沒有虛擬機指令會直接使用 gs 前綴(gs prefix)。相反,翻譯后的代碼將 fs 寄存器用於最初具有 fs 或 gs 前綴的VM指令。
VMM截斷段的方式是通過不改變基址減少段描述符(segment descriptor)的范圍,這會導致VMM必須在地址空間的最頂端區域。在他們的實現中,VMWare設置VMM的大小為 4MB。該大小對於具有翻譯緩存的VMM是足夠的,並且其他數據結構足夠大以適應VMM的工作集。
虛擬化內存
所有現代操作系統都使用虛擬內存(virtual memory),它是抽象內存的一種機制。虛擬內存的好處包括能夠使用超過實際內存大小的物理內存,並由於內存隔離而提高安全性。
虛擬內存到物理內存的轉換由名為頁表(Page Table)的查找表完成,這要歸功於MMU(內存管理單元)。當我們嘗試訪問某些虛擬內存時,硬件頁面遍歷器遍歷那些頁表來將VA翻譯為PA(physical address)物理地址。一旦計算出該轉換結果,它就會緩存在成為TLB的CPU緩存(CPU-cache)中。
如我們之前所看到的,我們不能讓客戶機弄壞硬件頁表,所以,需要虛擬化對物理內存的訪問。因此,地址轉換變得有些不同,不再是從VA到PA,我們首先需要從 gVA 翻譯成 gPA,之后從 gPA 翻譯成機器物理地址(MPA),所以,(過程是) gVA ->gPA ->mPA。
在虛擬機中,客戶機操作系統自身像往常一樣通過分段(受VMM的截斷)和分頁(通過以VM的%cr3寄存器為根的頁表結構)控制從客戶機虛擬內存到客戶機物理內存的映射。VMM通過名為影子頁表(shadow page tables)的技術,管理從客戶機物理內存到機器物理內存的映射。
由於性能原因,更重要的是注意從gVA到mPA的組合映射基本上必須駐留在硬件TLB中。因為你不能是VMM干預每次內存訪問,那樣做會非常慢。該解決方案是通過將硬件頁面遍歷器(%cr3)指向影子頁表,影子頁表是直接從 gVA 轉換到 mPA 的數據結構。它的名字是因為它保持跟蹤(shadowing)就頁表上客戶機做的事情以及VMM從 gPA 到 mPA 翻譯的內容。該數據結構必須由VMM主動維護和重新填充。
所以,當客戶機嘗試訪問虛擬地址時,首先檢查 TLB 是否已經有該VA的翻譯,如果是,我們立刻返回其機器物理地址。然而,如果沒有找到,硬件頁面遍歷器(指向影子頁表)執行查找來獲得 gPA 對應的 mPA,如果它得到對應的映射,它會填充 TLB,以便下一次訪問。如果它沒有在影子頁表中找到底層映射,它會產生頁面異常,VMM隨后會通過軟件(in software)遍歷客戶機的頁表以去頂 gPA 並返回 gPA。接下來,VMM使用 pmap(physical map,物理映射)結構確定 gPA 對應的 mPA 。通常,這一步很快,但首次觸及它要求主機操作系統分配支持頁面(backing page)。最后,VMM分配影子頁表用來映射,並連接它到影子頁表樹。頁錯誤和后續的影子頁表更新類似於正常的 TLB 填充,因為他們對客戶機不可見,所以它們被稱為隱藏頁面錯誤(hidden page faults)。
隱藏(頁面)錯誤的成本可能比TLB填充高1000倍,但往往但發生頻率極低,因為更高的虛擬TLB容量(例如,更高的影子頁表容量)。一旦客戶機在影子頁表中建立了工作集,內存訪問就會以本地速度運行直到客戶機切換到不同地址空間。x86 上的 TLB 語義要求上下文切換刷新(flush) TLB(某些特權指令如 invlpg 或 mov %cr3),所以MMU必須拋掉影子頁表並重新開始。我們說這樣的MMU是非緩存的(noncaching)。不幸的是,這會產生許多頁面錯誤,這比 TLB 沒命中(的代價)要昂貴的多。
取而代之的是,VMM維護大的客戶機操作系統的 pde/pte 頁面的影子備份,如下圖所示。通過在相應原始頁面(客戶機物理內存中)放置內存跟蹤(memory trace),VMM可以確保大量客戶機 pde/pte 頁面和VMM中它們的副本的一致性。這種影子頁表的使用顯著增加了虛擬機可用的有效頁表映射的數量,即使在上下文切換后也是如此。
通過內存跟蹤(memory trace),我們指的是VMM在VM的任何給定物理頁上設置讀或寫或讀寫跟蹤和以透明的方式被通知所有對頁面讀和/或寫的訪問的能力。這不僅包括以二進制翻譯或直接執行模式運行的VMM產生的訪問,還包括VMM自身產生的訪問。內存追蹤對VM的執行是透明的,也就是說,虛擬機無法檢測到追蹤的存在。在構成pte時,VMM遵循如下的追蹤設置:
- 具有只寫跟蹤的頁面通常被以硬件頁表中的只讀映射插入。
- 具有讀寫跟蹤的頁面被以無效映射插入。
由於可以在任何時候請求跟蹤,所以當一個新的跟蹤被放置后,系統使用后向機制(backmap mechanism)來降級(downgrade)現有映射。由於權限降級,隨后任何指令產生的對跟蹤頁面的訪問會觸發頁面錯誤。VMM模擬該指令並告知請求模塊關於訪問的具體細節,如頁內偏移的舊值和新值。
正如您可以概括的,該機制被VMM子系統使用來虛擬化MMU和段描述符表(接下來會看到的),以保證翻譯緩存的一致性(更久一點后會看到),來保護虛擬機的BIOS ROM。
虛擬化段描述符
VMM不可以直接使用虛擬機的GDT和LDT,因為這會允許虛擬機控制底層機器。內存分段需要被虛擬化。和影子頁表相似,稱為影子描述符表(shadow descriptor tables)的技術被用來虛擬化x86的分段體系結構。
為了使VMM虛擬化現有系統,VMM設置硬件處理器的GDTR的值為指向VMM的GDT。VMM的GDT被靜態分區為三組條目:
- 影子描述符(shadow descriptors):對應於VM的段描述符表中的項。
- 緩存描述符(cached descriptors):對vCPU的六個加載段進行建模。
-
VMM自身使用的vmm描述符。
影子描述符形成了VMM的GDT的下半部分和整個LDT。他們影(shadow)/復制並跟蹤更改,VM中的GDT和LDT中的項具有以下條件:
- **影子描述符被截斷以至於線性地址空間的范圍不會和為VMM保留的區域重疊。
- 在虛擬機表中,描述符權限級別(Descriptor Privilege Level,DPL)為0的項在影表中DPL為1,這樣VMM的二進制翻譯器可以使用他們(被翻譯的代碼運行在 %cpl = 1).
六個緩存描述符對應於vCPU中的段寄存器並被用來在軟件中模擬vCPU隱藏部分的內容。類似於影子描述符,緩存描述符也被階段並進行權限調整。而且,VMM需要在GDT中為自己保留一定數量的條目,這就是VMM描述符(VMM descriptors)。
只要分段是可逆的(reversible),影子描述符就被使用。這是直接執行的先決條件。如果處理器當前處於與段被加載時不同的模式,或者在保護模式下隱藏部分的段不同於當前對應描述符的內存區域的值時,該段被定義為不可逆的(nonreversible)。當段變得不可逆,緩存描述符對應被使用的特定的段。緩存描述符也在保護模式中被使用,當特定描述符沒有影子時。
另一個需要考慮的重點是,需要確保虛擬機不會(甚至是惡意的)加載VMM的段以供自己使用。這不是直接執行中要考慮的,因為所有的VMM段是 dpl ≤ 1,直接執行僅限於 %cpl = 3。然而,在二進制翻譯中,硬件保護不能用於 dpl = 1 的段描述符。因此,二進制翻譯在所有段分配指令之前插入檢查來確保只有影子項會被加載到CPU中。
與影子頁表一樣,內存跟蹤機制包括一個段跟蹤模塊,該模塊會將影子描述符和他們對應的VM段描述符比較,並指出影子描述符表和他們對應VM描述符表間的任何對應缺失,並更新影子描述符使他們對應他們各自對應的VM段描述符。
虛擬化CPU
正如之前所提到的,VMM由直接執行子系統,動態二進制翻譯器和決定適合使用DE或BT的決策系統組成。決策子系統進行了如下檢查:
- 如果 cr0.pe 未設置(意味着我們處於實模式或SMM模式)=>二進制翻譯。
- 由於 v8086 模式符合Popek和Goldberg對嚴格虛擬化的要求,因此VMWare使用該模式虛擬化自身=>直接執行。
- 在保護模式下,如果 eflags.iopl ≥ cpl (環混淆,ring aliasing)或者!eflags.if =>二進制翻譯。
- 如果段寄存器(ds,es,fs,gs,cs,ss)不被影射(shadowed)=>二進制翻譯。
下表提供了當系統執行VM的指令、經過二進制翻譯的指令或VMM自身時,硬件CPU怎樣被配置的摘要視圖。
當可以直接執行時,處理器的非特權態和虛擬狀態相同。這包括所有段寄存器(包括 %cpl ),所有的 %eflags 條件代碼,和所有 %eflags 控制代碼(%eflags.iopl ,%eflags.v8086 ,%eflags.if )。直接執行子系統的實現相對簡單直接,VMM在內存中保留了一個數據結構,即vcpu,在操作系統中扮演着傳統進程表入口的角色。該結構包含vCPU的狀態,即非特權狀態(通用寄存器,段描述符,條件標志,指令指針,段寄存器)和特權狀態(控制寄存器,%idtr,%gdtr,%ldtr,中斷控制標志等)。當恢復直接執行時,非特權狀態被加載到真實CPU上。當觸發陷阱時,VMM 在加載自身前首先保存非特權虛擬CPU狀態。
二進制翻譯系統
我們不會深入了解動態二進制翻譯代碼的細節,(盡管該部分)機制包含大約VMM所有代碼的45%。我們只對得到大體的架構感興趣。被稱為二進制翻譯(Binary Translation),是因為輸入x86 二進制(binary)代碼而不是簡單的源代碼,是動態的(dynamic),是因為翻譯發生在運行時。理解它的最好的方法是舉個實例:
如果我們編譯它,並反匯編代碼,你會得到類似如下的東西:
一旦翻譯器被調用,匯編代碼的二進制表示作為輸入進入它:53 89 c2 fa b9 01 00 00 00 31 db .... 。翻譯器隨后從每條指令中建立中間表示(Intermediate Representation,IR)對象。翻譯器將IR對象積累到翻譯單元(translation unit,TU)中,在滿12條指令或終止指令處停止:通常像 jmp 或 call 的控制流指令,然后檢查基本塊(Basic Block)。
當CPU處於二進制翻譯模式時,他將vCPU的狀態的一個子集加載到硬件CPU。這包括三個段寄存器(%ds,%es,%ss),所有通用寄存器,和標志寄存器(除了控制代碼)。盡管段寄存器可以指向一個影子(shadow)或一個緩存項(cache entry),但底層描述符總是會指向預期的客戶機定義的虛擬地址空間(盡管可能被截斷)。其含義是只有在這三個段中的任何指令,通用寄存器,或任何條件代碼才能硬件上以相同(identically)的方式執行,而沒有任何開銷。這個含義實際上是VMWare二進制翻譯器的一個中心點。
我們例子中的第一個TU是:
大多數代碼何以被IDENT(identically,相同的)的翻譯。push ,movs 和 xor 都屬於這類。由於cli是一個特權指令,它將中斷標志設置為 0,所以它必須由VMM專門處理。你可以相同的方式翻譯cli,這會導致VMM產生陷阱,之后VMM會模擬它。然而,不同的方式來翻譯它來避免陷阱會有更好的性能。and $0xfd,%gs:vcpu.flags。
由於翻譯不會保留代碼布局,因此最后的 jmp 必須是非 INDENT (non-IDENT)。相反,我們將它轉換為兩個翻譯器調用的延續,一個是每個后繼者(掉頭和采取分支,fall-through和taken-branch),產生該翻譯(方括號表示延續):
之后,VMM將執行以調用翻譯器結束的代碼區來產生 doTest 的翻譯。其他的TU將會類似翻譯。注意,VMWare二進制翻譯器執行一些優化(不是在二進制級),如鏈接優化(chaining optimization)和適應二進制翻譯(adaptive binary translation)的優化,其目的是減少陷阱的數量。我不會再深入,重點只是BT,我會留下足夠的資源,如果你想更深的了解。
在本章中,你已經看到VMWare如何利用分段來保護VMM地址空間,如何影射(shadow)頁表來虛擬化MMU的角色,以及段描述符是怎樣用影子描述符表進行虛擬化。你也看到客戶機用戶模式應用時直接執行,沒有虛擬化開銷,以及客戶機內核代碼以在 ring 1 的二進制翻譯代碼運行。我希望你已經從中學到了一些東西。最后我要感謝引用中的白皮書的作者所做的出色工作。
引用
- Bringing Virtualization to the x86 Architecture with the Original VMware Workstation
- Virtualization System Including a Virtual Machine Monitor For A Computer With A Segmented Architecture
- Foreign LINUX - Run unmodified Linux applications inside Windows
- Fast Binary Translator for the Kernel
- x86 Dynamic Binary Translator Library