天天看點

x86虛拟記憶體和qemu記憶體虛拟化

作者:後端開發進階

記憶體虛拟化是一個很大的話題,最近安全部門發現了一個qemu記憶體虛拟化的安全漏洞,回報給雲平台讓解決,感覺很棘手,引起了我對記憶體虛拟化的思考,想到什麼問題就把思考記錄下來。

x86虛拟記憶體

問題是由學習qemu MemoryRegion想到的,文檔memory.rst中有一句話“memory banks used when the guest address space is smaller than the amount of RAM addressed”,說是alias類型MemoryRegion适用于這種場景,大概意思就是qemu給guest提供的實體記憶體超過了guest的address space,這時就得用alias類型的MemoryRegion了,那這兒的memory banks是什麼意思,實體記憶體條有rank/bank,這兒的bank應當了解成岸,類似于一個岸把湖分成兩半,和真正的記憶體條中的rank/bank沒關系。qemu中有below_4g_mem_size和above_4g_mem_size兩個MemoryRegion Alias,我覺得這個命名不好,如果加上userspce和kernelspace就好了解了,程序的虛拟位址空間分為使用者空間和核心空間,兩者執行權限不一樣。所有程序核心空間都一樣,隻是使用者空間不一樣,這樣所有程序就可以共享核心,是以需要在4G空間有一條線分成兩部分。每個程序有自己的頁目錄,其中page table中關于核心部分指向相同,借用網上的這張圖說明一下,假設CPU是32位,核心空間1G,使用者态空間3G。

x86虛拟記憶體和qemu記憶體虛拟化

再想想虛拟位址空間是如果生成的,gcc編譯源代碼生成elf格式, linux核心load可執行程式elf格式檔案生成虛拟位址空間,虛拟位址空間由段和頁構成,段有code,data,heap和stack等, stack是固定大小,linux中的段都指向0,主要是page發揮作用。執行時大概是這樣IP指令寄存器告訴MMU要加載的指令,如果page fault, 增加page然後建立映射關系, load指令到記憶體,其它load指令告訴MMU,要把資料放到記憶體中,不知道還區分資料總線和位址總線不,課本中學過,程式執行用的都是虛拟位址,反彙編可以看到虛拟位址。

虛拟位址空間設計成這樣是因為核心不能發生pagefault,如果核心處理pagefault時發生pagefault沒法玩了,是以說核心常駐實體記憶體中。使用者态malloc一塊核心,用虛拟位址通路發生pagefault,核心找一個page然後對應起來,那核心配置設定一個page的記憶體,核心先得到的是這個page的實體位址,然後把實體位址轉換成核心虛拟位址,總之核心管理實體記憶體,并且和實體記憶體一一對應,為什麼要一一對應沒想明白,感覺這樣實作是簡單,核心經常需要在虛拟和實體位址之間轉來轉去的,一一對應用virt_to_phys和phys_to_virt就能實作虛拟和實體位址互相轉換,簡單性能高。也許是因為MMU不是一開始就開啟的,核心在CPU處于實模式時建立early_level4_pgt和init_level4_pgt,切換到保護模式才開啟MMU了,核心虛拟空間和實體核心一一對應是實模式要求這樣的,如果不這樣實模式時就沒法操作了,要了解虛拟記憶體肯定得看懂實模式時代碼幹的活,否則還是有點虛。核心虛拟空間是1G,實際上核心隻占用了896M虛拟空間,一一映射那就和實體位址0開始的896M,896M以上的實體位址就叫做high mem,kernel要通路就建立映射到它剩下的128M虛拟空間中,詳見函數kmap_high。

為什麼是896M?

https://stackoverflow.com/questions/4528568/how-does-the-linux-kernel-manage-less-than-1gb-physical-memory

Final kernel Page Table when RAM size is less than 896 MB - Linux Kernel Reference

