Zircon vDSO

Zircon vDSO 是 Zircon 中系統呼叫的唯一存取方式。vDSO 代表虛擬動態共用物件。(動態共用物件是 ELF 格式的共用程式庫用語)。而是「虛擬」,因為不是從位於檔案系統的 ELF 檔案載入。而是由核心直接提供 vDSO 映像檔。

使用 vDSO

系統呼叫 ABI

vDSO 是 ELF 格式的共用資料庫。運作方式與 ELF 共用程式庫的使用方式相同,也就是透過 ELF 動態符號表中的符號名稱 (.dynsym 區段,透過 DT_SYMTAB 查詢) 來查詢進入點。ELF 會定義雜湊資料表格式,以便依符號資料表中 (.hash 區段,透過 DT_HASH 找到 .hash) 更高效的雜湊資料表格式。.gnu_hashDT_GNU_HASHDT_GNU_HASH(您也可以透過線性搜尋直接使用符號表格,忽略雜湊表)。

vDSO 使用簡化的版面配置,沒有可寫入的區隔,且不需要動態重新定位。這樣就不必實作一般用途的 ELF 載入器和完整的 ELF 動態連結語意,也能更輕鬆地使用系統呼叫 ABI。

ELF 符號名稱與含有外部連結的 C ID 相同。 每項系統呼叫都會對應至 vDSO 中的 ELF 符號,並具備 C 函式的 ABI。vDSO 函式只會使用基本機器專屬 C 呼叫慣例,規範機器註冊和堆疊的使用。許多使用 ELF 的系統 (例如 Linux 和所有 BSD 變化版本) 都很常見。這些 API 不需依賴 ELF Thread-Local Storage 等複雜的功能,也不使用 Fuchsia 專用的 ABI 元素,例如 SafeStack 不安全的堆疊指標。如要進一步瞭解系統呼叫的生命週期及其與 vDSO 的關係,請參閱 Fuksia 系統呼叫的生命週期一文。

vDSO 解放資訊

vDSO 含有 PT_GNU_EH_FRAME 類型的 ELF 計畫標頭。這會指向以 GNU .eh_frame 格式解開的資訊,這是與標準 DWARF 呼叫頁框資訊格式相當的相對關係。這項資訊可讓您從 vDSO 程式碼中呼叫影格復原註冊值,這樣就能從任何執行緒的登錄狀態,使用 vDSO 程式碼中具有電腦值重建完整的堆疊追蹤。這些格式及其用途在 vDSO 中,與 Fuchsia 或其他使用常見 GNU ELF 擴充功能 (例如 Linux 和所有 BSD 變化版本) 上的一般 ELF 共用資料庫相同。

vDSO 版本 ID

vDSO 具有 ELF 版本 ID,如同其他使用常見 GNU 擴充功能建構的 ELF 共用程式庫和執行檔。版本 ID 是不重複的位元字串,可識別該二進位檔的特定版本。這會以 ELF 記事格式儲存,指向 PT_NOTE 類型的 ELF 程式標頭。名為 "GNU" 和類型 NT_GNU_BUILD_ID 的附註酬載是構成版本 ID 的位元組序列。

建構 ID 的主要用途之一,是將二進位檔與其偵錯資訊及用於建構這些 ID 的原始碼建立關聯。vDSO 二進位檔與核心二進位檔密切連結 (並在內部嵌入),其中包含每個核心版本的專屬資訊,因此 vDSO 的版本 ID 也能區分核心。

zx_process_start() 引數

zx_process_start() 系統呼叫是程式載入器指示核心開始新程序的第一個執行緒執行方式。最後一個引數 (zx_process_start() 說明文件中的 arg2) 是傳遞至註冊資料庫中新執行緒的純 uintptr_t 值。

依照慣例,程式載入器會將 vDSO 對應至每個新程序的位址空間 (在系統選擇的隨機位置),並將圖片的基本位址傳遞至 arg2 登錄中的新程序的第一個執行緒。這個地址可以在記憶體中找到 ELF 檔案標頭,指向查詢符號名稱並進而發出系統呼叫所需的所有其他 ELF 格式元素。

PA_VMO_VDSO 控制代碼

vDSO 映像檔會在編譯期間嵌入核心。核心會將其以唯讀 VMO 的形式對使用者空間公開。

程式載入器設定新程序後,要使該程序發出系統呼叫的唯一方法,就是讓程式載入器在第一個執行緒開始執行前,將 vDSO 對應至新程序的位址空間。因此,每個會啟動其他可發出系統呼叫程序的程序都必須能夠存取 vDSO VMO。

