The Design and Implementation of Hyperupcalls 翻譯

HustWolfzzb發表於2019-05-10

正文之前

這篇文章可有點叼了。。。廢話不多說,看圖!

The Design and Implementation of Hyperupcalls 翻譯

引用如下:Amit N, Wei M. The design and implementation of hyperupcalls[C]//2018 {USENIX} Annual Technical Conference ({USENIX}{ATC} 18). 2018: 97-112.

然後照樣是長篇的翻譯。。當然,主力是谷歌,我負責人工稽核一波。。PPT我也有,如果有需要的可以不去麻煩作者了,(作者如果沒人提醒估計是不會回你的。。我還是找了他學生才拿到的,不過貌似有個網站,他把PPT都丟了上去,不過我忘了)

The Design and Implementation of Hyperupcalls 翻譯

正文

摘要

虛擬機器抽象提供了各種各樣的好處,無可否認地支援了雲端計算。 然而,虛擬機器是雙重的 一把雙刃劍,因為他們執行於其上的Hypervisor必須將他們視為黑盒,這限制了他們之間能夠交換的資訊。 在本文中,我們介紹了一種新機制hyperupcalls的設計和實現,它使Hypervisor能夠安全地執行Guest虛擬機器提供的驗證程式碼,以便傳輸資訊。 Hyperupcalls是用C語言編寫的,可以完全訪問Guest資料結構,例如頁表。 我們提供了一個完整的框架,可以從hyperupcall中輕鬆訪問熟悉的核心函式。 與最先進的半虛擬化技術和虛擬機器內省相比,Hyperupcalls更靈活,更少侵入。 我們證明,hyperupcalls不僅可以用於將某些操作的Guest效能提高多達2倍,而且hyperupcalls也可以用作強大的除錯和安全工具。

1 簡介

硬體虛擬化引入了虛擬機器(VM)的抽象,使得稱為Hypervisor的主機能夠同時執行稱為Guest機的多個作業系統(OS),每個作業系統都假設它們在自己的物理機器上執行。 這是通過暴露模擬真實物理硬體的硬體介面來實現的。 這種簡單抽象的引入導致了現代資料中心和雲的興起,正如我們今天所知道的那樣。 不幸的是,虛擬化並非沒有缺點。 雖然虛擬化的目標是為VM和Hypervisor彼此分開,這種分離使得雙方無法理解在另一側做出的決定,被稱為語義間隙問題 。

【The semantic gap characterizes the difference between two descriptions of an object by different linguistic representations, for instance languages or symbols. According to Hein, the semantic gap can be defined as "the difference in meaning between constructs formed within different representation systems". 語義間隙通過不同的語言表示(例如語言或符號)來表徵物件的兩個描述之間的差異。 根據Hein的說法,語義差距可以定義為“在不同表示系統中形成的構造之間的意義差異”。】

解決語義差距對效能至關重要。 如果沒有關於Guest決策的資訊,Hypervisor可能會次優地分配資源。 例如,Hypervisor無法在不瞭解其內部作業系統狀態的情況下知道guest虛擬機器中的哪些記憶體空閒,從而破壞了VM抽象。 如今,最先進的Hypervisor通常通過半虛擬化[11,58]彌合語義鴻溝,這使得Guest瞭解Hypervisor。 半虛擬化使Guest免受物理硬體介面的限制,並允許與Hypervisor直接資訊交換,通過使Hypervisor能夠做出更好的資源分配決策來提高整體效能。

然而,半虛擬化涉及在Hypervisor和Guest機的上下文中執行程式碼。 Hypercalls要求Guest發出一個在Hypervisor中執行的請求,就像系統呼叫一樣,並且upcalls要求Hypervisor發出請求在Guest中執行。 這種設計帶來了許多缺點。 首先,半虛擬機器制在Hypervisor和Guest機之間引入了上下文切換,如果需要Guest機和Hypervisor之間的頻繁互動,這可能是實質性的[7]。 其次,半虛擬機器制的請求者必須等待它在另一個可能正忙的上下文中服務,或者如果它是空閒的則喚醒該Guest。 最後,半虛擬機器制將Hypervisor和Guest機的設計結合起來:需要為每個Guest機和Hypervisor實現半虛擬機器制,從而增加複雜性[46]並妨礙可維護性[77]。 新增準虛擬功能需要使用新介面更新guest虛擬機器和Hypervisor[69],並且有可能引入錯誤和攻擊面[47,75]。

【每一段程式都有很多外部變數。只有像Add這種簡單的函式才是沒有外部變數的。一旦你的一段程式有了外部變數,這段程式就不完整,不能獨立執行。你為了使他們執行,就要給所有的外部變數一個一個寫一些值進去。這些值的集合就叫上下文。譬如說在C++的lambda表達是裡面,[寫在這裡的就是上下文](int a, int b){ ... }。】

一種不同的技術,VM內省(VMI)[25]和反向,Hypervisor內省(HVI)[72]旨在通過內省其他上下文來解決半虛擬化的一些缺點,實現無需上下文切換或先前協調的通訊傳輸。 然而,這些技術是脆弱的:資料結構,行為甚至安全加強的微小變化[31]可能會打破內省機制,或者更糟糕的是,引入安全漏洞。 因此,內省通常被歸入入侵檢測系統(IDS)領域,該系統可檢測惡意軟體或行為不當的應用程式。

【VMI tools may be located inside or outside the virtual machine and act by tracking the events (interrupts, memory writes, and so on) or sending the requests to the virtual machine. Virtual machine monitor usually provides low-level information like raw bytes of the memory. Converting this low-level view into something meaningful for the user is known as the semantic gap problem. Solving this problem requires analysis and understanding of the systems being monitored. VMI工具可以位於虛擬機器內部或外部,並通過跟蹤事件(中斷,記憶體寫入等)或將請求傳送到虛擬機器來執行操作。虛擬機器監視器通常提供低階資訊,如記憶體的原始位元組。將此低階檢視轉換為對使用者有意義的內容稱為語義缺口問題。解決這個問題需要分析和理解被監控的系統。 VMI:即Virtual Machine Introspection,VMI是一種用於在外部監測系統級虛擬機器執行狀態的技術。監測器可置於另一個虛擬機器,在VMM內部或在虛擬化架構的任何其他部分。在VMI過程中,VM的執行狀態可被廣義地定義為包括處理器暫存器、記憶體、磁碟、網路及任何硬體級事件。】

