| RFC-0264:在 Fuchsia 上執行未修改的 AArch32 Linux 程式 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 透過受限模式和 starnix,啟用未修改且不受信任的 AArch32 客戶端程式碼。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2024-11-05 |
| 審查日期 (年-月-日) | 2025-01-21 |
問題陳述
安全且有效率 的外國 ABI 支援,可讓 AArch64 Linux 程式在未經修改的情況下執行。我們持續擴充希望在 Fuchsia 上執行的軟體領域,因此遇到需要能夠執行但未重新編譯的 32 位元 ARM (AArch32 ISA) Linux 程式,或可從針對 AArch32 Linux ABI 執行的程式充分獲益。
這個問題不會擴大到在 Fuchsia 程式中啟用任何其他 ABI 支援。
摘要
本文件建議新增 AArch32 外部 ABI 支援,以便執行未修改的 32 位元 ARM ISA Linux 程式。以 Zircon 的高效使用者空間核心模擬為基礎,來自 32 位元 AArch32 使用者空間的例外狀況會重新導向至 starnix。Starnix 將負責實作所有必要的 AArch32/Linux 外來 ABI 功能。
利害關係人
這項提案會影響專案的幾個領域,審查人員會反映相關情況:
- Zircon:核心必須安全且永續地支援 AArch32 例外狀況。
- Starnix:負責大部分的外部 ABI 相容性。
- 工具鍊:這會新增但限制編譯
AArch32 目標的需求。 - 基礎架構和測試:因為這會新增硬體和/或模擬
排列組合。
協助人員:
- hjfreyer@
審查者:
- maniscalco@
- lindkvist@
- mcgrathr@
- jamesr@
- mvanotti@
已諮詢:
- travisg@
- phosek@
- abarth@
- tkilbourn@
- olivernewman@
- ajkelly@
社交:
我們首先著手開發這項提案的技術原型,以驗證技術可行性,並進行初步成本評估。我們已將原型設計和這份文件的草稿,分享給受影響元件的代表。原型實作可更準確評估潛在的短期和長期影響,而設計審查則促使我們擴大討論設計選擇,並發現許多現已解決的問題。
需求條件
潛在的引導需求有很多,但選定問題陳述後,後續設計會套用下列需求:
Starnix 中的 AArch32 支援必須:
- 能夠剖析及載入現代 armv7-linux 二進位檔
- 能夠管理個別的 AArch32 系統呼叫路徑
- 合理地受到限制,不會影響整個 Starnix 中的程式碼
- 可停用 (例如透過建構時間旗標)
- 對現有的 AArch64 程式碼路徑影響極小
- 能夠在同一個 starnix 執行個體上執行 AArch64 和 AArch32 程式
請注意,雖然 AArch64 和 AArch32 軟體可能會在同一個 starnix 核心例項中平行執行,但目前我們並未提供這兩個環境互動的行為保證。
Zircon 中的 AArch32 支援必須:
- 僅適用於受限模式的執行緒
- 允許共用程序同時代管 AArch32 或 AArch64 受限模式程式碼
- 不會造成過度的長期維護負擔
- 盡量減少 API 介面變更
- 可輕鬆移除和/或停用
- 不會影響在 Zirecon 下執行的所有工作
- 不需要大幅變更 Zircon
雖然日後可能需要其他 Fuchsia 支援的架構,但目前我們希望執行的程式集是以 AArch32 環境為目標,例如 Debian 或 Android。我們的目標並非提供 armv7-linux 的完美模擬,而是以 starnix 的 API 層為基礎,支援公開的處理器 AArch32 功能。
設計
Starnix 可與 Linux 核心相容。Linux 核心已支援所謂的「相容」模式。啟用 CONFIG_COMPAT 後,64 位元 Linux 核心就能執行主機 CPU 架構支援的 32 位元或 64 位元 ISA 目標二進位檔。如果是 Arm 處理器,則為 AArch64 和 AArch32。
一般而言,CONFIG_COMPAT 可讓 32 位元架構執行。這項設計的目的是在 Starnix 上啟用相同行為,讓 Starnix 上的 Android 能夠執行 32 位元 (AArch32) 或 64 位元 (AArch64) ARM 二進位檔。在 Fuchsia 上實現這個目標與在 Linux 上不同,且需要變更 Zircon 和 Starnix。本文將從硬體開始,逐步向上說明。
Zircon
Zircon 是 Fuchsia 的核心,可透過受限模式啟用 Starnix。
啟用 AArch32 支援的方法是:(1) 判斷處理器的功能,(2) 確保受限模式可在進入時啟用 AArch32 執行作業,以及 (3) 確保所有例外狀況、錯誤或其他情況都會導向受限模式管理員 (starnix),同時盡量減少對 Zircon 的其他影響。
處理器支援
如前所述,ARM 架構提供稱為 AArch32 的執行狀態,可與較舊的 32 位元 ARM ISA 回溯相容,適用於較新架構版本 (ARMv8 以上) 的處理器。
ARM 架構提供相對明確的路徑,可實作 AArch32 的支援功能,不會影響現代核心,且除了最後一個 32 位元架構版本 ARMv7 之外,還能啟用一小組額外功能。下方各節將說明啟用 AArch32 執行作業所需的處理器功能。
本節其餘內容大多引用或改寫 ARM 技術指南和參考手冊的相關章節,這些章節與 Zircon 和 Starnix 的後續變更有關。
偵測 AArch32 支援
ARM 架構的最新修訂版本會公開處理器功能暫存器 ID_PFR0_EL1,以支援 AArch32。這個暫存器中的值會決定支援哪些 AArch32 功能 (如有)。許多適用於消費型裝置的 64 位元 ARM 晶片都支援 A32 (State0) 和 T32 (State1) 指令集 (如 ARMv7 支援)。為支援 Linux 的 32 位元相容使用者空間,這兩組指令集是必要的。請務必注意,許多以伺服器為導向的 64 位元 ARM 晶片不支援任何 AArch32 功能,但如果支援,兩組都會提供。
核心暫存器對應
| AArch32 | AArch64 |
|---|---|
| R0-R12 | X0-X12 |
| Banked SP 和 LR | X13-X23 |
| 已存入銀行的 FIQ | X24-X30 |
在大多數情況下,暫存器的架構對應是無縫的。AArch32 有 16 個預先宣告的核心暫存器,而 AArch64 有 32 個預先宣告的核心暫存器。AArch32 暫存器會對應至 AArch64 通用暫存器庫的前 16 個暫存器:x0-15 是 r0-15。根據 ARMv8 的 64 位元架構總覽,從 AArch32 存取暫存器時,系統會忽略或歸零高 32 位元:
- 系統會忽略來源暫存器的前 32 位元。
- 目的地暫存器的前 32 位元會設為零。
- 指令設定的條件旗標是從較低的 32 位元計算而來。
從 AArch64 刻意將這些暫存器存取為 AArch32 暫存器時,通常會使用 W 而不是 X,以免值過時。
請注意,程式計數器 (PC) 會對應至暫存器 R15,堆疊指標 (SP) 會對應至 R13,而連結 暫存器 (LR) 則會對應至 R14。
除了核心暫存器,目前的程式狀態暫存器 (CPSR) 對映在 AArch32 和 AArch64 互動中也扮演重要角色。下一節會介紹其對應關係。
目前程式狀態暫存器 (CPSR)
在 ARMv7 和更早版本中,只有一個暫存器,也就是目前的程式狀態暫存器 (CPSR),其中包含:
-
APSR 旗標,也就是
- N、Z、C 和 V 標記。
- Q (飽和度) 旗標。
- GE (大於或等於) 旗標。
- 目前的處理器模式。
- 中斷停用旗標。
- 目前的處理器狀態,也就是 ARM、Thumb、ThumbEE 或 Jazelle。
- 位元組順序。
- IT 區塊的執行狀態位元。
在 AArch64 中,這項資訊會分割並以不同方式存取 (但 PSTATE 基本上是重新命名的 CPSR)。不過,如果發生例外狀況,資訊會顯示在已儲存的程式狀態暫存器 (SPSR) 中,該暫存器會儲存:
- N、Z、C 和 V 標記。
- D、A、I 和 F 會中斷停用位元。
- 暫存器寬度。
- 執行模式。
- IL 和 SS 位元。
在架構上,AArch32 CPSR 會對應至例外狀況的 SPSR,適用於 AArch64。這會將 AArch32 CPSR 的 AArch32 專用位元儲存在 SPSR 的保留區域:
| 31 27 | 26 25 | 24 | 23 20 | 19 16 | 15 10 | 9 | 8 | 7 | 6 | 5 | 4 0 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| N Z C V Q | IT | J | 已預訂 | GE | IT | E | A | I | F | T | M |
| [1:0] | [3:0] | [7:2] | [4:0] |
在 AArch64 狀態下,IT、Q 和 GE 旗標無法讀取或寫入。 在 AArch32 中,GE 旗標是由平行加法和減法指令設定,而 Q 則會設為指出溢位或飽和。 Thumb 指令會使用 IT 位元進行條件式執行,在 A32 模式中應為 0,在 T32 模式中則應在例外狀況中保留。
執行模式和處理器狀態
在 ARMv8 和 ARMv9 中,執行模式 (上圖 SPSR/CPSR 中的 M[4]) 可設為指出程式是否處於 AArch32 模式。執行模式只能在重設或例外狀況層級變更。
在 AArch64 執行模式下,只能使用 A64 指令集。在 AArch32 中,僅支援 A32 和 T32 指令。啟用的指令集稱為處理器狀態,可使用 T 位元 (即 Thumb 執行狀態位元) 切換。在使用者空間中,這項變更是使用分支 (BX/BLX) 執行。任何核心軟體都應確保 PC 對齊方式與目前的指令集對齊方式一致。
浮點和進階 SIMD 暫存器
除了核心暫存器外,進階 SIMD 和浮點指令共用相同的暫存器庫。其中包含三十二個 64 位元暫存器,較小的暫存器會封裝到較大的暫存器中,如同 ARMv7 和更早的版本,以 AArch32 的 VFPv4 為基礎。
為符合上述要求,我們的目標是支援 ARMv7 提供的 ASIMD 和 VFP 擴充功能,但這些擴充功能在 AArch32 模式中受到支援。不一定會支援 AArch64 中的其他狀態,以及未來的 AArch32 ISA 擴充功能。
其他暫存器對應:計數器和執行緒
除了已討論的對應之外,還有許多其他暫存器對應,適用於 AArch32 和 AArch64。不過,有兩個與本設計的其餘部分相關:刻度計數器和執行緒儲存空間。
在現代作業系統中,所有程序都會將虛擬動態共用物件對應至位址空間,嘗試提供時間檢查功能存取權,而不需進入核心。這通常是透過刻度或週期計數完成。用於此作業的 AArch64 暫存器會一對一對應至 AArch32。
另一個長期使用的作業系統功能是 ELF 執行緒本機儲存空間 ABI。這是變數的儲存空間模型,可讓每個執行緒擁有全域變數的專屬副本。如要進一步瞭解 Fuchsia 的用途,請參閱這篇文章。如要進一步瞭解 Android 的使用方式,請參閱這篇文章。 這些介面在 AArch64 上使用的暫存器為 TPIDR_EL0。雖然這會對應至 AArch32 做為 TPIDRURW,但 ARMv7 軟體不應具有 TLS 暫存器的讀取/寫入存取權。並預期核心會代表其更新 TPIDRURO。這會對應至 AArch64 上的 TPIDRRO_EL0。
例外狀況和呼叫慣例
如先前所述,例外狀況層級變更提供變更執行狀態的方法。AArch64 核心在 EL1 執行時,一旦設定 M[4] 位元並返回使用者空間 (EL0),處理器就會在 AArch32 中執行。也就是說,觸發的任何例外狀況都會是 AArch32 例外狀況。
觸發 AArch32 例外狀況時,處理器會切換回 AArch64 狀態,執行例外狀況處理程式碼。由於暫存器是直接對應,因此處理 AArch32 例外狀況時,只需要進行最少的變更。從例外狀況傳回同樣簡單。不過,您必須保留 APSR/CPSR 位元,確保執行模式和處理器模式旗標相同。請注意,程式計數器可能無法直接在 AArch64 上設定,而是會將例外狀況連結暫存器 (ELR) 設為 PC 需要在從例外狀況返回 (ERET) 時設定的值。從 AArch64 透過 ERET 返回 AArch32 時,R15 會從例外狀況連結暫存器 (ELR_EL0) 還原。
此外,請注意 AArch64 和 AArch32 的系統呼叫 Linux 呼叫慣例有所不同。在 AArch64 中,x8 必須包含系統呼叫號碼,但如果是 AArch32,則為 r7。為確保運作穩定,還需要涵蓋其他細微差異。舉例來說,交叉檢查還原的暫存器和 APSR/CPSR 條件位元是否以類似於 Linux 的方式還原,可避免難以診斷的故障。在初步審查中,還原的狀態在功能上與 starnix 的 AArch64 行為相符,但由於進入和從例外狀況狀態傳回的潛在路徑數量眾多,因此在實作期間應仔細審查這個區域。
啟用嚴格篩選模式
嚴格篩選模式可為 Zircon 執行緒和程序提供核心輔助的沙箱。具體來說,當程序或執行緒啟用受限模式時,會將所需的「受限」暫存器狀態提供給 Zircon、要返回的位置 (向量),然後發出 zx_restricted_enter() 系統呼叫。Zircon 進入受限模式後,會按照指示設定處理器暫存器,然後在受限位址空間中執行沙箱化執行緒。如果受限模式的執行緒發生任何例外狀況,Zircon 會儲存受限模式的處理器狀態,從進入受限模式時還原呼叫執行緒的狀態,然後在呼叫執行緒中以指向狀態和例外狀況資訊的指標,在提供的向量中繼續執行。除了篩選及傳遞例外狀況,Zircon 本身不會處理受限執行緒引發的大部分例外狀況。(值得注意的是,Zircon 會以透明方式處理頁面錯誤)。因此,受限執行緒的監管部分必須處理進入受限模式的設定,以及處理脫離受限模式的各種狀態。
我們會先探討 Zircon 中需要哪些項目,才能支援進入 AArch32 受限模式,然後再說明支援退出該模式需要哪些項目。
入門
呼叫 zx_restricted_enter() 時,可能會進入受限模式。如果提供的參數和繫結狀態已針對主機架構適當設定,Zircon 會在受限執行緒的環境中重新進入使用者空間。請注意,進入限制模式並非一次性作業。雖然任何指定執行緒都有初始項目,但任何受限例外狀況都會由執行緒的監管部分處理,該部分必須呼叫 zx_restricted_enter() 才能繼續執行緒。
將處理器轉換為 AArch32 執行狀態
考量到這一點,以及上述有關 ARM 架構的詳細資料,您只需要進行少量變更,即可在 AArch32 中使用受限模式。也就是說,我們知道必須允許一般模式、受限模式管理員設定 M[4]、T、IT、GE 和 Q 位元。首次進入時,CPSR 必須能夠設定 M[4] 和 T 位元,讓執行緒解譯 A32 或 T32 指令集。IT、GE 和 Q 必須在退出和重新進入時保留。
在目前的 AArch64 限制模式進入程式碼路徑 (RestrictedState::ArchValidateStatePreRestrictedEntry) 中,CPSR 會經過篩選,只允許條件旗標。您必須變更遮罩,允許上述任一項目,並確保遮罩只會使用有效設定執行這項操作。這種做法很適合 AArch32,因為這是 AArch64 的自然延伸,但如果日後支援不同的 ISA,可能就不適合。從長遠來看,或許會將轉換決策移至受限的進入系統呼叫,但目前不需要進行這項變更。
由於 AArch32 模式不需要暫存器的前 32 位元,且可能會意外解讀過時的值,因此在進入 AArch32 或返回時,主動清除這些位元是合理的做法。
有許多方法可以解決這個問題,例如透過組語例外狀況路徑中的「w」暫存器存取子 (32 位元) 儲存/還原暫存器,或在 C++ 中輸入和/或傳回時歸零。由於控制流程路徑單一,因此在輸入時歸零最簡單,但如果偵錯工具可存取暫存器,就無法保證高位元會保持清除狀態。
為限制 Zircon 的變更範圍,請先將目標鎖定在 ArchSave* 受限模式函式。這可確保暫存器高位 32 位元中的任何過時或非預期值,都不會由受限模式管理員 (例如 starnix) 使用,同時視需要保留日後的最佳化作業。此外,除了任何預設的 AArch64 更新之外,未使用的暫存器也不需要儲存狀態。
啟用 ELF 執行緒本機儲存空間 ABI
部分執行緒本機儲存空間 ABI 實作項目會依賴 set_tls() 系統呼叫,並讀取 TPIDRURO 暫存器。不過,現代 armv7-linux 二進位檔傾向使用 TPIDRURW 暫存器,不需要系統呼叫即可修改。(如果是 AArch32,TPIDRURW 會對應至 TPIDR_EL0,TPIDRURO 則會對應至 TPIDRRO_EL0)。很遺憾,我們可能會遇到想要執行的二進位檔,但這些二進位檔也「硬式編碼」使用 TPIDRURO,或者在 glibc 的情況下,如果沒有 Linux 核心使用者輔助程式,就會預設使用 TPIDRURO。
目前無法透過受限模式主管設定 TPIDRURO。 AArch64 的 zx_restricted_state_t 結構體會公開 TPIDRURW (TPIDR_EL0) 的存取權,但不會公開 TPIDRRO_EL0。然後,您可以明確新增介面來設定暫存器,例如在 TPIDRURO (TPIDRRO_EL0) 的結構中新增項目,或是將為 TPIDRURW 設定的值隱含鏡像至 TPIDRURO。目前,由於沒有已知二進位檔同時使用 TPIDRURW 和 TPIDRURO,且沒有必要支援 AArch64 二進位檔的該暫存器,因此為此目的變更 zx_restricted_state_t 介面似乎不值得。
如果實作鏡像功能,應只在進入限制模式時進行。如果執行緒變更 TPIDRURW,TPIDRURO 不會更新,直到下次重新輸入為止。這應該可以接受,因為 ARMv7 使用者空間不會預期能夠變更 TPIDRURO,除非透過系統呼叫。如果我們發現 Linux 二進位檔透過系統呼叫和 TPIDRURW 直接存取 TPIDRURO,可能需要新增設定 TPIDRURO (TPIDRRO_EL0) 的明確支援。
結束
從受限模式返回時,會發生系統呼叫或其他例外狀況,並分兩個階段處理。在第一階段,例外狀況會在 AArch32 EL0 中引發,然後由 Zircon 在 AArch64 EL1 中處理,Zircon 會將例外狀況狀態綁定,然後傳回 AArch64 EL0 受限模式的監管執行緒。
如先前所述,AArch32 例外狀況會由 AArch64 例外狀況向量處理。目前 Zircon 刻意不處理 AArch32 例外狀況。同步例外狀況、非同步例外狀況和 (非同步) 系統錯誤例外狀況,都需要三個額外的處理常式。Zircon 的例外狀況處理常式大多是使用組語巨集實作。因此新增處理常式相當簡單。另一項簡化作業是,傳入例外狀況應由受限模式管理員處理。這樣一來,非同步例外狀況和系統錯誤 (serror) 就能由與 AArch64 對應項目相同的處理常式處理。只有同步例外狀況需要特殊處理,而這些例外狀況中只有系統呼叫。對於傳入的系統呼叫,我們可以將任何呼叫 (使用 r7 做為號碼) 傳遞至受限模式結束處理程式碼。如果系統呼叫抵達,但執行緒未標示為受限,我們會恐慌 Zircon,因為不應可到達該程式碼路徑。這項行為也會套用至其他 32 位元處理常式周圍的包裝函式。
這種做法應可盡量減少 Zircon 需要的變更,同時確保 AArch32 例外狀況獲得妥善保留並傳遞至受限的監管程式執行緒,不會在受限模式外意外啟用 AArch32 例外狀況處理程序。
偵錯支援
一開始,偵錯支援功能會分成兩類:使用受限模式軟體 (例如透過 ptrace 的 gdb),或使用 zxdb 但不支援 AArch32 ISA Linux ABI。由於暫存器狀態會從 AArch32 擴充至 AArch64,反之亦然,因此暫存器資訊會正確無誤,但處理堆疊追蹤和 A32 與 T32 指令時將無法運作。讀取和寫入暫存器狀態和旗標的偵錯存取權,會與授予受限模式監管員的存取權相符。偵錯工具可以檢查 CPSR 位元,判斷執行緒處於哪種執行模式,且應適用於第三方偵錯工具。zxdb 必須擴充才能支援 AArch32。本文不會介紹這項工作的詳細資料,您需要額外研究和實驗。
工具鏈
完成上述變更後,Zircon 就能支援執行 32 位元 ARM 指令,但如果沒有工具鍊支援,就無法傳送要執行的指令。如要編譯任何 AArch32 限制模式的測試,我們需要可指定 ARMv8 AArch32 或 ARMv7 的工具鍊,因為 Arm AArch32 的目標是與這些架構相容。此外,為了支援 Starnix,我們需要可處理 Linux 和 Bionic 目標的工具鍊。
啟用 linux_arm
我們的工具鍊目前包含 Linux ARM Clang 目標,以及編譯靜態和動態目標所需的系統根目錄 (sysroot)。但我們的建構系統並未設定這項功能。為此,我們必須為目標架構元組新增設定,並新增必要的建構系統目標。
Linux ARM 工具鍊和目標
在 build/config/BUILDCONFIG.gn 中,我們必須定義新的工具鍊。例如:
linux_arm_toolchain = "//build/toolchain:linux_arm"
並將目標元組新增至 build/config/current_target_tuple.gni。例如
. . .
} else if (current_cpu == "arm" && is_linux) {
current_target_tuple = "armv7-unknown-linux-gnueabihf"
} . . .
方便的是,Fuchsia 中已有的工具鍊是 gnueabihf。所有支援 AArch32 的 AArch64 處理器都會支援硬體浮點 (根據初步討論,為 VFPv4)。
我們也需要在 build/toolchain/BUILD.gn 中新增先前定義的工具鍊目標:
clang_toolchain_suite("linux_arm") {
toolchain_cpu = "arm"
toolchain_os = "linux"
use_strip = true
}
由於 Fuchsia 並非要建構或執行於舊版 ARM 核心或 AArch32 狀態,因此我們不需要為 Fuchsia 專屬的建構目標巨集啟用這項支援。
可能還有一些其他設定 (例如 Rust) 需要更新,建構作業才能繼續進行。
荒漠油廠
除了 vDSO 之外,Fuchsia 樹狀結構中幾乎沒有任何建構項目可在 Starnix 中執行。大部分都是透過 Android 建構系統提供。不過,有一些測試是以 Starnix 為目標,並使用 Rust 建構。如要支援這些目標,工具鍊中必須有 armv7-linux-gnueabihf 的執行階段程式庫支援。同時支援上述兩者和 LLVM,會增加工具鍊團隊的支援負擔,無論是前期還是我們打算維護 AArch32 建構環境的期間,都是如此。
如果成本過高,我們應考慮採用替代策略,透過一般工具鍊和執行階段測試相同程式碼路徑 (例如在 AArch64 starnix 中新增自訂個性,以便存取相容性系統呼叫表,或類似做法)。
Starnix 目標
Starnix 包含 Linux 和 Linux/Bionic 目標的專屬建構目標。 兩者皆可根據工具鍊團隊的現有工作建構。在 src/starnix/toolchain/config/BUILD.gn 中,必須為 bionic 目標設定正確的元組和標頭路徑,並在 src/starnix/toolchain/BUILD.gn 中宣告相同的工具鍊。
Starnix
Zircon 變更之所以簡單,是因為複雜度會傳遞至 Starnix。Starnix 服務會模擬 Linux 使用者空間軟體所依賴的所有核心介面。必須能夠執行 ELF 二進位檔,並處理該程式碼發出的系統呼叫,以及可能發生的其他例外狀況 (32 位元或 64 位元)。雖然有些差異無法避免,例如指標大小或系統呼叫號碼,但大部分的 AArch32 功能都可以從現有程式碼無縫處理。
這項設計採用的方法是盡可能在介面點附近處理 AArch32 功能,這樣一來,大部分的 Starnix 程式碼都不需要考慮 32 位元架構的影響。這些互動點位於處理器層級,然後是系統呼叫項目、使用者空間複製 (來回) 和可執行檔載入。只有在暫存器、指令長度或指標大小很重要時,才需要使用其他區域,例如訊號處理、ptrace 和核心傾印。
以下各節將以建構式的方式逐步說明 Starnix。
工作支援
雖然從「工作」支援開始,而非從介面點開始,可能看起來很奇怪,但如前所述,這是有充分理由的。AArch32 的 Starnix 支援不會是二進位狀態,而是會同時支援 AArch32 和 AArch64 工作。因此,工作是可註解執行模式的邏輯單元。此外,執行模式是執行緒專屬模式,因此這項設計會假設目前工作上的 ThreadState 結構體會指出執行緒是否為「arch32」執行緒。
請注意,我們使用「arch32」而非「AArch32」,是為了避免將命名繫結至特定處理器實作。
雖然可能可以完全避免註解,例如一律檢查 CPSR 中的暫存器值,但專用註解提供兩項實用屬性。首先,我們將確保 Starlark 程式碼能因應未來變化。舉例來說,如果新增 riscv32,所有使用「arch32」檢查的程式碼都會正常運作,無須進行架構專屬變更。
第二個屬性是在初始化時宣告工作狀態。這點非常重要,因為工作記憶體管理員至少會設定為 32 位元版面配置和強制執行。如果工作變更 CPSR 執行模式位元,則可執行的指令和引發的例外狀況類型都會變更,但預先設定的工作狀態不會變更。
雖然混淆 Starnix 的執行模式不應會立即造成安全性影響,但任意切換模式會違反 Starnix 開發人員在處理任何 64 位元或 32 位元特定程式碼路徑時,可能做出的正常假設。Linux 之前也發生過這類混淆情形。在「arch32」存取子或例外狀況處理層面強化這個區域,確保工作「arch32()」值與例外狀況模式的系統呼叫或例外狀況處理常式相符,是合理的做法。
處理器支援
在處理器層級,暫存器處理是主要介面點。ARM64 RegisterState 結構體必須更新,確保 PC、SP、LR、CPSR 和 TPIDR 的更新作業能準確更新 AArch32 暫存器,且不會清除必須維護的狀態 (例如 CPSR)。除了 starnix/kernel/arch/arm64 程式碼外,這也需要在 Zircon Rust 程式碼中解決 (例如 src/lib/zircon/rust/fuchsia-zircon-types/src/lib.rs)。對於處理結構體的暫存器,必須確保更新套用至正確的暫存器,且命名與基礎暫存器相符,例如 syscall_register() 使用 r7。在 Starnix arm64 程式碼和 Zircon Rust 程式庫中,必須確保所有 From/Into 實作項目都已更新,才能在受限狀態結構體和暫存器結構體之間正確對應。
在某些位置,程式碼只能依據 CPSR 值判斷是否要執行暫存器變更。如果可以存取目前的工作,系統就會使用 arch32 布林值。
UAPI 支援
每個架構的 Linux 核心介面都稱為 Linux UAPI 或使用者空間 API。Starnix 會使用 bindgen 和自己的 Python 指令碼 (用於驅動 bindgen) 產生 Rust 繫結。由於 UAPI 包含傳入和傳出核心介面的結構體,因此 Starnix 必須使用 AArch32 UAPI,才能正確處理這些結構體。
需要解決的三個主要領域如下:
- 32 位元指標和型別
- 同時存取兩個 UAPI
- 系統呼叫命名
指標和型別大小
就前者而言,有兩個問題。首先,我們需要將所有指標替換為 AArch32 大小的指標型別。Starnix 通常會使用 uaddr 和 uref。如果是 32 位元大小的指標,只要加入 uaddr32 和 uref32,並確保這些指標和參照會取代指標和參照,就應該足夠。此外,如果可以透過 From/Into 輕鬆轉換為 uaddr 和 uref,則 uaddr32 大致上應會隱藏,除非明確需要。
第二個問題是,相同型別名稱的大小不同。舉例來說,long 在 AArch64 上寬度為 64 位元,在 AArch32 上則為 32 位元。這表示預設類型對應無法在兩個 UAPI 之間共用,且必須為每個 UAPI 定義及插入一組特定的核心 C 類型。
平行 UAPI 存取權
為了清楚產生 ARM UAPI 繫結,Starnix 必須能夠同時存取 AArch64 和 AArch32。幸好,只要在 linux_uapi 的 lib.rs 中,將產生的「arch32」UAPI 對應至巢狀命名空間,即可解決這個問題。例如:「pub use arm as arch32」,以及正確的 cfg 修飾符。(請注意,在 Linux 中,「arm」代表舊版 ARM 32 位元 ISA 和 AArch32)。
這樣一來,starnix 中的任何程式碼都能使用 arch32 命名空間,存取適當大小的型別和產生的繫結。
系統呼叫號碼
有了專屬命名空間,就能使用 arch32 前置字串,將 AArch64 mmap 系統呼叫與 AArch32 mmap 分開。不過,我們會盡可能分享系統呼叫實作方式。部分系統呼叫的參數和行為是 arch32 專屬。在這種情況下,使用特定命名 (例如 __NR_arch32_open) 遮蔽定義可能較為合理,這樣稍後建構系統呼叫表格時,就能提供更易於解讀的名稱。
由於 Starnix 會在 UAPI 生成步驟中驅動所有變更,因此這裡也可以這麼做。新增用於存根和重新定義的 arch32 標頭後,任何自訂定義都能拉入 arch32 命名空間,不必持續維護獨立的 Rust 型對應。
支援載入器
即使有上述三項元素,目前仍無法有效啟動 AArch32 工作或執行緒。由於 CPSR 中的 M[4] 位元只能在例外狀況返回時變更 (EL1->0),因此使用者空間必須要求 Starnix 在返回 EL0 前設定位元。不需要特定支援,因為這項功能只會在呼叫 execve 系統呼叫時,或在第一個程序 (init) 啟動時發生。無論是哪種情況,都必須載入檔案,且檔案格式須為 Starnix 支援的格式。目前 Starnix 支援 ELF64 格式的檔案,可剖析、載入至記憶體,並最終執行。ELF32 格式的檔案也需要相同的功能,最好是盡量減少 Starnix 核心的變更。
ELF 剖析
ELF 剖析是在 src/lib/process_builder/elf_parse.rs 中實作。所有必要的 ELF64 定義都會顯示,且可從 VMO 剖析 ELF64 檔案標頭。您需要為 ELF32 重複相同的定義。不過,Starnix 預期能夠傳遞 Elf64* 結構體 (而非 Elf64* 特徵等)。因此,新增 ELF32 支援的方式與 uaddr32/uref32 類似:盡可能定義直接與 32 位元型別和資料互動所需的項目,然後提供 From<> 實作項目,轉換為 64 位元表示法。雖然這個方法會導致產生的 Elf32->Elf64 例項中出現 e_ident 類別不符的情況,但這個結構體會與所有現有介面和呼叫相容。
此外,透過次要進入點 (例如 from_vmo_with_arch32() 與 from_vmo()),即可支援載入與 arch32 相容的 ELF 檔案,確保呼叫端瞭解可能的輸出內容。同樣地,如果使用 arch32()->bool 這類輔助函式,載入器中的任何後續呼叫端都能輕鬆檢查 ElfIdent 類別,判斷是否需要 arch32 行為。
除了決定是否要轉換為與 Elf64 相容的執行個體外,這項工作基本上是機械式作業 (與 UAPI 和其他已討論的主題非常相似)。建議您考慮提升 Elf64 介面層級,以便處理特定格式,而不需符合檔案格式的特定期望,就像 elfldltl 一樣。
ELF 解析度和 MemoryManager
Starnix 會在成功剖析 ELF 及其解譯器後,將 ELF 視為已解決。這會在提交以完成 exec 呼叫之前發生。雖然 AArch32 支援可在此處繼續進行,但這是工作設定的關鍵時刻。此時會為使用者程序建立虛擬記憶體區域 (VMAR)。通常,這個 VMAR 是完整的可用受限位址空間。不過,透過將呼叫擴充至 MemoryManager (mm.exec()),Starnix 可以建立適合程序的 user_vmar,最多具有 32 位元可定址記憶體空間 (例如 4Gb)。
由於所有記憶體導向的作業都會使用工作的「mm」,因此適當大小的 VMAR 可確保對應只會發生在 arch32 工作可定址的位址空間中。此外,系統會允許從可用範圍中隨機或自動選取記憶體位址,且不需要特殊標記來限制對應偏移。使用 user_vmar 是 Starnix 的新功能,但可大幅簡化程序,強制限制受限執行緒與記憶體互動的位置。下一節會使用這項簡化作業。
除了記憶體設定外,解析度也允許在完整載入可執行檔之前,追蹤要執行的模式 (arch32),確保後續步驟不會發生記憶體設定與載入的 ELF 檔案不符的情況。
載入 ELF
檔案解析完成後,核心就會知道應將哪些區段對應至記憶體,以及應具備哪些權限。一般來說,Starnix 會嘗試從 mm 的基底指標所在位置,以及區域中列出的最低位址,判斷傾斜度。這項作業會透過 wrapping_sub 和 wrapping_add 作業完成。
一般來說,當計算虛擬位址時,包裝值會包裝回去 (或包裝嚴重到區域可對應至 64 位元位址範圍的高端)。大多數 ELF64 可執行檔的位址似乎都低於 mm 基礎 0x20000,因此這項功能可能不會廣泛使用。ELF32 可執行檔的檔案基底通常較小 (0x10000),因此會產生 64 位元包裝,但通常不會包裝回去。
解決這個問題的方法之一是使用 32 位元包裝作業。不過,任何 0x0 位移區段的結果位置仍可能失敗。因此,確保 ELF32 的檔案基底不會發生下溢情形,會更有意義。如要這麼做,請將可執行檔的檔案基底設為 mm 基底指標或最低 ELF 資訊值 (以較高者為準)。完成後,即可完全忽略 wrapping_add/sub,讓其保持不變,以供 64 位元使用 (如果使用)。對大多數二進位檔而言,這並不重要,因為許多 Linux 和 Android 二進位檔都是地址無關執行檔 (PIE),也就是說,這些二進位檔會取得隨機基底,且一律會位於 mm 的基底指標或更高位置。設定相對基底後,其餘 ELF 區段載入作業會照常進行,也就是將 mm/檔案偏移轉換為虛擬位址,然後從該處開始對應,就像處理 ELF64 檔案一樣。
請注意,如果未按照檔案中的指定方式對低位移的任何不可重新定位 ELF 二進位檔進行對應,這些檔案將無法正常運作。雖然這可能是需要解決的問題,但由於大多數現代 armv7-linux 二進位檔都是 PIC 或 PIE,因此這並非優先事項。
對應和 ASLR
雖然 ASLR 同時適用於 PIE 基本位址上方和下方,也就是堆疊和堆積的起點,但仍建議您瞭解使用者 VMAR 的變更如何影響後續對應。在目前剛起步的 ASLR 實作項目中,系統會擷取特定位元的熵,並套用至所需目標位址,使其在記憶體中的位置高於或低於起點。根據預設,由於可定址記憶體差異過大,這項作業會在 32 位元執行緒上失敗。AArch64 預期可新增 28 位元的熵。在 Linux 上,AArch32 幸運的話可達到 18 位元的熵。
雖然目前的做法在運算方面成本最低,但由於使用者 VMAR 的大小可彈性調整,因此隨機化策略也能同樣調整。對於每個需要隨機化的位置,地址隨機化及其隨機化方向的程序,都會集中到 MemoryManager 函式中。該處會根據提供的最大大小或範圍,計算 ASLR 熵遮罩。遮罩會比左移的位元數少一,位元數為範圍大小減去頁面大小的位元數。也就是說,對於最多 4 GB 的可定址區域,最多可套用 19 位元的熵。如果是 64 位元可定址區域,這個值會高出許多,但原始的 AArch64 特定熵位元會做為上限。
實際上,任何嘗試的隨機範圍都不是完整的位址空間,而是其他對應物件之間的區域,例如堆積頂端與堆疊。因此,隨機熵在實務上遠低於理論值。 這項設計的重點是啟用 AArch32 支援,因此應另外評估 ASLR 實作中的熵,並判斷是否應修改,而不必考量效能問題。
如果效能成為問題,我們可以快取熵遮罩,並改用硬式編碼遮罩,一個用於 arch32 工作,另一個用於一般工作。一開始的目標是簡化來電者的體驗,但如有需要,可以導入最佳化措施,例如如果對節目開始時間造成負面影響。
vDSO
虛擬動態共用物件是合成共用程式庫,核心會在執行程序時將其對應至程序。其目的是協助簡化使用者空間和核心之間的介面。vDSO 是由核心提供的程式碼,可供程序在使用者空間中執行。舉例來說,通常會有系統呼叫輔助程式,確保使用核心呼叫慣例。同樣地,當使用者空間程序嘗試檢查系統時間時,通常會有與系統時間相關的呼叫,可使用任何架構專屬最佳化。
如要瞭解 Starnix 如何管理 vDSO 時間,請參閱這篇文章。目前 vDSO 依賴 Zircon (即 fasttime) 以程式庫形式提供的功能。不過,vDSO 必須以 AArch32 為目標建構,而 Zircon 則不打算支援為 AArch32 架構建構。因此,可以擷取 fasttime 邏輯,納入 AArch32 專屬的 vDSO 輔助程式。請務必瞭解,在長期 Zircon vDSO 實現之前,這個方法只是權宜之計。這個模型與 AArch64 Starnix 採用相同的介面,不會為 Zircon 增加額外負擔,因此應能為 AArch32 減少類似的內容切換次數,且不會為暫時解決方案帶來大量技術工作。
除了快速時間之外,今天的 Starnix vDSO 無法依附其他共用物件。時間計算通常至少會部分使用 64 位元整數。 Clang 和其他編譯器會為 ARM 提供輔助程式 (例如 compiler-rt),以執行 64 位元模數和除法運算。一開始,這項工作只會使用 C++ 中的二進位除法,並匯出預期符號。很可能需要以最佳化的組語程式碼取代這些程式碼。此外,可能還有其他符號和函式需要新增至 AArch32 vDSO,這些符號和函式可能與 AArch64 vDSO 不同。您可以視需要新增這些項目。同樣地,如果沒有正當理由,也可以從 AArch32 vDSO 移除時間相關功能。
AArch32 vDSO 建構作業本身也必須與 AArch64 vDSO 同步進行。因此,您必須新增可使用 arch32 工具鍊強制建構,且不依附於 Zircon 標頭的目標。系統會在 Kernel 結構中新增一個與「vdso」欄位對應的欄位「arch32_vdso」。這會是<>選項,可讓 arch32 可執行檔在 arch32 vDSO 缺少時正常失敗。
準備堆疊
啟動可執行檔前的最後一個步驟,是以 ELF 進入點的程式碼知道如何處理的方式,設定可執行檔的堆疊。Starnix 已處理這些步驟,包括設定輔助向量 (AUXV),以及放置引數和環境變數。在 64 位元平台上,兩個引數的指標、環境變數和 AUXV 的整數都是 64 位元寬。此外,堆疊必須對齊 16 位元組。
也就是說,指標和值的大小至少必須針對 ELF32 二進位檔調整。在目前的 Starnix 實作中,系統會建構堆疊向量,然後複製到記憶體中的位置。如要支援 AArch32,最簡單的方法是複製程式碼路徑並分別處理。不過,堆疊向量是無符號 8 位元位元組的向量。也就是說,您可以將寫入堆疊的作業封裝起來,選取正確的寬度 (8 位元組或 4 位元組)。完成這類變更後,最終堆疊應會自動成為正確大小,因為要寫入的值必須已正確調整大小。此外,由於 32 位元堆疊必須為 8 位元組對齊,因此 16 位元組對齊已符合這項需求。
目前 Starnix 堆疊準備作業缺少一項功能,可透過探查發現,但可能會在程序執行時透過 HWCAP 項目揭露,例如是否支援 Thumb 模式,或允許哪種浮點運算。這些項目會盡早用於 AArch32 glibc 和 bionic 載入器。您需要在每個 kernel/arch/processor 中新增函式,傳回處理器支援的硬體功能清單。每項實作項目都可以使用「zx_cpu_get_features」呼叫,判斷如何填入「AT_HWCAP」欄位。一開始,這項功能只能為 arch32 支援新增,因為目前支援的架構不需要任何支援即可運作。
進入點
最後,可執行檔或其解譯器會提供 Starnix 或 Linux 核心的進入點,以啟動執行緒。通常,這個進入點必須與指令對齊。不過,ARM 上的 ELF32 會使用未對齊的進入點,表示處理器應以 Thumb 指令集執行模式啟動。如果與 0x1 進行位元 AND 運算的進入點為 0x1,則必須在執行程式碼前,於 CPSR 上設定 T 位元,否則處理器會嘗試將 T32 指令解譯為 A32。(請注意,這項對齊方式也適用於處理器層級的例外退貨)。
由於新執行緒的進入點必須是程式計數器 (和例外狀況連結暫存器值),因此會透過 ThreadStartInfo 轉換為 zx_thread_state_general_regs_t 填入。kernel/arch/arm64/loader.rs 中的 From<> 實作項目可以檢查 PC 的對齊方式,然後適當設定 Thumb 模式。在其他情況下,Thumb 值會透過使用者空間中的分支指令啟用或停用,且其狀態必須在呼叫 Starnix 時保持不變。
完成這項設定後,即可啟動 AArch32 可執行檔,但由於會使用 AArch64 系統呼叫編號叫用 AArch32 系統呼叫,因此會立即失敗。
系統呼叫
除了錯誤情況外,系統呼叫是使用者空間軟體和 (Starnix) 核心之間的主要介面。這表示您需要解決兩大挑戰:
- 通話慣例和轉接
- 讀取及寫入使用者空間記憶體
AArch32 系統呼叫
如先前所述,Linux AArch32 系統呼叫使用的號碼與 AArch64 系統呼叫不同。此外,AArch32 使用的系統呼叫號碼暫存器與 AArch64 不同,且兩者系統呼叫的參數也可能不同。
從參數的角度來看,由於登錄檔會在進入 Starnix 時擴充,因此不需要處理指標大小差異。數字和參數的差異帶來更有趣的挑戰。Starnix 必須能夠依編號查閱系統呼叫,並將該編號對應至實作項目。雖然可以強制每個系統呼叫實作項目執行某些 arch32 檢查,但這項工作可以在系統呼叫處理時間完成。也就是說,Starnix 可以設定次要系統呼叫表,該表只用於 arch32 工作。這會導致額外的記憶體用量略為增加,但可提供強制執行 AArch32 與 AArch64 使用者空間 ABI 的方法,並透過可辨識 AArch32 的包裝函式,直接或間接分享系統呼叫實作項目。
這種做法至少在初期需要複製一些巨集,但或許也能盡量減少複製作業。
AArch32 資料結構
雖然系統呼叫參數中的指標不會造成問題,但從 AArch32 使用者空間複製結構時,情況並非如此。AArch32 軟體會使用 AArch32 Linux UAPI 中定義的結構,而非 AArch64 UAPI。舉例來說,IO 向量 (iovec) 只是指標和大小配對的陣列結構。在 AArch32 上,iovecs 是兩個 32 位元欄位,而在 AArch64 上,則是兩個 64 位元欄位。也就是說,使用 iovecs 的任何系統呼叫都必須適當調整。
不過,並非所有系統呼叫都會受到這個問題影響。與 AArch64 對應項目相比,大約有三分之一的 AArch32 系統呼叫具有不同的結構。對於特定呼叫,可以在架構專屬的系統呼叫實作中處理特定結構體。舉例來說,stat 系統呼叫預期會傳回 stat64 結構體,但這與 AArch64 stat 呼叫傳回的結構體不符。這項問題可以透過 kernel/arch/arm64/syscalls.rs 下的新 stat 進入點解決,並在類型之間進行轉換。
常見的結構體 (例如 iovec) 可能會以更通用的方式處理。Starnix 的 MemoryManager 提供讀取 iovec 的輔助程式 read_iovec()。這取決於 read_objects_to_smallvec(),其中型別多型會將 UserBuffer 別名的 SmallVec 做為要讀取的型別。對於所有現有架構,UserBuffer 都是 iovec 的 Rust 版本。將 UserBuffer32 新增至 Starnix 的 UAPI 後,即可建立與 AArch32 相容的平行 iovec SmallVec 型別。然後,這可以透過 read_iovec32() 函式公開,該函式本身可以傳回常見的 UserBuffer (再次感謝 into()/collect()!)。雖然這不會隱藏 AArch32 iovec 讀取作業,但會限制 AArch32 特定型別對其餘呼叫程式碼的公開程度。目前有一個開放式設計問題,就是是否要將工作的 arch32 欄位傳遞至 read_iovec() 呼叫,還是呼叫個別函式。這樣標記可能可以簡化在常見系統呼叫實作 (例如 sys_writev()) 中新增對 arch32 的支援。
訊號
訊號處理和 ptrace() 一般來說是與基礎架構密切互動的領域。在大多數情況下,您需要新增訊號處理和 ptrace 的機械更新,才能正確連結正確的訊號結構內容和架構資訊。
由於重設程式計數器/指令指標取決於模式 (A32 或 T32),因此系統呼叫重新啟動會稍微複雜一些,因為 T32 指令為 2 個位元組,A32 指令則為 4 個位元組。
這個區域需要多加留意,但一旦其餘 arch32 支援功能正常運作,探索就會變得更加容易。
測試
與大多數軟體一樣,測試中的程式碼行數應超過實作 AArch32 支援所需的行數,Zircon 尤其如此。本節涵蓋計畫的測試程式碼。
工具鏈
Fuchsia Clang 工具鍊和工具鍊的其他部分,在 Fuchsia 建構作業以外,都有足夠的支援和測試。有兩個方面需要考慮:建構設定測試和內建工具鍊測試。這兩者通常會相互牽連,目標是確保 armv7-linux-gnueabihf 的建構作業持續正常運作。目前的一個例子是使用「--allow-experimental-crel」旗標。為 Linux/ARM 啟用此標記會導致二進位檔損毀。失敗原因可能是工具鍊中的程式碼產生錯誤,但建構設定會決定是否會中斷任何進行中的建構作業。
因此及早發現這些變化非常重要。如果我們在未經測試的情況下支援這項功能,就只會在 Zircon 測試 (稍後會討論) 發生當機時,或是在存取 AArch32 Starnix VDSO 失敗時,才會發現這項失敗。我們需要為已啟用的任何工具鍊目標元組啟用核心/必要 LLVM 測試,確保運作正常並正確產生程式碼。
Zircon 測試支援
Zircon 測試相當困難,因為測試應明確執行受限模式,且不依賴功能齊全的監管程式/核心。此外,如果測試不會從替代位置 (例如完整容器) 載入二進位檔,程式碼就必須是獨立的。
對於 Zircon 系統單元測試,我們會利用建構系統的 loadable_module() 支援功能,建構架構專屬的共用程式庫,這些程式庫可使用 elfldltl 程式庫動態載入及對應。這樣一來,整合測試就能在 AArch64 的主機架構上執行,同時從以 AArch32 為目標 (armv7-linux) 的共用程式庫載入及解析符號。這些共用程式庫會載入,並對應為可執行檔,就像 BOOTFS 上的任何其他可執行程式碼一樣。
其餘需求則與主機支援的項目,以及受限的 AArch32 共用程式庫可存取的項目有關。共用程式庫必須對應至可定址的記憶體 (4 GB 以下),以及任何共用指標,例如 std::atomic 或 TLS 儲存位址。完成上述設定後,測試也必須判斷主機架構是否支援執行 AArch32,如果支援,則針對以主機為目標的受限模式和以 AArch32 為目標的受限模式,執行測試套件。
Starnix 測試支援
初步驗證會針對在 Starnix 下執行的 Linux 最簡系統進行。這樣可確保驗證 Starnix 的工具鍊和建構系統變更時,依附元件數量最少。如要啟用這項功能,ARM64 Debian 容器的映像檔會新增 AArch32 sysroot,且 example/hello_debian 範例會擴充為包含 AArch32 範例,包括動態連結的 C++ 和靜態連結的組語。目標支援和工具鍊 sysroot 必須大致同步。
初始測試環境包含 Starnix,可針對 AArch64 程式庫或 AArch32 執行一個二進位檔 (hello_debian)。不會有其他程序或工作正在進行。不過,您仍需定期測試,以涵蓋與 AArch64 的差異:
- 系統呼叫支援涵蓋範圍
- 信號和 ptrace 行為
- 混合執行模式環境 (共用 sysroot、容器等)
- 各支援 ELF 檔案類型的測試
- ASLR 熵測量
其中部分測試需要為 Starnix 編寫,但理想狀態是使用現有專案,例如 Linux 測試專案,因為這些專案已致力於確保 Linux 持續相容。
Zircon
應新增多項 AArch32 專屬測試:
- CPSR:M[4]:A32 執行
- CPSR:T|M[4]: T32 執行
- 暫存器設定和清除
- >32 位元暫存器設定,清除行為
- 系統呼叫處理
- 非系統呼叫同步例外狀況處理
- 非同步例外狀況處理
- 錯誤處理
- CPSR:Q: Saturation bit setting and persistence through a system call
- CPSR:GE:透過系統呼叫保留 GE 位元
- IT 位元用量
- 用於執行緒的 read_state 和 write_state API
雖然基本條件很容易驗證,例如 A32 和 T32 的執行模式位元以及暫存器操控,但較複雜的例外狀況處理可能比較困難。舉例來說,從 EL0 誘發 SError 可能需要提供測試存取未對應的記憶體,才能進行互動。IT 位元測試需要能夠執行中繼狀態,這應該可以使用執行緒內測試完成,但可能需要一些額外的偵錯工具驅動測試案例。
現有測試也應使用受限的 aarch32 目標執行:
- FloatingPointState
- 本奇文
- ExceptionChannel
- InThreadExecution
- EnterBadStateStruct
- KickBeforeEnter
- KickWhileStartingAndExiting
- KickJustBeforeSyscall
目前測試會選取要測試的編譯器目標架構。雖然可以硬式編碼 arch32 檢查和特定測試,但最好將 32 位元架構納入另一個測試目標。為此,我們需要能夠在共用的 AArch64 架構中重複使用部分功能,同時定義 AArch32 ABI 的測試固定裝置。將這些測試重構為使用具有架構專屬類別實作項目的通用基本類別,可簡化 arch32 子類別的衍生作業。有了可靠的每個架構類型,TEST_P 巨集可用於根據要使用的架構專屬類別例項,將測試參數化。相同功能的常見測試可避免因測試複雜度導致錯誤,進而錯失涵蓋範圍。不過,如果非一般測試案例需要,仍可使用 TEST_F()。
Starnix
如工具鍊一節所述,初始整合測試將使用 hello_debian 進行。這可隔離功能:載入和系統呼叫。就單元測試而言,現有的類別測試將擴充,以涵蓋新增功能。
此外,您還需要進行新測試,以練習 AArch32 的特定行為,例如記憶體限制和系統呼叫。Linux 測試專案和其他系統呼叫測試架構,將與一般單元和功能測試一併使用,確保涵蓋範圍和正確性。
基礎架構測試
基礎架構的自動化測試必須注意兩大問題:
- 缺少 AArch32 支援
- 硬體變化
在 ARM64 處理器上測試時使用的任何硬體加速虛擬化功能,都不太可能適用於 AArch32 測試,因為伺服器導向的處理器通常不支援這項功能。完全模擬的虛擬化功能可以運作,但速度較慢。
此外,您也應在預期會使用這項功能的任何硬體上進行測試,因為不同處理器實作方式可能會出現例外狀況處理的怪異行為。
實作
實作策略如下:
- 在 CIPD 中更新 Debian arm 映像檔
- 工具鍊變更和測試,以啟用 arm clang
- Zircon 變更,可啟用及測試 32 位元受限模式
- Starnix:
- 新工作旗標
- 剖析及載入 ELF
- AArch64 暫存器
- 可調整大小的使用者 vmar 變更 (含 ASLR)
- Linux UAPI 變更
- AArch32 vDSO
- 載入器變更
- arch32 系統呼叫表和支援
- hello_debian 範例
接著,我們需要判斷需要哪些系統呼叫,並繼續實作。此外,偵錯工具和額外工具鍊調查可以繼續進行。
效能
請務必確保變更作業對 Fuchsia 或 Starnix 中工作的一般 64 位元作業造成的負擔極小。這項評估可透過 Zircon 版本間的微基準進行,但我們也可以評估完整系統基準,判斷是否有重大損失。
如果是 AArch32 作業,受限模式基準將是第一個路徑,可比較與 AArch64 受限模式的效能。之後,與類似的 Linux 系統進行基準測試,並使用實際工作負載,將可獲得最大效益。
持續最佳化系統呼叫、記憶體使用率和電力監控,有助於確認這項功能是否可行。
人體工學
在 Zircon 中,目前的方法不會導致特定的一組 API 變更。反之,系統會透過在受限模式中支援現有架構 ABI,啟用 AArch32 支援。這個方法不會大幅增加複雜度,也不會降低開發人員的人體工學。使用這項功能的開發人員必須自行實作 ABI 的其餘部分。
在 Starnix 中,變更一開始會受到旗標保護。必須區分的程式碼路徑應盡量靠近區別發生的位置,並盡量減少整個 starnix 中發生的程式碼分叉。舉例來說,如果可以在 MemoryManager 中強制執行記憶體限制,從開發人員的角度來看,所有系統呼叫和依賴 MM 的其他功能都不會改變。
回溯相容性
AArch32 使用者空間支援功能基本上是回溯相容性功能。預計 AArch32 的實用性會隨著時間逐漸降低。
安全性考量
本提案會變更 Zircon 處理例外狀況和 ABI 的方式。因此必須仔細評估。此外,程式碼應採取防禦性寫法,主動禁止非預期的程式碼路徑。如果基準顯示有此需求,則可進行最佳化。
從 Starnix 的角度來看,系統會新增額外的系統呼叫和信號處理路徑,以及額外的檔案剖析作業。記憶體用量減少也會直接影響位址空間隨機化。這些都是與安全性相關的變更。
凡是合理的情況,我們都應重複使用現有的 Linux 安全性測試工具,例如 syzkaller。
隱私權注意事項
這項異動不應會造成任何隱私權異動。
說明文件
受限模式 API 說明文件將更新,指出允許的其他註冊標記。
請更新 Starnix 說明文件,加入如何使用及開發 AArch32 支援功能的資訊。其他文件應著重於對開發人員的影響,例如管理兩個系統呼叫表,或在 AArch64 和 AArch32 路徑上測試變更。
在 Starnix 中執行的 AArch32 二進位檔範例位於 src/starnix/examples/hello_debian。
缺點、替代方案和未知事項
這項提案會增加長期維護作業,以及系統複雜度。但好處是,在以消費者為主的 SoC 支援 AArch32 Linux 軟體的剩餘年限內,使用者都能存取這類軟體。
目前沒有效能良好的替代方案,可支援未經修改的 AArch32 程式,因此只能將所有程式碼重新編譯為 AArch64。這些效能較差的替代方案是轉譯或選擇性使用者模式模擬。
我們持續進行調查的主要領域,是分析同時具有 AArch64 和 AArch32 呼叫慣例及指令集的執行緒時,如何提供高品質的偵錯體驗。
具體來說,目前的偵錯工作流程會依賴 libunwind,在 AArch64 和 zxdb 上透過 Linux 呼叫堆疊進行解除包裝,以便分析特定呼叫架構中的狀態。您必須在 zxdb 和 libunwind 中新增類似行為的支援功能。libunwind 應已支援 AArch32 Linux ABI,但不太可能支援在同一執行緒中順暢地在 ABI 之間轉換。我們的目標是讓這兩項工具的功能相同,但這需要進一步調查和實驗。
除了偵錯之外,開發人員工作流程也可能因全面測試而受到影響。使用功能強大的 Intel 環境的開發人員,必須完整模擬 AArch64/AArch32 環境。不過,如果他們已指定 AArch64,目前確實是這樣。如前所述,這裡增加的複雜度是伺服器導向的 ARM SoC 缺少 AArch32 支援。過去 Intel 開發人員可能依賴加速的伺服器端 AArch64 測試,現在則需要使用支援兩種執行模式的桌上型或代管裝置。您或許可以使用 starnix 中的執行緒特性,啟用或停用 AArch32 專屬的呼叫路徑,進一步減少無法在沒有 AArch32 硬體支援的情況下大規模執行的程式碼量。
既有技術和參考資料
參考資料已內嵌為連結。Linux 是指 AArch64 上的 AArch32 支援 (相容性模式),並提供先前技術。