管理 Zircon syscall struct 的版本

本指南說明如何變更跨越 Zircon 系統呼叫邊界的 struct,而不破壞現有二進位檔。這項程序在過去稱為「結構版本管理」或「結構演進」。目標對象是核心開發人員,以及任何修改 Zircon 系統介面的人員或代理程式。

如果您要新增、移除、重新排序、調整大小、重新命名或變更 //zircon/system/public/zircon/ 下宣告的任何 struct 欄位 (或鏡像至 //sdk/rust/zx-types),請先閱讀本指南。

如果結構體僅由加註 @next 的系統呼叫使用 (在 zircon/syscalls-next.h 中宣告),則不在本指南的討論範圍內。這些系統呼叫不屬於穩定 ABI,因此其結構遵循不同的規則。

為什麼無法就地變更結構體

syscall ABI 是核心與每個針對支援的 API 級別建構的二進位檔之間的合約,包括未與核心一起重建的預先建構二進位檔 (例如 SDK 使用者、預先建構套件和 CTF 測試)。這些二進位檔內建每個 struct 的大小、版面配置和意義。

核心只需要支援針對支援的 API 級別建構的二進位檔。與平台其他部分一樣,核心可以在 API 級別停用後,停止支援該級別。這會造成兩種影響:

  • API 級別淘汰後,核心可以停止支援僅限指定該級別的二進位檔使用的 struct 版本。請參閱「移除舊版本」。
  • 目前的標頭會使用 availability.h 中的 API 級別巨集 (例如 ZX_AVAILABLE_SINCE 和 FUCHSIA_API_LEVEL_AT_LEAST),視需要描述每個支援的 API 級別。您不必深入研究標頭的歷史記錄,或考慮已淘汰的 API 級別,就能找出核心必須支援的 struct 版本。

如要進一步瞭解 API 級別和適用的相容性保證,請參閱「RFC-0239:平台版本管理實務」和「Fuchsia 平台 API 演進指南」。

以下任何變更都會在不發出通知的情況下,破壞現有二進位檔:

  • 新增欄位,即使是在結尾處。舉例來說,如果是 zx_object_get_info(),核心會複製 sizeof(struct) 個位元組,並在呼叫端的緩衝區較小時傳回 ZX_ERR_BUFFER_TOO_SMALL。如果舊二進位檔傳遞的緩衝區大小適用於舊結構體,就會開始失敗。
  • 移除、重新排序或調整欄位大小。舊二進位檔讀取或寫入錯誤的位元組。
  • 變更現有欄位的意義或單位。版面配置不變,但舊二進位檔會誤解該值。

重新命名欄位而不變更其版面配置或含義,可與 ABI 相容,但會破壞來源相容性。請參閱「來源相容性」。

設計原理

版本管理配置遵循下列原則:

  • 保持呼叫端簡單。核心會負責支援多個版本。呼叫端不需要程式庫,就能編碼、解碼或轉譯結構體 (就像使用 protobuf 樣式的編碼一樣)。每種支援的語言都可以透過其原生 FFI 使用純 C 結構版面配置。
  • 維持成效:如果單一 syscall 目前會取得或設定一組相關值,新版本仍須允許在單一 syscall 中執行這項操作。
  • 允許柔和轉場。介面會逐步演進:新增版本與舊版本並存、遷移呼叫端,然後移除舊版本。

考慮過的替代方案

  • 只能在 struct 結尾附加新欄位。如果 syscall 讀取或寫入 struct 陣列 (例如 ZX_INFO_PROCESS_MAPS 或 ZX_INFO_PROCESS_VMOS),則不適用這種做法,因為元素步幅會改變。此外,這也會強制將新欄位放在可能浪費對齊邊框空間的位置。有了版本化結構體,每個新版本都能將欄位放在邏輯上所屬的位置。
  • 在每個 struct 中新增 version 欄位。這對 struct 陣列來說很麻煩,且需要更多核心工作才能輸出 struct。由於幾乎所有採用或傳回結構體的系統呼叫都已有 topic 或 options 引數,因此版本會改為在該處編碼。請勿在 syscall 結構中新增版本欄位。

版本管理配置