在本文中,我們描述了hyperupcalls 1的設計和實現,這種技術使Hypervisor能夠與Guest進行通訊,如upcalls,但沒有這樣的上下文切換,像VMI。 這是通過使用經過驗證的程式碼實現的,該程式碼使Guest能夠以靈活的方式與Hypervisor進行通訊,同時確保Guest不能提供行為不當或惡意程式碼。 一旦Guest註冊了一個hyperupcall,Hypervisor就可以執行它來執行諸如定位空閒Guest頁面或執行Guest中斷處理程式而不切換到Guest的操作。

Hyperupcalls易於構建:它們是用C語言等高階語言編寫的,我們提供了一個框架,允許hyperupcalls共享相同的程式碼庫並構建系統,因為Linux核心可以推廣到其他作業系統。 編譯核心時,工具鏈會將hyperupcall轉換為可驗證的位元組碼。 這樣可以輕鬆維護hyperupcalls。 在引導時,guest虛擬機器向hypervisor註冊hyperupcalls,Hypervisor驗證位元組碼並將其編譯回本機程式碼以獲得效能。 一旦重新編譯,Hypervisor可以隨時呼叫hyperupcall。

我們表明,使用hyperupcalls可以通過允許Hypervisor主動分配資源來顯著提高效能,而不是等待guest虛擬機器通過現有機制做出反應。 我們構建用於記憶體回收和處理內部處理器中斷(IPI)的hyperupcalls,並顯示高達2 x的效能提升。除了提高效能之外,hyperupcall還可以增強虛擬環境中系統的安全性和可除錯性。 我們開發了一個hyperupcall,使Guest能夠在不使用專用硬體的情況下對記憶體頁面進行防寫,另一個使ftrace [57]能夠在統一的跟蹤中捕獲Guest和Hypervisor事件,從而使我們能夠在虛擬化環境中獲得新的效能視野。

【write-protect防寫是硬體裝置或軟體程式阻止寫入新資訊或更改舊資訊的能力。 通常,這意味著您可以讀取資料,但不能寫入。 圖中是SD卡上的防寫開關示例,用於開啟和關閉該卡上的防寫。 Ftrace是一個直接內建於Linux核心的跟蹤實用程式。 許多發行版在其最新版本中已經啟用了各種Ftrace配置。 Ftrace為Linux帶來的好處之一是能夠檢視核心中發生的事情。】

本文做出以下貢獻:

  • 我們建立了一種機制分類,用於彌合Hypervisor和Guest之間的語義鴻溝,並在該分類中放置Hyperupcalls

  • 我們用以下內容描述和實現hyperupcalls(§3):

    • 編寫hyperupcalls的環境和使用Guest程式碼的框架(§3.1)
    • 用於hyperupcalls的編譯器(§3.2)和驗證器(§3.4),它解決了驗證程式碼的複雜性和侷限性。
    • Hyperupcalls的註冊(§3.3)和執行(§3.5)機制。
  • 我們對hyperupcalls進行原型設計和評估,並表明hyperupcalls可以提高效能(§4.3,§4.2),安全性(§4.5)和可除錯性(§4.4)。

2 溝通機制

現在人們普遍認為,為了從虛擬化中提取最大的效能和實用性,Hypervisor及其Guest需要彼此瞭解。 為此,存在許多促進Hypervisor和Guest之間通訊的機制。 表1總結了這些機制,這些機制可以由請求者,執行者以及機制是否要求Hypervisor和Guest提前協調來廣泛地表徵。

The Design and Implementation of Hyperupcalls 翻譯

在下一節中,我們將討論這些機制並描述hyperupcall如何滿足通訊機制的需求,其中Hypervisor在沒有上下文切換的情況下製作並執行其自己的請求。 我們首先介紹當今使用的最先進的半虛擬機器制。

2.1半虛擬化

超級呼叫和上行呼叫。 如今,大多數Hypervisor都利用半虛擬化來跨語義鴻溝進行通訊。 目前廣泛使用的兩種機制是超級呼叫,它允許Guest呼叫Hypervisor提供的服務和upcalls,這使得Hypervisor可以向Guest發出請求。 半虛擬化意味著這些機制的介面在Hypervisor和Guest之間提前協調[11]。

上行呼叫和超級呼叫的主要缺點之一是它們需要上下文切換,因為兩種機制都在請求的相反側執行。 因此,必須小心呼叫這些機制。 過於頻繁地呼叫hypercall或upcalls會導致高延遲和計算資源浪費[3]。

upcalls的另一個缺點,特別是請求由可能是忙於處理其他任務的Guest處理。 如果Guest忙碌或Guest閒置,則會因為等待Guest機有空閒或者喚醒而產生而外的懲罰。 這可能需要無限的時間,並且Hypervisor可能不得不依賴懲罰系統來確保Guest在合理的時間內做出響應。

最後,通過增加Hypervisor與其Guest之間的耦合,半虛擬機器制可能難以維持。 每個Hypervisor都有自己的半虛擬介面,每個guest虛擬機器必須實現每個Hypervisor的介面。 半虛擬介面並不薄:微軟的半虛擬介面規範長達300頁[46]。 Linux提供了各種準虛擬鉤子,Hypervisor可以使用它們與VM進行通訊[78]。儘管努力使半虛擬化介面標準化 ,但它們彼此不相容,並隨著時間的推移而發展,新增功能甚至刪除一些功能(例如,MicrosoftHypervisor事件跟蹤)。 因此,大多數Hypervisor並不完全支援標準化介面的工作,而專業作業系統則尋找替代解決方案[45,54]。

預虛擬化。 預虛擬化[42]是Guest從Hypervisor請求服務的另一種機制,但請求是在Guest自身的上下文中提供的。 這是通過程式碼注入實現的:Guest端留下存根,Hypervisor用Hypervisor程式碼填充。 預虛擬化提供了對hypercalls的改進,因為它們在Guest和Hypervisor之間提供了更靈活的介面。 可以說,預虛擬化存在一個基本限制:在guest虛擬機器中執行的程式碼是剝離的,無法執行敏感操作,例如,訪問共享I/O裝置。 因此,在預虛擬化中,在Guest端中執行的超級程式碼仍然需要使用hypercalls與特權Hypervisor程式碼進行通訊。

2.2內省

