RFC-0082:在 Fuchsia 上執行未經修改的 Linux 程式 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 本文提出在 Fuchsia 上執行未經修改的 Linux 程式機制。 |
問題 | |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2021-02-11 |
審查日期 (年-月-日) | 2021-03-25 |
摘要
本文提出在 Fuchsia 上執行未經修改的 Linux 程式的機制。這些程式會在使用者空間程序中執行,其系統介面與 Linux ABI 相容。我們不會使用 Linux 核心實作這個介面,而是會在名為 starnix
的 Fuchsia 使用者空間程式中實作這個介面。基本上,starnix
會做為相容性層,將 Linux 用戶端程式的要求轉譯為適當的 Fuchsia 子系統。為了支援 Linux 系統介面所暗示的所有功能,這些子系統中的許多項目都需要加以詳述。
提振精神
目前,如要在 Fuchsia 上執行,軟體必須從原始碼重新編譯,以便指定 Fuchsia 為目標。為了減少在 Fuchsia 上執行所需的來源修改量,Fuchsia 提供可供此軟體指定的 POSIX 相容性層,即 POSIX Lite。POSIX Lite 會以用戶端程式庫的形式,在底層 Fuchsia System ABI 上層疊。
不過,POSIX Lite 並非完整的 POSIX 實作。舉例來說,POSIX Lite 不包含暗示可變全域狀態 (例如 kill
函式) 的 POSIX 部分,因為 Fuchsia 是以物件能力規範為設計基礎,而該規範會避免可變全域狀態,以提供強大的安全性保證。相反地,使用 POSIX Lite 的軟體需要修改,才能在這些用途 (例如 zx_task_kill
函式) 中直接使用 Fuchsia 系統介面。
這個方法到目前為止運作良好,因為我們可以存取在 Fuchsia 上執行的軟體所需的原始碼,這讓我們可以重新編譯 Fuchsia 系統 ABI 的軟體,以及修改需要調整至物件能力系統的軟體部分。
隨著我們希望在 Fuchsia 上執行的軟體種類不斷擴大,我們發現有軟體無法在 Fuchsia 上執行,也無法重新編譯。舉例來說,Android 應用程式包含已為 Linux 編譯的原生程式碼模組。為了在 Fuchsia 上執行這項軟體,我們需要能夠在不修改的情況下執行二進位檔。
設計
在 Fuchsia 上執行 Linux 二進位檔最直接的方式,就是在虛擬機器中執行這些二進位檔,並將 Linux 核心做為虛擬機器中的訪客核心。不過,由於訪客程式與其他 Fuchsia 系統執行的作業系統不同,因此很難將訪客程式與其他 Fuchsia 系統整合。
Fuchsia 的設計可讓您自行提供執行階段,也就是說,Fuchsia 系統不會強制對元件的內部結構做出意見。為了與 Fuchsia 系統以一流的互通性進行互動,元件只需透過適當的 zx::channel
物件傳送及接收格式正確的訊息即可。
starnix
會在 Fuchsia 中原生建立 Linux 執行階段,而非在虛擬機器中執行 Linux 二進位檔。具體來說,Linux 程式可以搭配元件資訊清單進行包裝,該資訊清單會將 starnix
識別為該元件的執行程式。系統會將 Linux 程式的二進位檔交給 starnix
執行,而非直接使用 ELF Runner。
為了執行特定的 Linux 二進位檔,starnix
會手動建立 zx::process
,並使用與 Linux ABI 相符的初始記憶體配置。舉例來說,starnix
會將 argv
和 environ
填入程式,做為初始執行緒堆疊 (連同 aux
向量) 上的資料,而非 Bootstrap 管道上的訊息,因為這類資料會在 Fuchsia 系統 ABI 中填入。
系統呼叫
將二進位檔載入用戶端程序後,starnix
會註冊來處理來自用戶端程序的所有系統呼叫 (請參閱下方的「系統呼叫機制」)。每當用戶端發出系統呼叫時,Zircon 核心會將控制權轉移至 starnix
,後者會根據 Linux 系統呼叫慣例解碼系統呼叫,並執行系統呼叫的工作。
舉例來說,如果用戶端程式發出 brk
系統呼叫,starnix
會使用適當的 zx::vmar
和 zx::vmo
作業操控用戶端程序的位址空間,藉此變更用戶端程序的程式中斷點位址。在某些情況下,我們可能需要詳細說明某個程序的功能 (例如starnix
) 來操控其他程序 (即用戶端) 的位址空間,但早期實驗顯示 Zircon 已包含遠端位址空間操控所需的大部分機制。
舉另一個例子來說,假設用戶端程式發出 write
系統呼叫。為了實作檔案相關功能,starnix
會為每個用戶端程序維護一個檔案描述元資料表。收到 write
系統呼叫後,starnix
會在用戶端程序的檔案描述符表中查詢已識別的檔案描述符。通常,該檔案描述元會由實作 fuchsia.io.File
FIDL 通訊協定的 zx::channel
提供支援。為了執行 write
,starnix
會格式化 fuchsia.io.File#Write
訊息,其中包含來自用戶端位址空間的資料 (請參閱「記憶體存取」),並透過管道傳送該訊息,類似 POSIX Lite 在用戶端程式庫中實作 write
的方式。
全域狀態
為了處理暗示可變動全域狀態的系統呼叫,starnix
會在用戶端程序之間共用一些可變動狀態。舉例來說,starnix
會為執行的每個用戶端程序指派 pid_t
,並維護一張將 pid_t
對應至該程序的基礎 zx::process
句柄的表格。為了實作 kill
系統呼叫,starnix
會在這個表格中查詢指定的 pid_t
,並針對相關聯的 zx::process
句柄發出 zx_task_kill
系統呼叫。
如此一來,每個 starnix
例項都會做為相關 Linux 程序的容器。如果我們希望在兩個 Linux 程序之間實現強烈的隔離保證,可以透過不同的 starnix
例項執行這些程序,而不會產生執行多個虛擬機器的額外負擔 (例如排程複雜度)。
每個 starnix
例項也會公開其全域狀態,供其他 Fuchsia 程序使用。舉例來說,starnix
會維護 AF_UNIX
網路連線的命名空間。這個命名空間可透過 starnix
執行的 Linux 二進位檔,以及透過 FIDL 與 starnix
通訊的 Fuchsia 二進位檔存取。
Linux 系統介面也暗示全域檔案系統。由於 Fuchsia 沒有全域檔案系統,starnix
會從其專屬命名空間為其用戶端程序合成「全域」檔案系統。舉例來說,starnix
會在提供給用戶端程序的全域檔案系統中,將 /data/root
從其自身命名空間掛載為 /
。其他掛載點 (例如 /proc
) 可由 starnix
在內部實作,例如查詢其執行程序表。
安全性
starnix
會盡可能以基礎 Fuchsia 系統的安全機制為基礎。舉例來說,當您與檔案系統、網路和圖形等系統服務進行介面連線時,starnix
將主要充當轉譯層,將 Linux ABI 的請求重新格式化為 Fuchsia 系統 ABI。系統服務將負責執行自己的安全性不變量,就像為其他用戶端執行一樣。不過,starnix
需要實作一些安全機制,才能保護自身服務的存取權。舉例來說,starnix
需要判斷一個用戶端程序是否可 kill
另一個用戶端程序。
為了做出這些安全性決策,starnix
會追蹤每個用戶端程序的安全性內容,包括 uid_t
、gid_t
、有效的 uid_t
和有效的 gid_t
。需要安全性檢查的作業會使用這個安全性情境來做出適當的存取控管決策。我們預期這項機制一開始的使用頻率不高,但隨著使用情境越來越複雜,對存取權限的需求也可能會越來越複雜。
她說
在決定 starnix
應在特定情況下如何運作時,設計會盡可能讓其運作方式與 Linux 相近。目的是建立 Linux 介面的實作項目,以便執行現有的未修改 Linux 二進位檔。每當 starnix
與 Linux 語意不同時,就有可能導致部分 Linux 二進位檔發現差異,並出現不當行為。
為了更輕鬆地討論這項設計原則,我們說 starnix
會實作 Linux ,也就是說,它會保留真實 Linux 系統的所有美麗、醜陋、巧合和怪異之處。
在某些情況下,如果要實作 Linux 介面,就必須在 Fuchsia 服務中加入功能,以提供必要的語意。舉例來說,實作 inotify
時,需要底層檔案系統實作項目的支援,才能有效運作。我們應以可與服務公開的其他功能順利整合的方式,將這項功能新增至 Fuchsia 服務。
實作
我們打算將 starnix
實作為 Fuchsia 元件,具體來說是實作 runner 通訊協定的一般使用者空間元件。我們打算在 Rust 中實作 starnix
,以避免從用戶端程序提升至 starnix
程序的權限。
管理階層
starnix
的其中一個核心部分是執行緒,可在 Linux 系統介面中實作語意概念。舉例來說,執行緒會包含代表執行緒、程序和檔案說明的物件。
執行緒的結構可獨立於其他 starnix
系統,進行單元測試。舉例來說,我們可以進行單元測試,確認複製檔案描述元時會共用底層檔案描述,而不需要使用 Linux ABI 執行程序。
Linux 系統呼叫定義
為了實作 Linux 系統呼叫,starnix
需要每個 Linux 系統呼叫的說明,以及任何相關聯輸入或輸出參數的使用者空間記憶體配置。這些定義位於 Linux uapi
中,這是 C 標頭的獨立收集。為了在 Rust 中使用這些定義,我們會使用 Rust bindgen
產生 Rust 宣告。
Linux uapi
會隨著時間演進。我們會先以 Linux 5.10 LTS 為目標的 Linux uapi
,但日後可能需要調整所支援的 Linux uapi
確切版本。
Syscall 機制
starnix
的初始實作會使用 Zircon 例外狀況,擷取來自用戶端程序的系統呼叫。具體來說,每當用戶端程序嘗試發出系統呼叫時,Zircon 就會拒絕該系統呼叫,因為 Zircon 要求系統呼叫必須從 Zircon vDSO 中發出,而用戶端程序並未知曉該 vDSO 的存在。
Zircon 會產生 ZX_EXCP_POLICY_CODE_BAD_SYSCALL
例外狀況,藉此拒絕這些系統呼叫。starnix
程序會在每個用戶端程序上安裝例外狀況處理程序,以便擷取這些例外狀況。為了接收系統呼叫的參數,starnix
會使用 zx_thread_read_state
從產生例外狀況的執行緒讀取註冊。處理系統呼叫後,starnix
會使用 zx_thread_write_state
設定系統呼叫的傳回值,然後在用戶端程序中繼續執行執行緒。
這個機制可運作,但效能不太可能達到足以發揮效用的程度。建立足夠的 starnix
來執行 Linux 基準測試後,我們可能會想用更有效率的機制取代此系統呼叫機制。舉例來說,starnix
可能會連結 zx::port
,以便處理來自用戶端程序的系統呼叫,而 Zircon 會將封包排入佇列,並將用戶端程序的註冊狀態傳送至 zx::port
。有了基準後,我們就能製作各種方法的原型,並在那時選出最佳設計。
記憶體存取
starnix
的初始實作會使用 zx_process_read_memory
和 zx_process_write_memory
,從用戶端程序的位址空間讀取及寫入資料。這個機制可行,但有兩個缺點:
- 基於安全考量,這些系統呼叫會在正式版版本中停用。
- 這些系統呼叫的成本遠高於直接讀取及寫入記憶體。
建構足夠的 starnix
來執行 Linux 基準測試後,我們會將這個機制換成更有效率的機制。舉例來說,starnix
可能會限制用戶端位址空間的大小,並將每個用戶端的位址空間對應至其專屬位址空間,並在某些用戶端專屬偏移處進行對應。或者,當 starnix
服務用戶端的系統呼叫時,Zircon 會安排讓該用戶端的位址空間可從該執行緒中看到 (例如,當核心執行緒為用戶端程序服務系統呼叫時,可在該程序的位址空間中看到)。
與系統呼叫機制一樣,我們可以為各種方法建立原型,並在取得更多可用於評估方法的執行程式碼後,選出最佳設計。
互通性
我們將採用以測試為導向的方法開發 starnix
。一開始,我們會使用簡單的實作方式,足以執行基本的 Linux 二進位檔。我們已製作原型,實作可執行 hello_world.c
程式的 -static-pie
版本。下一步是清除該原型,並教導 starnix
如何執行動態連結的 hello_world.c
二進位檔。
執行這些基本二進位檔後,我們會從各種程式碼集取得單元測試二進位檔。這些二進位檔有助於確保 Linux ABI 的導入方式正確無誤 (也就是 Linux 是 spoke)。舉例來說,我們會執行 Android 來源樹狀結構中的某些低階測試二進位檔,以及 Linux 測試專案中的二進位檔。
成效
效能是這個專案的重要一環。一開始,starnix
的效能會相當低落,因為我們會使用效率不彰的機制來擷取系統呼叫和存取用戶端記憶體。不過,一旦我們有足夠的功能可在 Linux 執行環境中執行基準測試,這些領域就應該可以大幅改善。
除了最佳化這些機制之外,我們也有機會將高頻率作業卸載至用戶端。舉例來說,我們可以直接在用戶端位址空間中實作 gettimeofday
,方法是在將控制權轉移至 Linux 二進位檔之前,先將程式碼載入用戶端程序。舉例來說,如果 Linux 二進位檔透過 Linux vDSO 叫用 gettimeofday
,starnix
可以提供共用程式庫,取代直接透過 Zircon vDSO 實作 gettimeofday
的 Linux vDSO。
安全性考量
這項提案涉及許多細微的安全性考量。starnix
程序與用戶端程序之間有信任界線。具體來說,starnix
程序可保留未完全公開給用戶端的物件功能。舉例來說,starnix
程序會為每個用戶端程序維護一個檔案描述元資料表。一個用戶端程序應可存取檔案描述符表中儲存的句柄,但無法存取其他程序的檔案描述符表中儲存的句柄。同樣地,starnix
會維護共用的可變動狀態,讓用戶端只可與受存取權控管的對象互動。
為提供此信任範圍,starnix
會在與用戶端程序不同的使用者空間程序中執行。為避免權限提升,我們打算在 Rust 中實作 starnix
,並使用 Rust 的型別系統避免型別混淆。我們也打算使用 Rust 的類型系統,清楚區分用戶端資料 (例如用戶端位址空間中的位址,以及從用戶端位址空間讀取的資料) 與 starnix
本身維護的可靠資料。
此外,我們需要考量 Linux 二進位檔本身的來源,因為 starnix
會直接執行這些二進位檔,而不是在虛擬機器或 SFI 容器中執行。我們需要在涉及 Linux 二進位檔的特定端對端產品用途情境中,重新考慮這項考量。
starnix
中的存取控制機制需要進行詳細的安全性評估,最好是讓安全性團隊直接參與設計,甚至是實作。我們預計一開始會提供簡單的存取權控制機制。隨著這項機制的規定越來越嚴格,我們需要進一步審查安全性。
最後,高效能系統呼叫和用戶端記憶體機制的設計需要仔細的安全檢查,特別是如果我們最終使用 starnix
的特殊位址空間設定,或嘗試直接將註冊狀態從用戶端執行緒轉移至 starnix
執行緒。
隱私權注意事項
這項設計沒有任何立即的隱私權考量。不過,一旦我們有涉及 Linux 二進位檔的特定端對端產品用途,就必須評估該用途對隱私權的影響。
測試
測試是建構 starnix
的核心環節。我們會直接對 starnix
執行程序進行單元測試。我們也會嘗試傳遞要在 Linux 上執行的測試二進位檔,藉此建構 Linux 系統介面的實作方式。接著,我們會在持續整合中執行這些二進位檔,確保 starnix
不會退化。
我們也會比較在 starnix
中執行 Linux 二進位檔,以及在 Fuchsia 虛擬機器中執行相同的二進位檔。我們預期可以在 starnix
中更有效率地執行 Linux 二進位檔,但我們應該驗證這個假設。
說明文件
在這個階段,我們預計透過此 RFC 記錄 starnix
。一旦我們讓非簡單的二進位檔執行,就需要記錄如何在 Fuchsia 上執行 Linux 二進位檔。
缺點、替代方案和未知事項
我們可以探索許多設計空間,瞭解如何在 Fuchsia 上執行未經修改的 Linux 二進位檔。本節將概述主要的設計決策。
Linux 核心
重要的設計選擇是,是否要使用 Linux 核心本身來實作 Linux 系統介面。除了建構 starnix
,我們也會建立機制,在 Machina 虛擬機器中執行 Linux 核心,以便執行未修改的 Linux 二進位檔。由於 Linux 核心的設計目的是在虛擬機器中執行,且 Linux 核心已包含構成 Linux 系統介面的數百個系統呼叫的實作項目,因此這種方法實作負擔較小。
我們可以透過多種方式使用 Linux 核心。舉例來說,我們可以在虛擬機器中執行 Linux 核心,也可以使用使用者模式 Linux (UML),或是使用Linux 核心程式庫 (LKL)。不過,無論執行方式為何,為了執行 Linux 二進位檔而執行整個 Linux 核心,都會造成大量成本。從根本上來說,Linux 核心的工作就是減少高階作業 (例如 write
) 到低階作業 (例如 將 DMA 資料傳送至底層硬體。這項核心功能會導致將 Linux 二進位檔整合至 Fuchsia 系統時事與願違。我們希望將 write
作業轉換為 fuchsia.io/File.Write
作業,而非將 write
作業縮減為 DMA,因為這兩者在語意層級上是等同的。
同樣地,Linux 核心也提供排程器,用於控制其管理的程序中的執行緒。這項功能的目的,是將高階作業 (例如執行 12 個並行執行緒) 縮減為低階作業 (例如在這個處理器上執行這個時間片段)。同樣地,這項核心功能會適得其反。如果每個 Linux 二進位檔執行的執行緒實際上是 Zircon 執行緒,並由與系統中所有其他執行緒相同的排程器排程,我們就能為整個系統計算出更佳的排程。
環境
決定直接使用 Fuchsia 系統實作 Linux 系統介面後,我們需要選擇執行該實作的所在位置。
程序內
我們可以在與 Linux 二進位檔相同的程序中執行實作項目。舉例來說,POSIX Lite 會使用這種方法,將 POSIX 作業轉譯為 Fuchsia 作業。不過,在執行未經修改的 Linux 二進位檔時,這種做法不太理想,原因有二:
如果我們在程序中執行導入作業,就必須從 Linux 二進位檔中「隱藏」導入作業,因為 Linux 二進位檔不會預期系統會在其程序中執行 (太多) 程式碼。舉例來說,實作項目使用執行緒局部儲存空間時,必須注意不要與 Linux 二進位檔 C 執行階段管理的執行緒局部儲存空間衝突。
Linux 系統介面的許多部分都暗示可變動的全域狀態。在程序中實作仍需要與程序外伺服器協調,才能正確實作介面的各個部分。
基於這些原因,我們選擇從外部程序伺服器實作開始。不過,我們可能會將部分作業從伺服器卸載至用戶端,以提升效能。
使用者空間
在這種做法中,實作項目會在 Linux 程序的獨立使用者空間程序中執行。我們為 starnix
選用的就是這種做法。這種做法的主要挑戰在於,我們需要仔細設計用於系統呼叫和用戶端記憶體存取的機制,才能提供足夠的效能。由於我們需要執行額外的內容切換才能進入該程序,因此涉及第二個使用者空間程序會產生一些不可避免的額外負擔,但其他系統的證據顯示,我們可以達到出色的效能。
核心
最後,我們可以在核心中執行實作項目。這是為作業系統提供外國個性的傳統做法。不過,我們希望避免採用這種做法,以便降低核心的複雜度。只要核心遵循明確的物件能力規範,就能更輕鬆地推論核心的行為,進而提升安全性。
相較於使用者空間實作,核心內實作提供的主要優勢是效能。舉例來說,核心可以直接接收系統呼叫,而且已具備高效能機制,可與用戶端位址空間互動。如果我們能夠透過使用者空間方法達到優異的效能,就沒有理由在核心中執行實作項目。
非同步信號
Linux 二進位檔預期核心會在非同步信號處理常式中執行部分程式碼。Fuchsia 目前不包含在程序中直接叫用程式的機制,也就是說,沒有明確的機制可用於叫用非同步信號處理常式。一旦遇到需要支援非同步信號處理常式的 Linux 二進位檔,我們就必須設計出支援該功能的方法。
Futex
Fuchsia 和 Linux 上的 Futex 運作方式不同。在 Fuchsia 上,futex 會以虛擬位址為依據,而 Linux 則提供以實體位址為依據的 futex 選項。此外,Linux futexe 提供 Fuchsia futexe 無法提供的多種選項和作業。
為了實作 Linux futex 介面,我們需要在 starnix
中實作 futex,或是在 Zircon 核心中新增功能,以支援 Linux 二進位檔所需的功能。
既有技術與參考資料
在非 POSIX 系統上執行 Linux (或 POSIX) 二進位檔的先前技術已相當成熟。本節將說明兩個相關系統。
WSL1
本文件中的設計類似於第一個 Windows Subsystem for Linux (WSL1),這是在 Windows 上實作 Linux 系統介面,可執行未修改的 Linux 二進位檔,包括 Ubuntu、Debian 和 openSUSE 等完整的 GNU/Linux 發行版。與 starnix
不同,WSL1 會在核心中執行,並為 NT 核心提供 Linux 個性。
很遺憾,WSL1 受到 NTFS 的效能特性所阻礙,而這項特性與 Linux 軟體的預期不符。自此之後,Microsoft 已將 WSL1 換成 WSL2,後者會在虛擬機器中執行 Linux 核心,提供類似的功能。在 WSL2 中,Linux 軟體會針對 ext4
檔案系統執行,而非 NTFS 檔案系統。
我們從 WSL1 學到的重要警示是,starnix
的效能將取決於 starnix
向用戶端程式公開的基礎系統服務效能。舉例來說,如果我們希望 Linux 軟體在 Fuchsia 上有良好的效能,就必須提供與 ext4
相近的檔案系統實作方式。
QNX Neutrino
QNX Neutrino 是採用商業微核心的作業系統,可提供高品質的 POSIX 實作。本文件針對 starnix
所述的方法與 QNX 中的 proc
伺服器類似,後者會服務用戶端程序的 POSIX 呼叫,並維護 POSIX 介面所暗示的可變動全域狀態。與 starnix
類似,proc
是 QNX 上的使用者空間程序。