RFC-0264:在 Fuchsia 上執行未修改的 AArch32 Linux 程式

RFC-0264:在 Fuchsia 上執行未修改的 AArch32 Linux 程式
狀態已接受
區域
  • 外部 ABI 相容性
  • 核心
  • 安全性
  • 工具鏈
說明

透過受限制模式和 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 環境為目標,例如 DebianAndroid。提供 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) 確保所有例外狀況 (錯誤或其他) 會在盡量減少對 Zircon 的任何其他影響的情況下,傳送至限制模式監控器 (starnix)。

處理器支援

如前文所述,ARM 架構提供執行狀態,稱為 AArch32,可在採用較新架構版本 (ARMv8 以上) 的處理器上,與先前的 32 位元 ARM 指令集回溯相容。

ARM 架構提供相對明確的路徑,可實作 AArch32 支援功能,且不會影響現代核心,並啟用 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 的註冊對應

AArch32 AArch64
R0-R12 X0-X12
坡面 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 (saturation) 標記。
    • GE (大於或等於) 標記。
  • 目前的處理器模式。
  • 中斷停用旗標。
  • 目前的處理器狀態,也就是 ARM、Thumb、ThumbEE 或 Jazelle。
  • endianness。
  • IT 區塊的執行狀態位元。

在 AArch64 中,這項資訊會以不同的方式分割及存取 (雖然 PSTATE 基本上是 CPSR 重新命名)。不過,發生例外狀況時,系統會在已儲存的程式狀態暫存器 (SPSR) 中顯示資訊,其中儲存了以下資訊:

  • N、Z、C 和 V 標記。
  • D、A、I 和 F 中斷停用位元。
  • 註冊寬度。
  • 執行模式。
  • IL 和 SS 位元。

在架構上,AArch32 CPSR 會在 AArch64 例外狀況時對應至 SPSR。這樣一來,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 T M
[1:0] [3:0] [7:2] [4:0]

在 AArch64 狀態下,IT、Q 和 GE 標記無法讀取或寫入。在 AArch32 中,GE 標記會透過並行加減指令設定,而 Q 會設定為表示溢位或飽和IT 位元 可供 Thumb 指令用於條件執行,在 A32 模式下應為 0,並應在 T32 模式下在例外狀況中保留。

執行模式和處理器狀態

在 ARMv8 和 ARMv9 中,執行模式 (上圖 SPSR/CPSR 圖表中的 M[4]) 可以設為指出程式是否處於 AArch32 模式。執行模式只能在重設或例外狀況層級變更時變更

在 AArch64 執行模式下,只能使用 A64 指令集。AArch32 僅支援 A32 和 T32 指令。啟用的指令集稱為處理器狀態,可使用 T 位元 (Thumb 執行狀態位元) 切換。在使用者空間中,此變更會使用分支 (BX/BLX) 執行。任何核心軟體都應確保 PC 對齊方式與目前的指令集對齊方式相符。

浮點和進階 SIMD 暫存器

除了核心暫存器外,進階 SIMD 和浮點指令也共用相同的暫存器群組。它包含 32 個 64 位元暫存器,較小的暫存器會封裝至較大的暫存器,如 ARMv7 和舊版 (以 AArch32 的 VFPv4 為準)。

根據上述規定,目標是支援向 ARMv7 公開的 ASIMD 和 VFP 擴充功能,但在 AArch32 模式中支援。系統不一定會支援 AArch64 和日後 AArch32 ISA 擴充功能中的其他狀態。

其他暫存器對應:計數器和執行緒

除了上述 AArch32AArch64 的註冊對應項目外,還有許多其他註冊對應項目。不過,有兩個與此設計的其餘部分相關:刻度計數器和執行緒儲存空間。

在現代作業系統中,所有程序都會將虛擬動態共用物件對應至其位址空間,以便嘗試提供對時間檢查功能的存取權,而無須進入核心。這通常是透過使用計時器或週期計數來完成。為此目的而使用的 AArch64 暫存器會一對一對應至 AArch32。