當Hypervisor或Guest嘗試從其他上下文中推斷資訊而不直接與其進行通訊時,就會發生自省。 通過內省,無需任何介面或協調。 例如,Hypervisor可能僅僅通過其儲存器訪問模式來嘗試推斷完全未知的Guest的狀態。 內省和半虛擬化之間的另一個區別是沒有發生上下文切換:執行內省的所有程式碼都在請求者中執行。

虛擬機器內省(VMI)。 當Hypervisor對Guest進行內省時,它被稱為VMI [25]。 首次引入VMI是為了通過從特權主機提供入侵檢測(IDS)和核心完整性檢查來增強VM安全性[10,24,25]。 VMI還應用於檢查點和重複資料刪除VM狀態[1],以及監控和實施Hypervisor策略[55]。 這些機制的範圍從簡單地觀察VM的記憶體和I/O訪問模式[36]到訪問VM OS資料結構[16],並且在最末端它們可以修改VM狀態甚至直接將程式注入其中[26,19]。 VMI的主要好處是Hypervisor可以在沒有上下文切換的情況下直接呼叫VMI,並且guest虛擬機器無需“意識到”檢查VMI是否正常執行。 但是,VMI很脆弱:VM OS中的一個無害的變化,例如為資料結構新增額外欄位的修補程式可能會導致VMI無法正常工作[8]。 因此,VMI往往是一種“盡力而為”的機制。

HVI。 在較小程度上,Guest可能會反省它正在執行於其上的Hypervisor,稱為Hypervisor內省(HVI)[72,61]。 HVI通常用於保護VM免受不受信任的Hypervisor[62]或惡意軟體以繞過Hypervisor安全[59,48]。

2.3可擴充套件的作業系統

雖然Hypervisor提供了固定的介面,但OS研究表明,多年來靈活的作業系統介面可以在不犧牲安全性的情況下提高效能。 Exokernel提供了低階原語,並允許應用程式實現高階抽象,例如記憶體管理[22]。 SPIN允許擴充套件核心功能以提供特定於應用程式的服務,例如專門的程式間通訊[13]。 使這些擴充套件能夠在不影響安全性的情況下執行良好的關鍵特性是使用簡單的位元組程式碼來表達應用程式需求,並在與核心相同的保護環上執行此程式碼。 我們的工作受到這些研究的啟發,我們的目標是在Hypervisor和Guest之間設計一個靈活的介面,以彌合語義鴻溝。

2.4 Hyperupcalls

本文介紹了hyperupcalls,它滿足了Hypervisor與guest虛擬機器通訊的機制的需求,該機制是協調的(與VMI不同),由Hypervisor本身執行(與upcalls不同)並且不需要上下文切換(與hypercalls不同)。通過hyperupcalls,VM通過註冊可驗證程式碼與Hypervisor進行協調。 然後,Hypervisor響應於事件(例如記憶體壓力或VM進入/退出)執行該程式碼。 在某種程度上,hyperupcalls可以被認為是由Hypervisor執行的upcalls。

與VMI相比,訪問VM狀態的程式碼由guest提供,因此hyperupcalls完全瞭解guest虛擬機器內部資料結構 - 實際上,hyperupcalls是使用guest虛擬機器作業系統程式碼庫構建的,並共享相同的程式碼,從而簡化了維護,同時提供了作業系統具有表達機制來向底層Hypervisor描述其狀態。

與Hypervisor向guest虛擬機器發出非同步請求的upcalls相比,Hypervisor可以隨時執行hyperupcall,即使guest虛擬機器未執行也是如此。 通過upcalls,Hypervisor受到Guest的支配,這可能會延遲upcalls[6]。 此外,由於upcalls的操作類似於遠端請求,因此upcalls可能會被迫以不同的方式實現OS功能。 例如,當重新整理用於識別空閒Guest記憶體的規範技術時的ballooning中的遠端頁面[71]時,Guest使用虛擬程式來釋放記憶體壓力以釋放頁面。 通過hyperupcall,Hypervisor可以像Guest核心執行緒一樣,直接掃描Guest的空閒頁面。

Blooning 在虛擬機器上安裝的VMtools就包括了ballooningdriver。它告訴Hypervisor哪些不活動的記憶體頁面可以被收回。這對虛擬機器上應用的效能是沒有任何影響的。】

Hyperupcalls類似於預虛擬化,因為程式碼是跨語義間隙傳輸的。 傳輸程式碼不僅可以實現更具表現力的通訊,還可以將請求的執行移至間隙的另一端,從而增強效能和功能。 與預虛擬化不同,Hypervisor不能信任虛擬機器提供的程式碼,並且Hypervisor必須確保高呼叫的執行環境在呼叫之間保持一致。

3 架構

Hyperupcalls是由guest虛擬機器提供給Hypervisor的簡短可驗證程式,用於提高效能或提供其他功能。 guest虛擬機器通過啟動時的註冊過程向Hypervisor提供hyperupcall,允許Hypervisor訪問guest虛擬機器作業系統狀態,並在驗證後通過執行它們來提供服務。 Hypervisor執行hyperupcalls以響應事件或何時需要查詢guest狀態。 hyperupcalls的體系結構和我們為利用它們而構建的系統如圖1所示。

The Design and Implementation of Hyperupcalls 翻譯

我們的目標是使hyperupcalls儘可能簡單地構建。 為此,我們提供了一個完整的框架,允許程式設計師使用Guest作業系統程式碼庫編寫hyperupcalls。 這極大地簡化了hyperupcalls的開發和維護。 該框架將此程式碼編譯為可驗證的程式碼,Guest向Hypervisor註冊。 在下一節中,我們將描述OS開發人員如何使用我們的框架編寫hyperupcall。

3.1構建Hyperupcalls

Guest作業系統開發人員為他們希望處理的每個Hypervisor事件編寫hyperupcall。 Hypervisor和Guest同意這些事件,例如VM進入/退出,頁面對映或虛擬CPU(VCPU)搶佔。 每個hyperupcall都由預定義的識別符號標識,非常類似於UNIX系統呼叫介面[56]。 表2給出了hyperupcall可以處理的事件的示例。

The Design and Implementation of Hyperupcalls 翻譯

3.1.1提供安全程式碼

hyperupcalls的一個關鍵屬性是必須保證程式碼不會破壞Hypervisor。 為了使hyperupcall安全,它必須只能訪問由Hypervisor指示的受限記憶體區域,執行一段有限的時間而不會阻塞,休眠或鎖定,並且只能使用明確允許的Hypervisor服務。