前面說的都是記憶體,外設記憶體要是怎麼通路的?個人了解外設記憶體分為配置,BAR和其它記憶體,配置記憶體是PCI規範指定的,配置記憶體中指定BAR空間開始位址和長度,BAR空間中指定其它記憶體如常說的顯示卡顯存大小。裝置寫固件時在配置記憶體中指定BAR開始實體位址和長度,開機時bios周遊PCI總線發現PCI裝置和記憶體,bios拼湊出實體位址空間,拼湊完有可能改變一個裝置BAR的開始實體位址,把改變後的值重新寫入配置記憶體中,配置記憶體個人了解是linux pci系統統一映射到記憶體中的,BAR是加載裝置驅動時映射的,pci bar mmio了解為從pci configure space中得到bar的phy_addr,然後ioremap建立page entry,通路這個phy_addr,pci bus把請求路由給裝置而不是記憶體。外設的其它記憶體CPU不能直接通路,隻有外設自己可以通路,CPU要通路得委托外設DMA把資料寫到記憶體,這部分記憶體位址CPU不處理,隻要驅動和外設配合來就行了,外設可以通路記憶體,可以通路自己的這部分記憶體。

x86中cr3指定頁目錄,同一個程序系統調用從使用者态切換到核心隻切換stack和cpu context,不切換cr3,隻有不同程序切換時才切換cr3。

32位CPU可以通路的實體記憶體最大是4G,但有了PAE就不一樣了,一個CPU的虛拟位址空間還是4G,隻是這4G不再局限于映射到實體低4G上了,可以通路的最大實體空間總和一定不會超過4G,總的幾級頁結構換算出來的值一定是4G,但page table entry的結果是64位,用其中46位,加上4k中的偏移即可以得到實體位址。

x86虛拟記憶體和qemu記憶體虛拟化

qemu記憶體虛拟化

host的記憶體實體記憶體是bios拼湊出來的,guest的實體記憶體是qemu用MemoryRegion拼湊出來的,guest實體記憶體也包含記憶體條記憶體和裝置記憶體,隻是guest記憶體條記憶體和裝置記憶體都是由host的的記憶體虛拟出來的,guest通路記憶體條記憶體和裝置記憶體觸發kvm執行的動作是不一樣的。

guest和host是獨立的系統,兩者都有自己的虛拟位址和實體位址,唯一的關系就是把guest的實體位址映射到host的虛拟位址,也就是qemu程序的虛拟位址。拿用的最多的EPT來說,guest有自己的頁目錄,kvm又維護了一個guest實體位址到host實體位址映射的頁目錄,cpu進入guest模式時一個虛拟位址要依次查找這兩個頁目錄,guest查找自己的頁目錄,如果找不到就發生pagefault,處理pagefault,找到一個實體頁,給頁目錄增加一項,通路實體頁面發生EPT violation exit,kvm增加guest實體位址到host實體位址映射的頁目錄表項,重新enter guest繼續執行。如果guest在自己頁目錄中找到,繼續查找kvm維護的頁目錄,如果找不到發生EPT violation exit,kvm調用handle_ept_violation增加guest實體位址到host實體位址映射的頁目錄表項,然後重新enter guest繼續執行,kvm的過程對于guest來說是透明的。如果guest核心回收一個page,删除一個自己的頁目錄表項,此時guest exit,因為kvm對guest頁目錄占用的記憶體做過特殊标記,kvm調用handle_invlpg删除自己的表項。

guest啟動時是實模式,還沒有頁目錄,沒有MMU功能,早期guest實模式時由qemu來模拟,後來Intel CPU中加入了Unrestricted Guest,EPT開始支援實模式。

https://lists.gnu.org/archive/html/qemu-devel/2019-09/msg06225.html

https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=3a624e29c7587b79abab60e279f9d1a62a3d4716

guest通路自己的裝置記憶體,qemu和kvm對這些記憶體做了特殊标志,guest通路就觸發EPT misconfig,然後kvm調用handle_ept_misconfig處理,根據位址範圍找到屬于的裝置,然後調用裝置模拟的代碼,如果kvm搞不定退回qemu繼續處理,kvm和qemu要做的事情就是把guest的實體位址轉換成host的虛拟位址,然後讀寫轉換後的虛拟位址,這樣就等價于guest讀寫自己的外設記憶體了,處理完後,enter guest讓guest繼續運作。

如果實體CPU支援pae特性,比較新一點的linux guest和kvm會檢測自動把pae利用起來。

總結

個人産生一些想法,努力去找答案,然後記錄下來,剛開始都是一些問題,由一個問題想到更多問題,有些問題之間扯的很遠,然後找答案,記錄下來的都是問題和答案,努力加工整理成文章,感覺還是不成體系,了解不深而且未必正确。

原文連結:https://mp.csdn.net/mp_blog/creation/editor?not_checkout=1%3Fnot_checkout%3D1

繼續閱讀