另一個長期的作業系統功能是 ELF 執行緒本機儲存空間 ABI。這是變數的儲存模式,可讓每個執行緒擁有全域變數的專屬副本。如要進一步瞭解 Fuchsia 的用途,請參閱本文。如要進一步瞭解 Android 的使用情形,請參閱這篇文章。這些介面在 AArch64 上使用的登錄器為 TPIDR_EL0。雖然此項目會對應至 AArch32 做為 TPIDRURW,但 ARMv7 軟體不會預期具有對 TLS 登錄資料的讀/寫存取權。預期核心會代表其更新 TPIDRURO。這會對應至 AArch64 上的 TPIDRRO_EL0

例外狀況和呼叫慣例

如前所述,例外層級變更可提供變更執行狀態的方法。在 EL1 執行的 AArch64 核心設定 M[4] 位元後,然後傳回使用者空間 EL0,處理器就會在 AArch32 中執行。也就是說,任何觸發的例外狀況都會是 AArch32 例外狀況

當 AArch32 例外狀況觸發時,處理器會切換回 AArch64 狀態,執行例外狀況處理程式碼。由於註冊是直接對應的,因此處理 AArch32 例外狀況時,只需要進行最少的變更。從例外狀況傳回的操作同樣簡單。不過,您必須保留 APSR/CPSR 位元,確保執行模式和處理器模式標記相同。值得注意的是,程式計數器可能無法直接在 AArch64 上設定,而是會將例外狀況連結註冊 (ELR) 設為在從例外狀況返回 (ERET) 時,PC 需要設為的值。如果是從 AArch64 返回 AArch32 (透過 ERET),R15 會從例外狀況連結註冊 (ELR_EL0) 還原。

另外值得注意的是,系統呼叫的 Linux 呼叫慣例在 AArch64 和 AArch32 之間有所不同。在 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。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 的例外狀況處理程序大多是使用組譯巨集實作。這麼做可讓您輕鬆新增新的處理常式。另一個簡化方式是,傳入例外狀況會由受限模式的監督器處理。這樣一來,非同步例外狀況和系統錯誤 (serrors) 就能由 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

由於複雜性已傳遞至 Starnix,因此 Zircon 變更作業才能簡化。Starnix 服務會提供 Linux 使用者空間軟體所需的所有核心介面。它必須能夠執行 ELF 二進位檔,並處理該程式碼發出的系統呼叫,以及可能發生的其他例外狀況 (32 位元或 64 位元)。雖然有些差異無法避免,例如指標大小或系統呼叫號碼,但大部分的 AArch32 功能都可以透過現有程式碼無縫處理。

這項設計採用的方法是盡可能以接近其介面點的方式處理 AArch32 功能,這樣大部分的 Starnix 程式碼就不需要考量 32 位元架構的影響。這些互動點位於處理器層級,接著是系統呼叫項目、使用者空間複製 (傳送至和傳送自),以及可執行檔載入。其他只有在寄存器、指令長度或指標大小有影響的情況下才會發生,例如信號處理、ptrace 和核心傾印。

以下各節將以建設性的方式逐步介紹 Starnix。

工作支援

雖然如前述所述,從「Task」支援開始,而非從介面點開始,似乎有點奇怪,但這麼做是有充分理由的。Starnix 對 AArch32 的支援並非二進位狀態,而是同時支援 AArch32 和 AArch64 工作。因此,工作是可註解執行模式的邏輯單元。此外,執行模式是特定於執行緒的,因此這個設計會假設目前工作中的 ThreadState 結構體會指出執行緒是否為 'arch32' 執行緒。

請注意,我們使用的是「arch32」,而非「AArch32」,以免將命名與特定處理器實作綁在一起。

雖然您可以完全避免使用註解 (例如一律檢查 CPSR 中的註冊值),但專屬註解可提供兩個實用的屬性。第一個是讓 starnix 程式碼能因應未來需求。舉例來說,如果新增 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 指令碼產生 Rust 繫結。由於 UAPI 包含傳入和傳出核心介面的結構體,因此 Starnix 需要 AArch32 UAPI 才能正確處理這些結構體。

需要解決的三個主要問題如下:

  1. 32 位元指標和型別
  2. 並行存取兩個 UAPI
  3. 系統呼叫命名