由於Guest不受信任,Hypervisor必須建立一個保證這些安全屬性的安全機制。 我們可以選擇許多解決方案:軟體故障隔離(SFI)[70],攜帶證據的程式碼[51]或安全語言,如Rust。 為了實現hyperupcalls,我們選擇了增強型Berkeley Packet Filter(eBPF)VM。

我們選擇eBPF有幾個原因。 首先,eBPF相對成熟:BPF是在20多年前引入的,並且在整個Linux核心中廣泛使用,最初用於包過濾,但擴充套件到支援其他用例,如沙盒系統呼叫(seccomp)和核心事件跟蹤[34] 。 eBPF受到廣泛採用,並得到各種執行時的支援[14,19]。 其次,可以證明eBPF具有我們所需的安全屬性,並且Linux附帶驗證器和JIT,用於驗證和有效執行eBPF程式碼[74]。 最後,eBPF有一個LLVM編譯器後端,它使用編譯器前端(Clang)從高階語言生成eBPF位元組碼。 由於作業系統通常用C語言編寫,因此eBPF LLVM後端為我們提供了一種簡單的機制,可將不安全的Guest作業系統原始碼轉換為可驗證的安全eBPF位元組碼。

3.1.2從C到eBPF 一 框架

不幸的是,寫一個hyperupcall並不像在eBPF位元組碼中重新編譯OS程式碼那麼簡單。 但是,我們的框架旨在使編寫hyperupcalls的過程儘可能簡單和可維護。 該框架提供了三個關鍵功能,簡化了hyperupcalls的編寫。 首先,框架負責處理Guest地址轉換問題,因此guest OS符號可用於hyperupcall。 其次,該框架解決了eBPF對C程式碼施加了很大的限制的侷限性。 最後,框架定義了一個簡單的介面,它為 hyperupcall 提供了資料,因此可以高效,安全地執行。

Guest作業系統符號和記憶體。 即使hyperupcalls可以訪問guest虛擬機器的整個實體記憶體,訪問guest虛擬機器作業系統資料結構也需要知道它們駐留的位置。 作業系統通常使用核心地址空間佈局隨機化(KASLR)來隨機化OS符號的虛擬偏移,使其在編譯期間未知。 我們的框架通過使用地址空間屬性關聯指標並注入程式碼來調整指標,從而在執行時解析OS符號偏移。 當註冊hyperupcall時,guest虛擬機器提供實際的符號偏移,使hyperupcall開發人員能夠在C程式碼中引用OS符號(變數和資料結構),就像它們被核心執行緒訪問一樣。

全域性/本地Hyperupcalls。 並非所有的hyperupcall都需要及時執行。 例如,通知Guest機Hypervisor事件(例如VM進入/退出或中斷注入)的通知僅影響Guest機而不影響Hypervisor。 我們指的是隻影響將其註冊為guest虛擬機器的本地的hyperupcalls,以及影響整個Hypervisor作為全域性的hyperupcalls。 如果將超級呼叫註冊為本地,我們放寬時序要求並允許超級呼叫阻塞和休眠。 本地hyperupcalls在Guest的VCPU時間中與捕獲類似,因此行為不端的超級呼叫會對自己進行懲罰。

但是,全域性超級呼叫必須及時完成執行。 我們確保對於Guest作業系統,全域性hyperupcalls請求的頁面在超級呼叫期間被固定,並將可訪問的記憶體限制為Guest總實體記憶體的2%(可配置)。 由於本地hyperupcalls可能會阻塞,因此他們使用的記憶體不需要固定,允許本地hyperupcalls來解決所有·Guest記憶體。

解決eBPF限制。雖然eBPF具有表現力,但eBPF位元組碼的安全保證意味著它不是圖靈完備且有限的,因此只有一部分C程式碼可以編譯成eBPF。 eBPF的主要限制是它不支援迴圈,ISA不包含原子,不能使用自修改程式碼,函式指標,靜態變數,本機彙編程式碼,並且不能太長且複雜而無法驗證。

這些限制的後果之一是hyperupcall開發人員必須意識到hyperupcall的程式碼複雜性,因為複雜的程式碼將使驗證者失敗。 雖然這似乎是一個不直觀的限制,但其他使用BPF的Linux開發人員面臨同樣的限制,我們在框架中提供了一個輔助函式來降低複雜性,例如memset和memcpy,以及執行本機原子操作的函式,如CMPXCHG。 表3中顯示了這些輔助函式的選擇。此外,我們的框架掩蓋了記憶體訪問( 第 3.4 章 ),這大大降低了驗證的複雜性。 在實踐中,只要我們小心地展開迴圈,我們在使用 4096指令的設定和1024的堆疊深度 開發( 第 4 章 )中 的用例時沒有遇到驗證者問題 。

The Design and Implementation of Hyperupcalls 翻譯

Hyperupcall介面。 當Hypervisor呼叫hyperupcall時,它會填充一個上下文資料結構,如表4所示. hyperupupall接收一個事件資料結構,它指示呼叫回撥的原因,以及一個指向guest虛擬機器的指標(在Hypervisor的地址空間中,正在執行hyperupcall)。 當hyperupcall完成時,它可以返回一個值,該值可以由Hypervisor使用。

The Design and Implementation of Hyperupcalls 翻譯

編寫hyperupcall。 藉助我們的框架,作業系統開發人員編寫C程式碼,可以訪問作業系統變數和資料結構,並輔以框架的輔助功能。 典型的hyperupcall將讀取事件欄位,讀取或更新OS資料結構並可能將資料返回到Hypervisor。 由於hyperupcall是作業系統的一部分,開發人員可以引用作業系統本身使用的相同資料結構 - 例如,通過標頭檔案。 這大大增加了hyperupcalls的可維護性,因為資料佈局更改在OS源和hyperupcall源之間同步。

值得注意的是,hyperupcall不能直接呼叫guest虛擬機器作業系統函式,因為該程式碼尚未受到框架的保護。 但是,OS功能可以編譯為hyperupcalls並整合在經過驗證的程式碼中。

3.2編譯

一旦寫入了Hyperupcall,就需要將其編譯成eBPF位元組碼,然後Guest才能將其註冊到Hypervisor。 我們的框架通過Clang和eBPF LLUM後端執行hyperupcall C程式碼,生成此位元組碼作為Guest作業系統構建過程的一部分,並進行一些修改以協助地址轉換和驗證:

Guest記憶體訪問。 為了訪問Guest記憶體,我們使用eBPF的直接資料包訪問(DPA)功能,該功能旨在允許程式在不使用輔助功能的情況下安全有效地訪問網路資料包。 我們不是傳遞網路資料包,而是將Guest端視為“資料包”。 以這種方式使用DPA需要對eBPF LLUM後端進行錯誤修復[2],因為它是在假設資料包大小為G64KB的情況下編寫的。

地址翻譯。 Hyperupcalls允許Hypervisor無縫地使用Guest虛擬地址(GVA),這使得它看起來好像是在guest虛擬機器中執行了hyperupcall。 但是,程式碼實際上是由Hypervisor執行的,其中使用了主機虛擬地址(HVAs),使得Guest機指標無效。 為了允許在主機上下文中透明地使用Guest指標,因此需要將這些指標從GVA轉換為HVAs。 我們使用編譯器進行這些翻譯。

為簡化此轉換,Hypervisor將GVA範圍連續對映到HVA空間,因此可以通過調整基址輕鬆完成地址轉換。 由於guest虛擬機器可能需要hyperupcall來訪問多個連續的GVA範圍 - 例如,一個用於guest 1:1直接對映和OS文字部分[37] - 所以框架使用其各自的“地址空間”屬性來註釋每個指標。 我們擴充套件LLUM編譯器以使用此資訊來注入eBPF程式碼,該程式碼通過簡單的減法操作將每個指標從GVA轉換為HVA。 應當注意,生成的程式碼安全性不是由Hypervisor承擔的,並且在註冊Hyperupcall時被驗證。

繫結檢查。 驗證者拒絕直接記憶體訪問的程式碼,除非它可以確保記憶體訪問在“資料包”(在我們的例子中是Guest記憶體)邊界內。 我們不能指望hyperupcall程式設計師執行所需的檢查,因為新增它們的負擔很大。 因此,我們增強編譯器以自動新增在每次 記憶體訪問 之前執行繫結檢查的程式碼 ,從而允許驗證通過。 正如我們在3.4節中所述,邊界檢查是使用遮蔽完成的,而不是分支以簡化驗證。

上下文快取。 我們的編譯器擴充套件引入了內在函式來獲取指向上下文的指標或讀取其資料。 在回撥中經常需要上下文來呼叫輔助函式和轉換GVA。將上下文作為函式引數需要進行侵入式更改,並且可以防止guest虛擬機器與其Hyperupcall之間共享程式碼。 相反,我們使用編譯器將上下文指標快取在其中一個暫存器中,並在需要時檢索它。

3.3註冊

將hyperupcall編譯成eBPF位元組碼後,就可以註冊了。 guest可以隨時註冊hyperupcalls,但大多數hyperupcalls都是在Guest引導時註冊的。 guest提供hyperupcall事件ID,hyperupcall位元組碼和hyperupcall將使用的虛擬記憶體。 每個引數如下所述:

  • Hyperupcall事件ID。 要處理的事件的ID。
  • 記憶體註冊。 guest虛擬機器註冊hyperupcall使用的虛擬連續記憶體區域。 對於全域性hyperupcalls,此記憶體最多限制為guest虛擬機器總實體記憶體的2%(可由Hypervisor配置和實施)。
  • Hyperupcall位元組碼。 guest提供了一個指向hyperupcall位元組碼及其大小的指標。

3.4驗證

Hypervisor驗證每個hyperupcall在註冊時是否安全。 我們的驗證程式基於Linux eBPF驗證程式,並檢查hyperupcall的三個屬性:記憶體訪問,執行時指令數和使用的輔助函式。

理想情況下,驗證是合理的,確保只有安全的程式碼才能通過驗證,並且能夠完整、成功驗證任何安全程式。 雖然健全性不會因為可能危及系統安全而受到損害,但許多驗證系統(包括eBPF)都會犧牲完整性來保證驗證者的簡單性。 在實踐中,驗證者要求以某種方式編寫程式以通過驗證 [66] ,即使這樣,驗證也可能由於路徑爆炸而失敗。 這些限制與我們使hyperupcalls易於構建的目標不一致。

我們將討論下面的驗證程式檢查的屬性,以及我們如何簡化這些檢查以使驗證儘可能簡單。

有界執行時指令。 對於全域性hyperupcalls,eBPF驗證程式確保hyperupcall的任何可能執行都包含有限數量的指令,這些指令由Hypervisor設定(預設為4096)。 這可以確保Hypervisor可以及時執行hyperupcall,並且沒有無限迴圈可以導致hyperupcall不退出。

記憶體訪問驗證。 驗證器確儲存儲器訪問僅發生在由“分組”限定的區域中,該分組在超級呼叫中是在註冊期間提供的虛擬儲存器區域。 如前所述,我們增強編譯器以自動新增程式碼,證明每個記憶體訪問都是安全的驗證者。

但是,天真地新增這樣的程式碼會導致頻繁的驗證失敗。 當前的Linux eBPF驗證程式在驗證記憶體訪問安全性方面的能力非常有限,因為它要求它們之前會有比較和分支指令,以防止出現繫結訪問。 驗證者探索可能的執行路徑並確保其安全性。 雖然驗證者採用各種優化來修剪分支並避免走在每個可能的分支上,但驗證通常耗盡可用資源並且因為我們和其他人經歷過而失敗[65]。

因此,我們的增強編譯器不是使用compare和branch來確保記憶體訪問安全,而是新增了掩蓋每個範圍內的記憶體訪問偏移的程式碼,從而防止了越界記憶體訪問。 我們增強驗證程式以將此遮蔽識別為安全。 應用此增強功能後,我們編寫的所有程式都通過了驗證。

輔助功能安全。 Hyperupcalls可以呼叫輔助函式來提高效能並幫助限制執行時指令的數量。 輔助函式是標準的eBPF特性,驗證者強制執行可以呼叫的輔助函式,這些函式可能因事件而異,具體取決於Hypervisor策略。 例如,Hypervisor可能在記憶體回收期間不允許使用flush_tlb_vcpu,因為它可能會阻塞系統一段延長的時間。