請勿直接修改 struct,而是導入新版 struct,並繼續提供舊版,直到沒有二進位檔依賴舊版為止:

  1. 使用版本化名稱 (例如 zx_info_vmo_v3_t) 建立現有結構的快照,並在核心中繼續支援該結構。
  2. 將未版本化的名稱 (zx_info_vmo_t) 參照至新版面配置。
  3. 當所有呼叫端都已針對新版本重新編譯,且沒有針對支援的 API 級別建構的二進位檔使用舊版本,請刪除舊版本。

只有在呼叫端能告知核心其使用的版本時,這個配置才有效。每個支援版本管理功能的系統呼叫,其引數中都有版本管道:

系統呼叫 版本管道 編碼 範例
zx_object_get_info() topic 引數 主題的前 4 個位元,使用 object.h 中的 __ZX_INFO_TOPIC(topic, version) ZX_INFO_VMO_V3 = __ZX_INFO_TOPIC(23u, 2),ZX_INFO_VMO = __ZX_INFO_TOPIC(23u, 3)
zx_clock_create()、zx_clock_get_details()、zx_clock_update() options 引數 clock.h 中的 ZX_CLOCK_ARGS_VERSION(n) (第 58 位元的 6 個位元) zx_clock_update_args_v1_t和zx_clock_update_args_v2_t
zx_job_set_policy() topic 引數 policy.h 中的不重複主題常數 ZX_JOB_POL_BASIC_V1和ZX_JOB_POL_BASIC_V2

__ZX_INFO_TOPIC 編碼有 4 位元的版本,因此單一 zx_object_get_info() 主題在生命週期內最多可有 16 個版本。

沒有版本管道的系統呼叫

有幾個系統呼叫會採用或傳回結構,但沒有版本管道:

  • 結構體視為最終版本的系統呼叫。例如,zx_object_wait_many() 會採用 zx_wait_item_t 陣列。struct 已包含等待信號所需的所有內容,因此變更會暗示不同的作業 (例如等待信號以外的內容)。在這種情況下,請新增 syscall。
  • 結構格式由其他系統呼叫決定的系統呼叫。舉例來說,zx_port_queue() 和 zx_port_wait() 沒有版本管道,但會交換 zx_port_packet_t (標記聯集)。相同大小的新封包類型可以新增至聯集,而 zx_port_create() 的 options 引數可以為通訊埠選取不同的封包格式。

否則,如果使用或產生 struct 的 syscall 沒有主題、選項位元或其他方式可讓呼叫端選取版本,就無法使用這個配置。您可以選擇新增系統呼叫,或是在新的系統呼叫中新增版本管道,並將呼叫端遷移至該管道。無論選擇哪一種,都會變更 Zircon 系統介面,請參閱「程序規定」。

設計會採用或傳回 struct 的新 syscall 時,請從一開始就加入版本管道 (例如 options 中的保留版本位元),以便日後更新 struct。系統 API 評分標準要求這麼做。

命名慣例

請遵循 object.h 中的現有慣例:

  • 未版本化的名稱 (zx_info_vmo_t、ZX_INFO_VMO) 一律參照最新版本。大多數程式碼都應使用這些。
  • 舊版的 struct 會加上 _vN 後置字串 (zx_info_vmo_v1_t、zx_info_vmo_v2_t、...),主題或常數則會加上 _VN 後置字串 (ZX_INFO_VMO_V1、...)。
  • 變更的巢狀結構會與其父項一起建立版本 (例如 zx_info_maps_v2_t 包含 zx_info_maps_mapping_v2_t)。請參閱「巢狀和共用結構」。
  • 版本號碼一律不會重複使用。移除舊版本後,無版本主題會保留其版本位元。例如,即使 ZX_INFO_PROCESS_V1 已移除,ZX_INFO_PROCESS 仍為 __ZX_INFO_TOPIC(3u, 1u)。

巢狀和共用結構體

版本號碼會識別核心針對該呼叫讀取或寫入的所有內容,而不只是最外層的 struct。請將版本管道選取的 struct 視為「根」。根目錄版本涵蓋的 ABI 包括:

  • 根目錄依值遞迴嵌入的每個 struct、陣列和 union。
  • 核心透過根指標讀取或寫入的任何資料。