指標和類型大小

就第一個問題而言,有兩個問題。首先,我們需要將所有指標替換為 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 產生步驟中引導所有變更,因此這裡也可以執行相同的操作。新增用於 Stub 和重新定義的 arch32 標頭後,即可將任何自訂定義拉入 arch32 命名空間,而無須持續維護任何以 Rust 為基礎的對應項目。

Loader 支援

有了上述三個元素,您仍無法有效啟動 AArch32 工作或執行緒。由於 CPSR 中的 M[4] 位元只能在從例外狀況 (EL1->0) 返回時變更,因此使用者空間必須有一種方式,要求 Starnix 在返回 EL0 之前設定位元。不需要特定支援,這項功能只會在呼叫 execve 系統呼叫或首次啟動、初始化、啟動程序時發生。無論是哪種情況,都必須載入檔案,且檔案格式必須由 Starnix 支援。目前,Starnix 支援 ELF64 格式檔案,可剖析、載入至記憶體,並最終執行。ELF32 格式檔案需要相同的功能,最好是盡量減少對 Starnix 核心的變更。

ELF 剖析

ELF 剖析是在 src/lib/process_builder/elf_parse.rs 中實作。所有必要的 ELF64 定義都會出現,並且能夠從 VMO 剖析 ELF64 檔案標頭。同樣定義也必須複製到 ELF32。不過,Starnix 預期能夠傳遞 Elf64* 結構體 (而非 Elf64* 特徵等)。因此,您可以類似於 uaddr32/uref32 的方式新增 ELF32 支援:定義足以直接與 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

當 ELF 及其轉譯器已成功剖析,Starnix 就會將 ELF 視為已解析。這會在執行呼叫完成前發生。雖然 AArch32 支援功能可以不經過這裡的變更就繼續執行,但這在工作設定中是關鍵時刻。此時,系統會為使用者程序建立虛擬記憶體區域 (VMAR)。通常,這個 VMAR 是可用的完整受限位址空間。不過,透過擴充對 MemoryManager (mm.exec()) 的呼叫,Starnix 可以建立適合最多 32 位元位址記憶體空間 (例如 4 GB)。

由於所有以記憶體為導向的作業都會使用工作「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 熵遮罩。遮罩會比左移一位元後的範圍大小減去頁面大小的位元數少一個。也就是說,最多可為 4Gb 位址區域套用 19 位元熵。對於 64 位元可尋址區域,這會高出許多,但原始的 AArch64 專屬位元熵會用作上限。

實際上,任何指定嘗試的隨機化範圍並非完整的位址空間,而是其他對應物件之間的區域,例如堆積頂端與堆疊。這就是為什麼隨機化熵值在實際應用中會大幅降低的原因。這項設計主要著重於啟用 AArch32 支援功能,因此應另外採取行動,以便評估 ASLR 實作中的熵,並判斷是否應單獨修改此項功能,以免影響效能。

如果效能變成難題,我們可以快取熵遮罩,並移至硬式編碼遮罩,一個用於 arch32 工作,另一個用於一般工作。一開始,目標是簡化呼叫端的使用者體驗,但可視需要導入最佳化功能,例如如果會對程式啟動時間造成負面影響。

vDSO

虛擬動態共用物件是一種合成共用程式庫,可在執行時將核心對應至程序。其目的是協助平滑使用者空間和核心之間的介面。vDSO 是核心提供的程式碼,可由程序在使用者空間中執行。舉例來說,系統呼叫輔助程式通常會出現,以確保使用核心呼叫慣例。同樣地,系統時間相關的呼叫通常會允許使用者空間程序嘗試檢查系統時間時,使用任何架構專屬的最佳化方式。

如要進一步瞭解如何在 Starnix 中管理 vDSO 時間,請參閱這篇文章。目前,vDSO 會依賴 Zircon 提供的程式庫功能,即 fasttime。不過,vDSO 必須以 AArch32 為目標進行建構,而 Zircon 並未計劃支援以 AArch32 架構進行建構。因此,您可以擷取快速時間邏輯,並納入 AArch32 專屬的 vDSO 輔助程式。請務必瞭解,在長期的 Zircon vDSO 成熟之前,這種做法只是權宜之計。這個模型依賴與 AArch64 Starnix 相同的介面,不會為 Zircon 造成額外的額外負擔,因此應可為 AArch32 提供類似的內容切換減少效果,且不會為臨時性解決方案造成大量技術工作。