驗證程式檢查以確保輔助函式的輸入是安全的,確保輔助函式僅訪問允許訪問的記憶體。 雖然可以在輔助函式中完成這些檢查,但新的eBPF擴充套件允許驗證程式靜態驗證輔助函式輸入。 此外,Hypervisor還可以基於每個事件設定輸入策略(例如,全域性超級呼叫的記憶體大小)。

輔助函式的數量和複雜性也應該受到限制,因為它們成為可信計算基礎的一部分。因此,我們僅介紹簡單的輔助函式,這些函式主要依賴於guest虛擬機器可以直接或間接觸發的程式碼,例如中斷注入。

eBPF安全性。最近發現的“幽靈”硬體漏洞[38,30]的兩個概念驗證漏洞攻擊目標是eBPF,這可能會引發對eBPF和高呼叫安全性的擔憂。如果攻擊者可以在特權上下文中執行非特權程式碼,那麼利用這些漏洞就更容易了,就像hyperupcalls那樣,可以防止發現的攻擊[63]。實際上,這些安全漏洞可能會使hyperupcall更具吸引力,因為當使用傳統的半虛擬機器制(如upcalls和hypercalls)進行上下文切換時,它們的緩解技術(例如,返回堆疊緩衝區填充[33])會產生額外的開銷。

3.5執行

已驗證的hyperupcalls安裝在每個guest虛擬機器hyperupcall表中。一旦註冊並驗證了超級呼叫,Hypervisor就會響應事件執行hyperupcall。

Hyperupcall補丁。為了避免測試hyperupcall是否已註冊的開銷,Hypervisor使用程式碼修補技術,在Linux中稱為“靜態金鑰”[12]:只有當hyperupcalls是已註冊的狀態時,才會在每個Hypervisor上的Hyperupcall呼叫程式碼上設定一個無操作指令。

訪問遠端VCPU狀態。一些hyperupcalls讀取或修改遠端VCPU的狀態。這些VCPU可能沒有執行,或者它們的狀態可能被Hypervisor的不同執行緒訪問。即使遠端VCPU被搶佔,Hypervisor也可能已經讀取了一些暫存器,並且在VCPU恢復執行之前不會期望它們發生變化。如果hyperupcall寫入遠端VCPU暫存器,它可能會破壞Hypervisor的常量甚至引入安全問題。

【vCPU代表虛擬中央處理單元。 將一個或多個vCPU分配給雲環境中的每個虛擬機器(VM)。 VM的作業系統將每個vCPU視為單個物理CPU核心。 如果主機具有多個CPU核心,那麼vCPU實際上由所有可用核心上的多個時隙組成,從而允許多個VM託管在較少數量的物理核心上。】

此外,讀取遠端VCPU暫存器會導致高開銷,因為VCPU狀態的一部分可能被快取在另一個CPU中,並且如果要讀取VCPU狀態,則必須首先將其寫回儲存器。更重要的是,在Intel CPU中,VCPU狀態不能通過公共指令訪問,並且必須首先“載入”VCPU,然後才能使用特殊指令(VMREAD和VMWRITE)訪問其狀態。切換載入的VCPU會產生很大的開銷,我們的系統大約需要1800個週期。

為了提高效能,我們定義了通常搶佔Hypervisor的同步點,並且已知訪問VCPU狀態是安全的。在這些點上,我們從VMCS“解密”VCPU暫存器並將它們寫入儲存器,以便hyperupcall可以讀取它們。超級呼叫寫入遠端VCPU暫存器並更新已分解的值以標記Hypervisor,以在恢復該VCPU之前將暫存器值重新載入到VMCS中。訪問遠端VCPU的Hyperupcalls以盡力而為的方式執行,僅在VCPU處於同步點時執行。在hyperupcall執行時,防止遠端VCPU恢復執行。

使用Guest作業系統鎖。一些OS資料結構受鎖保護。需要一致的Guest作業系統資料結構檢視的Hyperupcalls應遵守Guest作業系統規定的同步方案。然而,Hyperupcall只能機會性地獲取鎖,因為VCPU可能在持有鎖時被搶佔。可能需要調整鎖實現以支援外部實體的鎖定,而不是任何VCPU。釋放鎖可能需要相對較大的程式碼來處理慢速路徑,這可能會阻止及時驗證超級呼叫。

雖然可能會提出各種臨時解決方案,但似乎完整的解決方案要求Guest作業系統鎖定具有高上報功能。它還需要支援從eBPF程式碼呼叫eBPF函式,以避免可能導致驗證失敗的程式碼大小膨脹。由於最近新增了此支援,我們的實現不包括鎖支援。

4 用例和評估

我們的評估 由以下問題指導:

  • 使用經過驗證的程式碼(eBPF)與本機程式碼的開銷是多少?(章節4.1)
  • hyperupcalls 和其他半虛擬化機制的對比(章節4.3,4.2,4.5)?
  • hyperupcalls如何不僅可以提高效能(章4.3,4.2),而且安全性(章節4.5)和可除錯(章4.4 虛擬化環境的)?

測試平臺。我們的測試平臺包括一個帶有Intel ES-2670 CPU的48核雙插槽Dell PowerEdge 8630伺服器,一個希捷ST1200磁碟,它執行帶有Linux核心v4.8的Ubuntu 17.04。基準測試適用於具有16個VCPU和8GB RAM的guest虛擬機器。每次測量進行5次,並報告平均結果。

Hyperupcall原型。我們在Linux v4.8和KVM上實現了一個用於hyperupcall支援的原型,這是一個整合在Linux中的Hypervisor。 Hyperupcalls通過修補的LLVM 4進行編譯,並通過Linux核心eBPF驗證程式使用我們在第3章中描述的補丁進行驗證。我們啟用Linux eBPF“JIT”引擎, 它在驗證後將eBPF程式碼編譯為本機機器程式碼。已經研究了BPF JIT引擎的正確性並且可以驗證[74]。

用例。我們評估了表5中列出的四個hyperupcall用例。每個用例演示了在不同的hypervisor事件中使用hyperupcalls,並使用不同複雜度的hyperupcalls。

The Design and Implementation of Hyperupcalls 翻譯

4.1 Hyperupcall開銷

我們通過將hyperupcall與本機程式碼的執行時間與相同函式進行比較來評估使用經過驗證的程式碼來管理Hypervisor請求的開銷(表5)。總的來說,我們發現驗證程式碼相對於原生的絕對開銷很小(<250個迴圈)。對於處理TLB擊落到非活動核心的TLB用例,我們的hyperupcall執行速度比本機程式碼快,因為TLB重新整理被推遲。驗證hyperupcall的開銷很小。對於最長的hyperupcall(跟蹤),驗證花了67ms。