依照慣例,vDSO 的 VMO 控制代碼會從程序傳送到各個新程序的 zx_proc_args_t 啟動訊息中處理到處理 (請參閱「<zircon/processargs.h>」)。處理表中 VMO 控制代碼的項目會由處理資訊項目 PA_HND(PA_VMO_VDSO, 0) 識別。

vDSO 導入詳細資料

zither 工具

zither 工具會產生 C/C++ 函式宣告,這些宣告會形成公開系統呼叫 API,以及實作 vDSO 時使用的某些 C++ 和組合程式碼。核心與 vDSO 程式碼之間的公用 API 和私人介面,是由 //zircon/vdso 中的 .fidl 檔案指定。

系統呼叫分為下列幾個群組,具體區分系統呼叫名稱後方的屬性:

  • vdsocallinternal 的項目都是簡單的情況 (這是系統呼叫的主要案例),其中公用 API 與私有 API 完全相同。這是完全由產生的程式碼實作。公用 API 函式的名稱前置字串為 _zx_zx_ (別名)。

  • vdsocall 項目只是公用 API 的宣告。這些函式是透過核心來源中的一般手寫 C++ 程式碼實作。這些來源檔案 #include "private.h",然後在名稱前面加上 _zx_ 做為系統呼叫的 C++ 函式定義。最後,這些呼叫會在系統呼叫的名稱中使用 VDSO_INTERFACE_FUNCTION 巨集,並在開頭加上 zx_ (前面沒有底線)。此實作程式碼可以為任何其他系統呼叫項目 (無論是公開的呼叫、公開手寫 vdsocallinternal 產生的呼叫) 呼叫 C++ 函式,但必須使用其私人進入點別名,其中包含 VDSO_zx_ 前置字串。否則,程式碼屬於正常 (最小) C++,但必須為無狀態和遞補 (僅使用其堆疊和註冊)。

  • internal 項目是私人 API 的宣告,只有 vDSO 實作程式碼會使用該 API 進入核心 (例如由實作 vdsocall 系統呼叫的其他函式使用)。這些物件在 vDSO 實作中產生的函式會與 C 簽名相同;該簽名會在公用 API 中宣告,前提是系統呼叫項目的簽章。不過,這些前置字串只能透過 #include "private.h" 加上 VDSO_zx_ 前置字串,而非使用 _zx_zx_ 前置字串命名。

唯讀動態共用物件版面配置

vDSO 是一般的 ELF 共用資料庫,可以視為其他程式庫。不過,我們刻意保留到一般的 ELF 共用程式庫中可執行的一小部分。這有幾個優點:

  • 將 ELF 圖片對應至單一程序相當簡單,而且不涉及任何複雜的 ELF PT_LOAD 程式標頭一般支援情況。vDSO 的版面配置可由特殊案例程式碼處理,沒有迴圈會讀取 ELF 標頭中的少數值。
  • 使用 vDSO 不需要完整的 ELF 動態連結。具體來說,vDSO 沒有動態重新定位。只需進行 ELF PT_LOAD 區隔中的對應設定即可。
  • vDSO 代碼為無狀態且可重複作業。這僅指呼叫其呼叫的註冊和堆疊。如此一來,就能在各種情境中使用,而且對於使用者程式碼本身組織方式的限制也幾乎沒有限制,非常適合作業系統的必要 ABI。也能讓程式碼更容易進行推理和稽核,從而確保穩定性和安全性。

版面配置只有兩個連續區段,每個片段都包含對齊的整個頁面:

  1. 第一個區隔是唯讀狀態,包含動態連結的 ELF 標頭和中繼資料,以及 vDSO 實作方式專屬的常數資料。
  2. 第二個區隔是可執行檔,其中包含 vDSO 程式碼。

整個 vDSO 圖片僅包含這兩個片段的頁面,而 ELF 圖片中呈現的頁面應與記憶體中顯示的相同。如要對應 vDSO,只需要從 vDSO 的 ELF 標頭中擷取出兩個值:每個區隔的頁數。

開機唯讀資料

部分系統呼叫只會傳回在整個系統執行階段中固定的值,但系統 ABI 必須在執行階段查詢其值,因此無法編譯至使用者程式碼。這些值在編譯期間在核心中是固定的,或是由核心在啟動時根據硬體或啟動參數所決定。例如 zx_system_get_version_string()zx_system_get_num_cpus()zx_ticks_per_second()