除了快速時間外,現今的 Starnix vDSO 無法依附其他共用物件。時間運算通常會使用 64 位元整數,至少部分會使用這類整數。Clang 和其他編譯器會提供輔助程式 (例如 compiler-rt),讓 ARM 執行 64 位元模數和除法運算。一開始,這項工作只會在 C++ 中使用二進位分數,並匯出預期的符號。這些程式碼很可能需要替換為經過最佳化的彙整程式碼。也可能有其他符號和函式需要新增至 AArch32 vDSO,而這些符號和函式可能與 AArch64 vDSO 不同。您可以視需要新增這些屬性。同樣地,如果沒有必要,也可以從 AArch32 vDSO 中移除時間相關功能。

AArch32 vDSO 的建構作業也必須與 AArch64 vDSO 並行進行。因此,您必須新增可透過 arch32 工具鍊強制建構,且不依賴 Zircon 標頭的新目標。系統會在 Kernel 結構體中新增一個欄位,用來鏡像「vdso」欄位,也就是「arch32_vdso」。在缺少 arch32 vDSO 的情況下,這將是允許僅 arch32 可執行檔優雅失敗的 Option<>。

堆疊準備

在可執行檔啟動前,最後一個步驟就是以 ELF 進入點的程式碼可瞭解如何處理的方式設定其堆疊。Starnix 已處理這些步驟,從設定輔助向量 (AUXV) 到放置引數和環境變數。在 64 位元平台上,參數、環境變數和 AUXV 的整數指標都是 64 位元寬。此外,堆疊必須對齊 16 位元組。

也就是說,至少必須針對 ELF32 二進位檔調整指標和值的大小。在目前的 Starnix 實作項目中,系統會建立堆疊向量,然後複製到記憶體中。如要支援 AArch32,最簡單的方法就是複製程式碼路徑並個別處理。不過,堆疊向量是無符號 8 位元位元組的向量。這表示可以將寫入作業封裝到堆疊中,以便選取正確的寬度 (8 個位元組或 4 個位元組)。在進行這類變更後,最終堆疊應會自動調整為正確大小,因為要寫入的值必須已正確調整大小。此外,由於 32 位元堆疊必須以 8 位元組對齊,因此 16 位元組對齊已符合需求。

目前 Starnix 堆疊準備作業中缺少的部分,是可透過探測發現的功能,但可能會在程序執行時透過 HWCAP 項目揭露,例如是否支援 Thumb 模式,或允許哪種浮點運算。這些項目最早會在 AArch32 glibc 和 bionic 載入器中使用。您必須在每個核心/架構/處理器中新增函式,以便傳回處理器支援的硬體功能清單。每個實作項目都可以使用 '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) 核心之間的主要介面。也就是說,您必須處理兩項主要挑戰:

  1. 呼叫慣例和轉送
  2. 使用者空間記憶體讀取和寫入
AArch32 系統呼叫

如前所述,Linux AArch32 系統呼叫使用的編號與 AArch64 系統呼叫不同。此外,AArch32 使用的系統呼叫號碼註冊機制與 AArch64 不同,兩者的系統呼叫參數也可能不同。

從參數的角度來看,由於註冊會在進入 Starnix 時延伸,因此不需要處理指標大小的差異。數字和參數的差異帶來更有趣的挑戰。Starnix 必須能夠依號碼查詢系統呼叫,並將該號碼對應至實作項目。雖然可以強制每個系統呼叫實作項目執行某些 arch32 檢查作業,但這項工作可以在系統呼叫處理期間完成。也就是說,Starnix 可以設定次要系統呼叫表,但只用於 arch32 工作。這會導致額外記憶體使用量略微增加,但可提供一種強制執行 AArch32 與 AArch64 使用者空間 ABI 的方法,並提供一種簡單的方法,可透過 AArch32 感知包裝函式直接或間接共用系統呼叫實作項目。