4.2 TLB Shootdown

雖然通常可以有效地完成向VCPU的中斷傳送,但是如果目標VCPU沒有執行則會有很大的損失。如果CPU過載,並且排程目標VCPU需要搶佔另一個VCPU,則會發生這種情況。對於同步處理器間中斷(IPI),傳送方僅在接收方指示IPI已交付和已接通後才恢復執行,從而導致過高的開銷。

在轉換後備緩衝器(TLB)擊落的情況下,IPI傳遞的開銷最為顯著,這是一種軟體協議,OS用於保持虛擬到實體地址對映相關的TLB快取。由於常見的CPU架構(例如,x86)不能使TLB在硬體中保持一致,因此修改對映的OS執行緒會將IPI傳送到可能快取對映的其他CPU,然後這些CPU會重新整理其TLB。 【TLB(轉換後備緩衝區)是從虛擬儲存器地址到物理儲存器地址的轉換快取。當處理器更改地址的虛擬到物理對映時,它需要告訴其他處理器使其快取中的對映無效。 這個過程被稱為“TLBshootdown】

我們使用hyperupcalls來處理這種情況,方法是註冊一個hyperupcall,當中斷傳遞給VCPU時,它會處理TLB擊落。Hypervisor在確保其處於靜止狀態後,使用中斷向量和目標VCPU提供超級呼叫。我們的hyperupcall檢查此向量是否是“遠端函式呼叫”向量以及函式指標是否等於OS TLB重新整理函式。如果是這樣,它執行此函式時只需要很少的修改:(1) 不使用本機指令重新整理TLB,而是使用輔助函式執行TLB重新整理,將其推遲到下一個VCPU重入; (2)即使禁用VCPU中斷也會執行TLB重新整理,因為實驗上它可以提高效能。

不可否認,還有另一種解決方案:引入一個將TLB重新整理委託給Hypervisor的超級呼叫[52]。雖然這種解決方案可以防止TLB重新整理,但它需要不同的程式碼路徑,這可能會引入隱藏的錯誤[43],使與OS程式碼的整合變得複雜或引入額外的開銷[44]。此解決方案也僅限於TLB重新整理,並且無法處理其他中斷,例如,重新安排IPI。

評估。我們使用預設的mpm_event模組在guest虛擬機器中執行Apache Web伺服器[23],該模組執行多執行緒工作程式來處理傳入的請求。為了衡量效能,我們使用ApacheBench,Apache HTTP伺服器基準測試工具,使用16個連線生成10k請求,並測量請求延遲。結果顯示在圖2中,顯示hyperupcalls將延遲減少了1.3 x。即使物理CPU沒有超額訂閱,效能也會提高,這似乎令人驚訝。但是,由於VCPU在此基準測試中通常暫時處於空閒狀態,因此它們也可以觸發對Hypervisor的退出。

The Design and Implementation of Hyperupcalls 翻譯

4.3丟棄可用記憶體

根據定義,可用記憶體不包含任何所需資料,可以丟棄。如果Hypervisor知道guest虛擬機器中有哪些記憶體空閒,它可以在記憶體回收,快照,實時遷移或鎖定步驟執行期間丟棄它[20]並避免I/O操作以儲存和恢復其內容。然而,關於哪些儲存器頁面是空閒的資訊由Guest持有,並且由於語義上的差異而不可用於Hypervisor。

多年來,已經提出了幾種機制來通知Hypervisor哪些儲存器頁面使用半虛擬化是空閒的。然而,這些解決方案要麼將Guest端與Hypervisor耦合[60];由於頻繁的超級呼叫引起的開銷[41]或僅限於實時遷移[73]。所有這些機制都受到固有的限制:沒有耦合Guest機和Hypervisor,Guest機需要與Hypervisor通訊哪些頁面是空閒的。

相反,支援hyperupcalls的Hypervisor不需要通知有關空閒頁面的資訊。相反,guest虛擬機器設定了一個hyperupcall,它根據頁面後設資料(Linux的結構頁面)描述頁面是否可丟棄,並且基於在Linux中的is_free_buddy_page函式。當Hypervisor執行可以從丟棄空閒Guest儲存器頁面(例如回收頁面)中受益的操作時,Hypervisor呼叫該超級呼叫來檢查該頁面是否是可丟棄的。當頁面已經被取消對映時,也會呼叫hyperupcall,以防止在不再空閒時丟棄它的競賽。

檢查是否可以丟棄頁面必須通過全域性超級呼叫來完成,因為必須在有限且短時間內提供答案。結果,guest虛擬機器只能註冊其部分記憶體以供hyperupcall使用,因為該記憶體從不被分頁以確保及時執行hyperupcall。我們的Linux guest虛擬機器註冊了頁面後設資料的記憶體 約佔Guest實體記憶體的1.6%。評價。為了評估“記憶體丟棄”hyperupcall的效能,我們測量其對因記憶體壓力而被回收記憶體的guest虛擬機器的影響。當記憶體不足時,Hypervisor可以執行“不合作的交換” - 直接回收Guest機記憶體並將其交換到磁碟。然而,這種方法通常會導致次優的回收決策。或者,Hypervisor可以使用memory ballooning,這是一種半虛擬機器制,其中Guest模組被告知主機記憶體壓力並導致Guest直接回收記憶體[71]。然後Guest可以做出知識淵博的回收決定並丟棄空閒頁面。雖然記憶體膨脹通常表現良好,但是當記憶體需要突然回收時效能會受到影響[4,6]或當Guest盤設定在網路附加儲存[68]上時,因此不在高記憶體壓力下使用[21]。

為了評估記憶體膨脹,不合作的交換和使用hyperupcalls進行交換,我們執行了一個場景,其中需要突然回收記憶體和物理CPU,以便容納新的guest虛擬機器。在Guest,我們開始並退出“memhog”,使4GB可用於在Guest回收。接下來,我們讓Guest忙執行與低記憶體佔用CPU密集型任務 一 的sysbench的CPU效能測試,它計算使用的所有虛擬處理器[39]素數。