由於這些值是固定值,因此不需支付進入核心讀取核心的負擔。相反地,這些的 vDSO 實作是簡單的 C++ 函式,只會傳回從 vDSO 唯讀資料區隔讀取的常數。編譯時間固定的值 (例如系統版本字串) 會直接編譯到 vDSO。

針對啟動時決定的值,核心必須修改 vDSO 的內容。這項作業是由設定 vDSO VMO 的啟動時間程式碼完成,然後開始第一個使用者空間程序並提供 VMO 控制代碼。在編譯期間,系統會從將嵌入核心的 vDSO ELF 檔案擷取 vdso_constants 資料結構的 vDSO 圖片偏移值。在啟動時,核心會暫時將涵蓋 vdso_constants 的 VMO 頁面對應至其本身的位址空間,以便將結構初始化使用正確的值來初始化系統執行作業。

違規處置

vDSO 進入點是唯一輸入系統呼叫核心的方法。用於輸入核心的機器專用指令 (例如 x86 上的 syscall) 不屬於系統 ABI,而且無法直接執行這類指示。核心和 vDSO 程式碼之間的介面是私人的實作詳細資料。

由於 vDSO 是在使用者空間中執行的一般程式碼,因此核心必須妥善處理使用者空間中所有可能進入核心模式的項目。不過,您可以強制讓每個核心項目只透過適當的 vDSO 程式碼產生,從而減輕潛在的核心錯誤。這項強制措施還可避免開發人員規避 ABI 規則規避 ABI 規則 (因為忽略、惡意或不當意圖企圖因應官方 ABI 的部分限制),這可能導致私人核心 vDSO 介面成為應用程式程式碼的「事實」ABI。

核心會透過兩種方式強制正確使用 vDSO:

  1. 限制 vDSO VMO 可對應至程序的方式。

    使用 vDSO VMO 發出 zx_vmar_map() 呼叫並要求 ZX_VM_PERM_EXECUTE 時,核心要求對應的偏移值和大小必須與 vDSO 的可執行片段完全相同。也只允許進行一次對應。透過程序建立有效的 vDSO 對應後,即無法移除。嘗試再次將 vDSO 對應至同一程序、從程序取消對應 vDSO 程式碼,或為未使用正確偏移和大小的 vDSO 建立可執行對應作業,會導致 ZX_ERR_ACCESS_DENIED 失敗。

    在編譯期間,vDSO 程式碼區段的偏移值和大小會從 vDSO ELF 檔案擷取,並做為核心對應強制執行程式碼中的常數。

    在程序中建立有效的 vDSO 對應時,核心會記錄該程序的位址,以便快速進行檢查。

  2. 限制可用來進入核心的電腦位置。

    使用者執行緒進入系統呼叫的核心時,註冊會指出要叫用哪個低階系統呼叫。低階系統呼叫是核心與 vDSO 之間的不公開介面;許多系統呼叫會直接對應公開 ABI 中的系統呼叫,但有些則不會。

    每次呼叫低階系統呼叫時,vDSO 程式碼中都有一組固定的電腦位置,用於叫用該呼叫。vDSO 的原始碼會定義可識別每個這類位置的內部符號。在編譯期間,系統會從 vDSO 的符號表中擷取這些位置,並用來產生核心程式碼,為每個低階系統呼叫定義電腦有效性述詞。由於系統中所有使用者程序使用的 vDSO 程式碼只有一種定義,因此這些述詞只會檢查從 vDSO 程式碼片段開頭算起的已知、有效常數偏移。

    進入系統呼叫的核心時,核心會檢查 x86 上 syscall 指令的電腦位置 (或在其他機器上的對等指令)。此方法會將在 zx_vmar_map() 時間從電腦中記錄的 vDSO 程式碼基本位址減去,然後將產生的偏移值傳遞至叫用系統呼叫的有效性述詞。如果述詞規則 PC 無效,呼叫執行緒就無法繼續進行系統呼叫,而是採取類似機器例外狀況的綜合例外狀況,而這些例外狀況會叫用未定義或具有特殊權限的機器指令。

版本

TODO(mcgrathr):vDSO 變化版本是尚未實際使用的實驗功能。做法包括實作概念驗證和簡單的測試,但如要順利實作這個概念,並決定要提供哪些變化版本,則需要更多工作。概念是提供 vDSO 映像檔的變化版本,此變化版本只會匯出完整 vDSO 系統呼叫介面的一部分。舉例來說,僅供裝置驅動程式使用的系統呼叫可能不會顯示在一般應用程式程式碼所用的 vDSO 變化版本中。