這種做法至少在初期會需要重複使用一些巨集,但也可能會盡量減少重複。

AArch32 資料結構

雖然系統呼叫參數中的指標不會造成問題,但從 AArch32 使用者空間複製結構或從 AArch32 使用者空間複製結構時,情況就不同。AArch32 軟體會使用 AArch32 Linux UAPI 中定義的結構體,而非 AArch64 UAPI。舉例來說,IO 向量 (iovec) 是結構體,也就是指標和大小配對的陣列。在 AArch32 上,iovec 是兩個 32 位元欄位,而在 AArch64 上,則是 64 位元欄位。這表示任何使用 iovec 的系統呼叫都必須適當調整。

不過,這項問題不會影響每個系統呼叫。約三分之一的 AArch32 系統呼叫具有與 AArch64 對應項目不同的結構。針對特定呼叫,可在特定架構的系統呼叫實作中處理特定結構體。舉例來說,stat 系統呼叫會預期傳回 stat64 結構體,但這不符合從 AArch64 stat 呼叫傳回的結構體。您可以使用 kernel/arch/arm64/syscalls.rs 下的全新統計資料進入點,以及類型之間的轉換,解決這個問題。

常見的結構體 (例如 iovec) 可能會以更通用的方式處理。Starnix 的 MemoryManager 提供 read_iovec() 輔助程式,用於讀取 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 的 u 測試,我們會利用建構系統的 loadable_module() 支援功能,建構可使用 elfldltl 程式庫動態載入及對應的架構專屬共用程式庫。這樣一來,整合測試就能在 AArch64 主機架構上執行,同時從 AArch32 目標 (armv7-linux) 共用程式庫載入及解析符號。這些共用程式庫會載入,並且會與 BOOTFS 上的任何其他可執行程式碼一樣,對應為可執行程式。

其餘的規定則是圍繞主機支援的內容,以及受限的 AArch32 共用程式庫可存取的內容。共用程式庫必須對應至可尋址記憶體 (低於 4GB) 以及任何共用指標,例如 std::atomic 或 TLS 儲存空間位址。在完成這項設定後,測試也必須判斷主機架構是否支援執行 AArch32,如果支援,則針對主機指定的限制模式和 AArch32 指定的限制模式執行測試套件。

Starnix 測試支援

初步驗證將針對在 Starnix 下執行的 Linux 最小系統進行。這樣可確保在驗證 Starnix 的工具鍊和建構系統變更時,只需最少的依附元件。為啟用這項功能,ARM64 Debian 容器會將 AArch32 sysroot 新增至映像檔,而 example/hello_debian 範例會擴充為包含 AArch32 範例,包括動態連結的 C++ 和靜態連結的組合語。目標支援和工具鍊 sysroot 必須大致同步。

初始測試環境包含 Starnix 執行一個二進位檔 (hello_debian) 的 AArch64 程式庫或 AArch32 程式庫。不會有其他程序或工作正在進行。不過,您必須定期測試,才能涵蓋與 AArch64 的差異:

  • 系統呼叫支援範圍
  • 信號和 ptrace 行為
  • 混合執行模式環境 (共用 sysroot、容器等)
  • 依據支援的 ELF 檔案類型進行測試
  • ASLR 熵值測量

其中部分測試需要針對 Starnix 使用情形編寫,但理想狀態是使用現有專案,例如 Linux 測試專案,因為該專案已旨在確保持續的 Linux 相容性。

Zircon

您需要新增一些 AArch32 專屬測試:

  • CPSR:M[4]: A32 執行
  • CPSR:T|M[4]:T32 執行
  • 註冊設定和清除
  • > 32 位元登錄設定、清除行為
  • 系統呼叫處理
  • 非系統呼叫同步例外狀況處理
  • 非同步例外狀況處理
  • SError 處理
  • CPSR:Q:透過系統呼叫設定飽和位元並保留
  • 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 系統進行基準測試,這將是最有益的做法。

持續進行系統呼叫最佳化、記憶體使用率和電源監控,有助於確認這項功能的可行性。