現在,在系統繁忙的情況下,我們通過增加記憶體和CPU過量使用來模擬回收資源以啟動新guest虛擬機器的需求。我們降低了guest虛擬機器可用的物理CPU數量,並將其限制為僅1GB記憶體。我們根據為guest虛擬機器分配的物理CPU數來衡量回收記憶體所需的時間(圖3a)。這模擬了一個新的Guest開始。然後,我們停止增加記憶體壓力,並使用4GB的SysBench檔案讀取基準測量執行具有大記憶體佔用量的Guest應用程式的時間(圖3b)。這模擬了guest虛擬機器重用由Hypervisor回收的頁面。

The Design and Implementation of Hyperupcalls 翻譯

當物理CPU過量使用時,Ballooning會緩慢地回收記憶體(最多110秒),因為記憶體回收操作會在CPU時間與CPU密集型任務競爭。不合作的交換(交換基礎)可以更快地回收(32秒),但由於它不知道記憶體頁是否是空閒的,它會導致更高的Guest空閒頁面的開銷。相反,當使用hyperupcalls時,Hypervisor可以促進空閒頁面的回收並丟棄它們,從而回收記憶體的速度比Ballooning快8倍,而記憶體只有10%的減速。

當然,CPU過量使用並不是Ballooning無響應或無法使用的唯一情況。當記憶體壓力非常高時,Hypervisor會避免膨脹,而變成使用主機級交換[67]。 hyperupcalls可以與Ballooning協同執行:Hypervisor可以正常使用Ballooning,並在資源壓力高或Ballooning沒有響應時使用hyperupcalls。

4.4 Tracing

事件跟蹤是除錯正確性和效能問題的重要工具。但是,收集虛擬化工作負載的跟蹤有些限制。在guest虛擬機器內收集的跟蹤不會顯示Hypervisor事件,例如強制VM退出時,這會對其產生重大影響 效能。對於在Hypervisor中收集的跟蹤資訊,他們需要有關Guest作業系統符號的知識[15]。無法在雲環境中收集此類跟蹤。此外,每個跟蹤僅收集部分事件,並不顯示guest虛擬機器和虛擬機器監控程式事件如何交錯。

為了解決這個問題,我們在hyperupcall中執行Linux核心跟蹤工具ftrace [57]。 Ftrace非常適合在hyperupcall中執行。它簡單,無鎖,並且可以在多個上下文中啟用併發跟蹤:不可遮蔽中斷(NMI),硬體和軟體中斷處理程式以及使用者程式。因此,它很容易適應與Guest事件同時跟蹤Hypervisor事件。使用ftrace hyperupcall,guest虛擬機器可以在一個統一日誌中跟蹤Hypervisor和Guest事件,從而簡化除錯。由於跟蹤所有事件僅使用Guest邏輯,因此新的OS版本可以更改跟蹤邏輯,而無需更改Hypervisor。評價。跟蹤是有效的,儘管超級呼叫複雜度(3308 eBPF指令),因為大多數程式碼處理不常見的事件,處理跟蹤頁面填滿的情況。使用hyperupcalls跟蹤比使用本機程式碼232個週期要慢,這仍然比Hypervisor和Guest端之間的上下文切換時間短得多。 .

跟蹤是效能除錯的有用工具,可以暴露各種開銷[79]。例如,通過在VM-exit事件上註冊ftrace,我們看到許多程式(包括短期程式)由於CPUID指令的執行而觸發多個VM退出,這些指令列舉CPU功能並且必須由Hypervisor。我們發現大多數Linux應用程式使用的GNU C庫使用CPUID來確定支援的CPU功能。通過擴充套件Linux虛擬動態共享物件(vDSO)以便應用程式查詢支援的CPU功能而不觸發退出,可以防止這種開銷。

4.5核心自我保護

作業系統採用的一種常見安全加固機制是“自我保護”:作業系統程式碼和不可變資料防寫。但是,這種保護是使用頁表完成的,允許惡意軟體通過修改頁表條目來規避它。為防止此類攻擊,建議使用巢狀頁表,因為這些表無法從guest虛擬機器中訪問[50]。

但是,巢狀只能提供有限數量的策略,例如,不能將允許訪問受保護記憶體的Guest程式碼列入白名單。 Hyperupcalls更具表現力,允許Guest以靈活的方式指定記憶體保護。

我們使用hyperupcalls來提供Hypervisor級別的Guest核心自我保護,可以輕鬆修改它以適應複雜的策略。在我們的實現中,guest設定了一個標記受保護頁面的點陣圖,並在退出事件上註冊hyperupcall,它檢查退出原因,是否發生了記憶體訪問以及guest虛擬機器是否嘗試根據點陣圖寫入受保護的記憶體。如果嘗試訪問受保護的記憶體,則會觸發VM關閉。 guest虛擬機器在“頁面對映”事件上設定了另一個超級呼叫,該事件查詢Guest頁面幀所需的保護。此超級呼叫可防止虛擬機器監控程式主動預先取消Guest機記憶體。

評價。這種超級呼叫程式碼很簡單,但每次退出會產生43個週期的開銷。可以說,只有已經經歷過大量上下文切換的工作負載才會受到額外開銷的影響。現代CPU可以防止這種頻繁的切換。

5 結論

彌合語義差距是關鍵效能,並且Hypervisor可以為Guest提供高階服務。現在使用Hypercalls和upcalls來彌補差距,但它們有幾個缺點:Hypercalls不能由Hypervisor啟動,upcalls沒有有限的執行時,並且都會導致上下文切換的懲罰。內省,避免上下文切換的替代方案可能是不可靠的,因為它依賴於觀察而不是顯式介面。 Hyperupcalls通過允許guest虛擬機器將其邏輯暴露給Hypervisor來克服這些限制,通過使hyperupcall能夠直接安全地執行guest虛擬機器邏輯來避免上下文切換。

正文之後

老師上課點評的時候,認同了我所理解的第三方公證監視器的思想提取,真是amazing~~!!語義鴻溝問題是虛擬機器的一個很經典的問題,他必須存在但是又對效能有極大的影響。那麼為什麼不像是內省一樣在虛擬機器裡面放一個監視器呢?因為這樣會破壞虛擬機器的封裝性。虛擬機器必須要自我欺騙成我是一個完整的機器,而不是別的物理機的一個時隙,所以如果發現自己體內還存在一個監視器,會作何感想?!第三方的話,虛擬機器完全可以考慮成是對外的通訊。

相關文章