因此,當您變更 struct 時,版本升級會向上傳播到每個可觸及的根:

  • 依值嵌入:如果巢狀結構的大小變更,則包含該結構的所有結構版面配置都會變更,這些結構的任何陣列步幅也會變更。即使大小不變,舊二進位檔也會誤讀含義已變更的欄位。
  • 透過指標存取:外部 struct 的版面配置不會變更,但舊二進位檔仍會傳遞或預期指向資料的舊格式。核心只能依據根目錄的版本區分格式,因此根目錄需要新版本。

由於核心會繼續提供舊根,因此變更後的結構體和根之間的每個結構體也需要舊副本。舉例來說,當 zx_arm64_exc_data_t 成長時,舊版面配置會保留為 ZX_INFO_THREAD_EXCEPTION_REPORT_V1 下提供的版本化結構鏈:

typedef struct zx_exception_report_v1 {
    zx_exception_header_t header;
    zx_exception_context_v1_t context;   // contains zx_arm64_exc_data_v1_t
} zx_exception_report_v1_t;

版本管理只會向上傳播至根目錄。變更外部 struct 時,不需要為其包含的 struct 建立版本。

共用結構體

由於未版本化的名稱一律參照最新版本,因此變更未版本化的結構體會變更「每個」嵌入該結構體的結構體,包括您不打算變更的系統呼叫所使用的結構體。如果其中任何系統呼叫未取得版本更新,就會導致 ABI 中斷。

舉例來說,zx_exception_context_t 會透過多個路徑到達核心:

  • 在 zx_exception_report_t 內,由 zx_object_get_info() 傳回 ZX_INFO_THREAD_EXCEPTION_REPORT。
  • 做為 zx_thread_raise_exception() 的 context 引數。
  • 在 zx_restricted_exception_t 內 (透過 zx_exception_report_t),嚴格篩選模式寫入時不含任何版本管道。這個結構體目前之所以豁免,只是因為它是在不穩定的 zircon/syscalls-next.h 中宣告。

變更結構前,請找出可從中連到的每個根:每個 zx_object_get_info() 主題、透過 options 版本控制的每個引數,以及核心遵循的每個指標。針對包含已變更 struct 的每個 struct,執行下列其中一項操作:

  • 也為其建立版本,並與根層級代理一起建立。這需要根目錄具有版本管道。
  • 將其固定在舊版本,方法是將其變更為明確嵌入 _vN struct,使其版面配置保持不變。

如果任何受影響的根目錄沒有版本管道,請參閱「沒有版本管道的 Syscall」。

結構體版面配置規則

新的 struct 版本必須遵循 syscall struct 的一般規則:

  • 使用固定寬度類型 (uint32_t、uint64_t、zx_koid_t 等)。
  • 使用 uint8_t padding1[4]; 等具名欄位明確指定所有邊框間距。請勿依賴隱含編譯器邊框間距。
  • 建議使用 uint32_t 或 uint64_t 中的位元旗標,而非 bool 欄位。
  • 記錄每個欄位,包括是否僅在某些情況下有效。

逐步說明:新增版本

下列步驟說明近期變更所使用的常見單一 CL 方法,例如導入目前 ZX_INFO_KMEM_STATS (RFC-0262) 的變更,以及 RFC-0254 的 zx_info_vmo_t、zx_info_maps_t 和 zx_info_task_stats_t 版本。

1. 更新 C 標頭

在 //zircon/system/public/zircon/syscalls/ 下方的適當標頭中:

  1. 將目前的結構定義逐字複製到具有下一個 _vN 名稱的新結構 (例如 zx_info_kmem_stats_v1_t)。版面配置必須與現有結構完全相同。
  2. 為該舊結構體新增 _VN 主題或常數,並使用無版本主題的現有數值。舊二進位檔會繼續傳送這個值。
  3. 將未建立版本的 struct (zx_info_kmem_stats_t) 修改為新的版面配置。
  4. 提高無版本主題的版本位元 (例如從 __ZX_INFO_TOPIC(17u, 0) 提高至 __ZX_INFO_TOPIC(17u, 1))。
  5. 如果變更的 struct 嵌入其他 syscall struct 或由其他 syscall struct 指向,請按照「巢狀和共用 struct」一節所述,為每個 struct 及其根目錄建立版本或固定。

舉例來說,ZX_INFO_KMEM_STATS 的版本編號如下:

-#define ZX_INFO_KMEM_STATS     ((zx_object_info_topic_t) 17u) // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS     __ZX_INFO_TOPIC(17u, 1)        // zx_info_kmem_stats_t[1]
+#define ZX_INFO_KMEM_STATS_V1  __ZX_INFO_TOPIC(17u, 0)        // zx_info_kmem_stats_v1_t[1]

2. 更新核心

核心必須接受所有支援的版本。

  • 輸出結構體 (例如 zx_object_get_info() 主題):先以最新的內部形式收集資訊,然後轉換為要求的版本,再複製出來。請參閱 object_info.rs 中 ZX_INFO_VMO_V1、ZX_INFO_VMO_V2 和 ZX_INFO_VMO_V3 的處理方式,以及 memory_stats.rs 中的 vmo_info_to_version 和 MapsInfoVersion 輔助程式。
  • 輸入結構體 (例如 zx_clock_update() 引數或 zx_job_set_policy() 政策):從版本管道解碼版本、驗證「該」版本的緩衝區空間,並將舊版本轉譯為內部表示法,並為新欄位定義明確的預設值。使用 ZX_ERR_INVALID_ARGS 拒絕不明版本。

3. 更新繫結、說明文件和驗證工具

在樹狀結構中搜尋現有的主題和結構名稱,並更新所有對應的位置。通常包括:

4. 新增測試

新增或更新 Zircon 核心測試 (例如在 //zircon/system/utest/core/ 下),測試「每個」支援的版本,而不只是最新版本。舉例來說,請參閱 vmo.cc 如何查詢 ZX_INFO_VMO_V1、ZX_INFO_VMO_V2、ZX_INFO_VMO_V3 和 ZX_INFO_VMO,並檢查共用欄位是否一致。

使用 fx core-tests 執行核心測試,並使用 fx build 建構。

來源相容性

上述配置可為所有現有二進位檔保留 ABI 相容性。來源相容性取決於變更類型:

  • 僅新增欄位:使用未版本化名稱的程式碼會在重新編譯時採用新版本,且通常會繼續編譯。如果程式碼仰賴確切大小或每個欄位的匯總初始化,可能需要更新。
  • 移除或重新命名欄位:這會破壞使用這些欄位的程式碼來源相容性。在同一個 CL 中更新所有樹狀結構內使用者。樹狀結構外使用者可以固定舊的 _vN 名稱,直到遷移為止。

如果來源破壞性變更會影響許多樹狀結構外程式碼,請改用 RFC-0074 中所述、以別名為基礎的軟轉換:

  1. 同時導入 _V1 和 _V2,並將未版本化的名稱暫時別名為 _V1。
  2. 將所有呼叫端遷移至 _V2。
  3. 將未建立版本的別名重新指向 _V2。
  4. 將呼叫端遷移回未版本化的名稱。
  5. 移除_V1。

移除舊版本

只有在沒有任何二進位檔 (針對支援的 API 級別建構) 仍可使用舊版本時,才能移除舊版本。指定已淘汰 API 級別的二進位檔則不需考慮。移除前,請確認下列事項:

  • 沒有任何樹狀結構內程式碼參照 _vN_t 結構體或 _VN 常數。
  • 沒有預先建構的二進位檔、支援 API 級別的 SDK 使用者,或 CTF 測試依附於此。如有疑問,請洽詢 Zircon 核心擁有者。

移除 CL 應從 C 標頭、核心處理、Rust 繫結、ABI 驗證工具項目、工具解碼器、vDSO 說明文件和該版本的測試中,刪除舊的 struct 和常數。舉例來說,請參閱移除 ZX_INFO_PROCESS_V1 的 CL (修訂版本 934bcf504d3)。

請勿重新編號其餘版本,也不要重複使用已移除的版本編號。

程序需求

  • 變更 syscall 結構體會影響 Zircon 系統介面,因此需要 API Council 成員的 API-Review+1。
  • 新增或移除 Zircon 系統介面,或大幅變更其語意,可能需要 RFC。請參閱「RFC-0006:Zircon RFC 程序附加條款」。
  • 在所有版本中變更現有欄位的含義是例外狀況,需要明確理由和 API 審查,如 RFC-0254 所示。

既有技術

以下 RFC 採用這項配置:

如要瞭解新增系統呼叫的機制,請參閱「新增系統呼叫」。