Ergonomics

在 Zircon 中,目前的方法不會導致特定一組 API 變更。相反地,您可以透過在受限模式下支援現有的架構 ABI 來啟用 AArch32 支援。這種做法不會大幅增加開發人員的複雜度,也不會降低人因工程學效益。任何使用這項功能的開發人員都必須自行實作 ABI 的其餘部分。

在 Starnix 中,變更內容一開始會受到標記保護。必須區分的程式碼路徑應盡量靠近發生區分作業的位置,並盡量減少在 starnix 中發生的程式碼分支。舉例來說,如果記憶體限制可以在 MemoryManager 中強制執行,那麼從開發人員的角度來看,所有仰賴 MM 的系統呼叫和其他功能都不會有所改變。

回溯相容性

AArch32 使用者空間支援功能基本上是回溯相容性功能。我們預期 AArch32 的實用性會隨著時間流逝而逐漸降低。

安全性考量

此提案會變更 Zircon 處理例外狀況和 ABI 的方式。因此,您應仔細評估這類內容。此外,程式碼應以防禦性的方式編寫,主動禁止不當的程式碼路徑。如果基準測試顯示有需要,系統就會進行最佳化。

從 Starnix 的角度來看,系統呼叫和信號處理路徑會增加,檔案剖析也會增加。地址空間隨機化也會直接受到記憶體占用空間減少的影響。這些都是與安全性相關的變更。

我們應在適當情況下重複使用現有的 Linux 安全性測試工具,例如 syzkaller

隱私權注意事項

這項變更不應有任何特定的隱私權變更。

說明文件

我們會更新受限制模式 API 說明文件,指出額外允許的註冊標記。

Starnix 說明文件應更新為包含如何使用及開發 AArch32 支援功能的相關資訊。其他說明文件應著重於開發人員的影響,例如管理兩個系統呼叫表格,或測試 AArch64 和 AArch32 路徑的變更。

在 src/starnix/examples/hello_debian 中,您可以找到在 Starnix 中執行的 AArch32 二進位檔範例。

缺點、替代方案和未知因素

這項提案會增加長期維護作業和系統複雜度。權衡之處在於,在消費者導向的 SoC 支援的剩餘年數內,您可以存取 AArch32 Linux 軟體。

目前沒有任何效能良好的替代方案可支援未經修改的 AArch32 程式,因此建議您將所有程式碼重新編譯為 AArch64。這些效能較差的替代方案是轉譯或選擇性使用者模式模擬。

我們將持續調查的主要領域,以便在分析同時含有 AArch64 和 AArch32 呼叫慣例和指令集的執行緒時,提供優質的偵錯體驗。

具體來說,目前的偵錯工作流程會使用 libunwind,透過 AArch64 上的 Linux 呼叫堆疊進行解開,並使用 zxdb 分析特定呼叫框架中的狀態。您必須在 zxdb 和 libunwind 中加入類似行為的支援功能。libunwind 應該已支援 AArch32 Linux ABI,但不太可能支援在同一個執行緒中,在 ABI 之間進行清楚的轉換。我們的目標是讓這兩項工具達到一致,但這需要額外的調查和實驗。

除了偵錯之外,開發人員的工作流程也可能會受到全面測試的影響。使用功能強大的 Intel 架構環境的開發人員,必須完整模擬 AArch64/AArch32 環境。不過,如果應用程式已指定 AArch64,則這項說法目前是正確的。如先前所述,這裡的複雜性增加,是因為伺服器導向 ARM SoC 缺乏 AArch32 支援。以 Intel 為基礎的開發人員可能會依賴加速的伺服器端 AArch64 測試,現在則需要使用桌面或代管裝置,這類裝置支援這兩種執行模式。您可能會使用 starnix 中的執行緒個性,啟用或停用 AArch32 專屬的呼叫路徑,進一步減少在沒有 AArch32 硬體支援的情況下,無法大規模執行的程式碼。

既有技術與參考資料

參考資料已嵌入為連結。Linux 將 AArch64 上的 AArch32 支援稱為相容性模式,並提供先前技術。