Zircon 程式載入和動態連結

在 Zircon 中,核心不會直接參與一般程式載入,相反地,核心會提供建構使用者空間程式載入的構成元素,例如虛擬記憶體物件程序虛擬記憶體位址區域執行緒

ELF 和系統 ABI

標準 Zircon 使用者空間環境使用可執行和可連結格式 (ELF) 用於機器程式碼執行檔,並提供以 ELF 為基礎的動態連結器和 C/C++ 執行環境。Zircon 程序只能透過 Zircon vDSO 使用系統呼叫 (該 Zircon vDSO 是由核心以 ELF 格式提供),並採用 ELF 式系統常見的 C/C++ 呼叫慣例。

使用者空間程式碼 (具有適當的功能) 可以使用系統呼叫,在不使用 ELF 的情況下直接建立程序和載入程式,但 Zircon 用於機器碼的標準 ABI 會使用 ELF (本文所述)。

背景:傳統 ELF 程式載入

ELF 是在 Unix System V 版本 4 推出,成為大多數類 Unix 系統通用的標準可執行檔案格式。在這些系統中,核心會使用 POSIX execve API,整合載入程式與檔案系統存取權。這些系統載入 ELF 程式的方式有一些差異,但大部分遵循以下模式:

  1. 核心會依名稱載入檔案,並檢查檔案是 ELF 還是系統支援的其他檔案類型。這裡也會處理 #! 指令碼,如果存在,則支援非 ELF 格式。
  2. 核心會根據 PT_LOAD 程式標頭對應 ELF 圖片。針對 ET_EXEC 檔案,這會將程式的片段放置在 p_vaddr 所指定記憶體中的固定位址。如果是 ET_DYN 檔案,系統會選擇載入程式第一個 PT_LOAD 的基本位址,然後根據第一個片段的 p_vaddr 的相對位置放置下列片段。p_vaddr通常系統會隨機選擇基準位址 (ASLR)。
  3. 如果存在 PT_INTERP 程式標頭,其內容 (p_offsetp_filesz 提供的 ELF 檔案裡之位元組範圍) 會查詢為檔案名稱,以找出另一個名為 ELF 解譯器的 ELF 檔案。格式必須為 ET_DYN 檔案。核心的載入方式與載入執行檔相同,但一律會在自己選擇的位置載入。解譯器程式通常是名稱為 /lib/ld.so.1/lib/ld-linux.so.2 的 ELF 動態連結器,但核心會載入任何已命名的檔案。
  4. 核心會設定堆疊並註冊初始執行緒,並在所選的進入點位址中,啟動與電腦一起執行的執行緒。

    • 進入點是 ELF 檔案標頭中的 e_entry 值,且根據基本地址調整。如果有 PT_INTERP,進入點就是解譯器,而非主要執行檔。
    • 已有註冊和堆疊內容的組合層級通訊協定,可讓程式接收其引數和環境字串,以及實用值的輔助向量。存在 PT_INTERP 時,這些標記包括主要執行檔 ELF 標頭中的基本地址、進入點和程式標頭資料表位址。透過這項資訊,動態連結器可以在記憶體中找到主要執行的 ELF 動態連結中繼資料,並執行其工作。動態連結啟動完成後,動態連結器會跳到主要執行檔的進入點位址。

這個傳統是 Zircon 程式載入作業的靈感來源,但運作方式稍有不同。在載入動態連結器前,傳統模式載入執行檔的主要原因,是動態連結器隨機選擇的基本位址不得與 ET_EXEC 執行檔使用的固定位址交錯。Zircon 完全不支援固定位址程式載入 (ELF ET_EXEC 檔案),只支援位置獨立的執行檔或 PIE,也就是 ELF ET_DYN 檔案。

檔案系統不屬於 Zircon API 的較低層。程式載入是以 VMO 和透過管道使用的處理序間通訊 (IPC) 通訊協定為基礎。

程式載入要求開頭為:

  • 包含執行檔的 VMO 控制代碼 (需要 ZX_RIGHT_READZX_RIGHT_EXECUTE 權限)
  • 引數字串清單 (在 C/C++ 程式中變成 argv[])
  • 環境字串清單 (在 C/C++ 程式中成為 environ[])
  • 初始帳號代碼清單,每個帳號代碼都有處理資訊輸入

處理以下三種類型的檔案:

開頭為 #! 的指令碼檔案

檔案的第一行以 #! 開頭,長度不得超過 127 個字元。#! 後的第一個非空白字詞是指令碼解譯器名稱。如果之後發生任何情況,所有結果都會合併成為「指令碼解譯器引數」

  • 指令碼解譯器名稱會在原始引數清單前面加上 (轉換為 argv[0])。
  • 如果有指令碼解譯引數,則會在解譯器名稱和原始引數清單之間插入 (做為 argv[1],而原始的 argv[0] 會變成 argv[2])。
  • 程式載入器會透過載入器服務查詢指令碼解譯器名稱,以取得新的 VMO。
  • 程式載入作業會在該指令碼解譯器 VMO 與修改後的引數清單上重新啟動,但其他方面維持不變。原始執行檔的 VMO 控制代碼剛關閉;指令碼解譯器只會取得原始 argv[0] 字串可以使用,而非原始的 VMO。巢狀結構有上限 (目前為 5 個),限製程式載入失敗前可重新啟動多少次。

不含 PT_INTERP 的 ELF ET_DYN 檔案

  • 系統會為第一個 PT_LOAD 區隔選擇隨機的基本地址,然後根據該基本地址在每個 PT_LOAD 片段中對應。方法是建立涵蓋第一個區段第一頁到最後一個區段最後一頁的 VMAR
  • 系統會建立 VMO 並對應至另一個隨機位址,以存放初始執行緒的堆疊。如果 PT_GNU_STACK 程式標頭包含非零的 p_memsz,以決定堆疊的大小 (無條件捨去至整個頁面)。否則,系統會使用合理的預設堆疊大小。
  • vDSO 會對應至程序 (另一個包含 ELF 映像檔的 VMO),以及隨機的基本位址。
  • 系統會在使用 zx_thread_create() 的過程中建立新的執行緒。
  • 系統會建立新的管道,稱為「Bootstrap 管道」。程式載入器會以 processargs 通訊協定格式將訊息寫入這個管道。此主啟動訊息包含引數和環境字串,以及原始要求的初始控制代碼。這份清單會擴增為以下項目的帳號代碼:

    • 新的程序本身
    • 根層級 VMAR
    • 其初始執行緒
    • VMAR 涵蓋了執行檔的載入位置
    • 您為堆疊建立的 VMO
    • 您也可以選擇預設工作,讓新程序本身可以建立更多程序
    • 也可選擇 vDSO VMO,讓新程序 讓其建立的程序自行發出系統呼叫

    節目載入器接著會關閉管道。

  • 初始執行緒會透過 zx_process_start() 系統呼叫啟動:

    • entry 會從執行檔的 ELF 標頭中將新執行緒的 PC 設為 e_entry,並依基本位址調整。
    • stack 會將新執行緒的堆疊指標設為堆疊對應關係頂端。
    • arg1 會將控制代碼轉移至 Bootstrap 管道中,移至 C ABI 中註冊的第一個引數。
    • arg2 會將 vDSO 的基本位址傳遞至 C ABI 中的第二個引數。

    因此,可以編寫為 C 函式的程式進入點:

    noreturn void _start(zx_handle_t bootstrap_channel, uintptr_t vdso_base);
    

含有 PT_INTERP 的 ELF ET_DYN 檔案

在這種情況下,程式載入器不會在讀取 PT_INTERP 標頭後直接使用包含 ELF 執行檔的 VMO,而是使用 PT_INTERP 內容做為 ELF 解譯器的名稱。這個名稱用於向載入器服務提出的要求,以取得含有 ELF 解譯器的新 VMO,也就是另一個 ELF ET_DYN 檔案。接著載入 VMO,而非主要執行檔的 VMO。如上所述,啟動程序有以下差異:

  • processargs 通訊協定中的額外訊息會寫入「Bootstrap 管道」,位於主要 Bootstrap 訊息之前。ELF 解譯器應取用此載入器啟動訊息本身,以便其執行工作,但在管道中留下第二個 Bootstrap 訊息,然後將 Bootstrap 管道把手移交給主要程式的進入點。載入器系統啟動訊息只包含程式載入器所新增的必要控制代碼,不包含用於主要啟動程序訊息的完整組合,以及下列內容:

    • 主要 ELF 執行檔的原始 VMO 控制代碼
    • 載入器服務的管道控制代碼

      如此一來,ELF 解譯器從 VMO 自行載入執行檔,並使用載入器服務取得其他用於載入共用程式庫的 VMO。訊息中也包含引數和環境變數,可讓 ELF 解譯器在其記錄訊息中使用 argv[0],並檢查 LD_DEBUG 等環境變數。

  • 系統會忽略 PT_GNU_STACK 計畫標頭。程式載入器會選擇最小的堆疊大小,且堆疊大小必須夠大,包含載入器系統啟動訊息,加上 ELF 解譯器啟動程式碼的呼吸空間,以便當做呼叫頁框使用。這個「客廳」大小在來源中為 PTHREAD_STACK_MIN,且經過調整,使整個堆疊使用較小的 Bootstrap 訊息大小,只是單一網頁,但謹慎的動態連接器實作有足夠空間可以運作。動態連結器應讀取主要執行檔的 PT_GNU_STACK,並在跳至主要執行檔的進入點之前,切換至合理的大小堆疊進行正常使用。

processargs 通訊協定

<zircon/processargs.h> 定義程式載入器在啟動管道上傳送的開機程序訊息通訊協定。程序啟動時,具有該啟動管道的控制代碼,並且可透過 vDSO 存取系統呼叫。這個程序只有一個控制代碼,因此只能查看全域系統資訊和本身的記憶體,直到更多資訊透過 Bootstrap 管道處理為止。

processargs 通訊協定是單向通訊協定,適用於在 Bootstrap 管道上傳送的訊息。新程序不應回覆到管道,程式載入器通常會傳送訊息,然後在新程序開始前關閉管道端。這些訊息必須傳達新程序所需的一切,但以這種格式接收及解碼訊息的程式碼必須在受到嚴格限制的環境中執行。堆積分配不可能,在程式庫設施無法使用時也不容易。

如需郵件格式的完整詳細資料,請參閱標頭檔案。這個臨時通訊協定最終會替換為正式的 IDL 通訊協定,但格式會保持簡單,易於使用簡單的手寫程式碼進行解碼。

開機訊息會傳達以下資訊:

  • 初始帳號代碼清單
  • 與各個帳號代碼對應的 32 位元帳號代碼資訊項目
  • 帳號代碼資訊項目可參照的名稱字串清單
  • 引數字串清單 (在 C/C++ 程式中變成 argv[])
  • 環境字串清單 (在 C/C++ 程式中成為 environ[])

處理資訊項目

帳號代碼有許多用途,詳情請參閱帳號代碼資訊項目類型:

大部分的這些元素都是由程式載入器傳遞,因此不需瞭解它們的用途。

載入器服務

在動態連結系統中,可執行檔會在執行階段額外檔案參照並使用共用程式庫和外掛程式的其他檔案。動態連結器會以 ELF 解譯器的形式載入,且在控制主要程式的進入點之前,負責取得所有額外檔案的存取權,以完成動態連結。

Zircon 的所有標準使用者空間都會使用動態連結,直到 userboot 載入的第一個程序為止。裝置驅動程式和檔案系統會透過這種方式載入使用者空間程式。因此,由於傳統系統已經,無法以較高層的抽象層 (例如檔案系統範例) 定義程式載入。而是以 VMO 和簡單的管道型通訊協定為基礎載入程式。

這個「載入器服務」通訊協定是動態連結器取得 VMO 的方式,用來表示需要以共用程式庫形式載入的其他檔案。

這是在 <zircon/processargs.h> 中定義的簡易 RPC 通訊協定。程式碼在動態連結器啟動期間,傳送載入器服務要求及接收回覆的程式碼可能無法存取常用程式庫設備。

ELF 解譯器會在 processargs 系統啟動訊息中收到載入器服務的管道控制代碼,該訊息由處理資訊項目 PA_HND(PA_LDSVC_LOADER, 0) 識別。所有要求都是利用 zx_channel_call() 發出的同步 RPC。要求和回覆的開頭都是 zx_loader_svc_msg_t 標頭;有些要求包含其他資料;有些要求包含 VMO 控制代碼。要求運算碼為:

  • LOADER_SVC_OP_LOAD_SCRIPT_INTERPstring -> VMO 控制代碼

    程式載入器會從 #! 指令碼傳送指令碼解譯器名稱,並取回 VMO 以取代指令碼。

  • LOADER_SVC_OP_LOAD_OBJECTstring -> VMO 控制代碼

    動態連結器會傳送物件 (共用資料庫或外掛程式) 的名稱,然後取回包含該檔案的 VMO 控制代碼。

  • LOADER_SVC_OP_CONFIGstring -> reply ignored

    動態連結器會傳送一個字串,識別其載入設定。 藉此影響 LOADER_SVC_OP_LOAD_OBJECT 要求之後如何決定要為指定名稱提供的特定實作檔案。

  • LOADER_SVC_OP_DEBUG_PRINTstring -> reply ignored

    這是簡單的臨時記錄設施,可用於對動態連結器和早期程式啟動問題進行偵錯。因為早期啟動程式碼使用的是載入器服務,但還無法存取許多其他控點或複雜的功能,所以非常方便。日後會替換為一些易於使用且不透過載入器服務運作的記錄設備。

  • LOADER_SVC_OP_LOAD_DEBUG_CONFIGstring -> VMO 控制代碼

    這主要是為開發人員導向的功能,因此可能無法在正式版中執行。

    程式執行階段會傳送具備某種偵錯設定的字串名稱,並取回 VMO,以便讀取設定資料。掃毒執行階段會使用此功能,將大型選項文字儲存在檔案中,而不是直接在環境字串中傳遞。

  • LOADER_SVC_OP_PUBLISH_DATA_SINKstringVMO 控制代碼 -> reply ignored

    這主要是為開發人員導向的功能,因此可能無法在正式版中執行。

    程式執行階段會傳送名為「資料接收器」的字串,並將唯一控制代碼轉移至要在當中發布的 VMO。資料接收器字串可識別資料類型,VMO 的物件名稱則可特別識別這個 VMO 中的資料集。用戶端必須將唯一的控制代碼轉移至 VMO (這樣可以防止 VMO 在接收者不知情的情況下調整大小),但 VMO 可能還是會對應,並繼續將資料寫入 VM。程式碼檢測執行階段會使用這個程式碼傳送大量的二進位檔追蹤記錄結果。

Zircon 標準 ELF 動態連結器

上述的 ELF 慣例以及 processargs載入器服務通訊協定,是程式載入時的永久系統 ABI。程式可以使用任何符合基本 ELF 格式慣例的機器程式碼執行檔實作。實作項目可以使用 vDSO 系統呼叫

ABI、processargs 資料,以及遇到的載入器服務設施。程式透過這些通訊協定接收哪些處理程序和資料,取決於較高層級的程式環境。Zircon 的系統程序會使用 ELF 解譯器實作基本 ELF 動態連結,以及簡單的載入器服務實作。

Zircon 的標準 C 程式庫和動態連接器原本是由 musl 衍生的統合實作。可以透過 PT_INTERP 字串 ld.so.1 來識別。它會使用 DT_NEEDED 字串將共用程式庫命名為載入器服務的「物件」名稱。

簡易載入器服務會將要求對應至檔案系統存取:

  • 指令碼解譯器偵錯設定名稱必須以 / 開頭,做為絕對檔案名稱使用。
  • 資料接收器名稱會成為 /tmp 中的子目錄,而每個發布的 VMO 都會成為該子目錄中的檔案,其中含有 VMO 的物件名稱
  • object 名稱會在系統 lib/ 目錄中搜尋為檔案,
  • 「load configuration」字串做為子目錄名稱,可選擇後面加上 ! 字元。在系統 lib/ 目錄中搜尋名稱的子目錄,會先搜尋 lib/ 本身。如果有 ! 字尾,則「只會」搜尋這些子目錄。 舉例來說,掃毒執行階段會使用 asan,因為這項檢測作業與未檢測的程式庫程式碼相容,但 dfsan!,因為這項檢測作業需要程序中的所有程式碼都必須進行檢測。

使用 LLVM AddressSanitizer 檢測的標準執行階段版本會以 PT_INTERP 字串 asan/ld.so.1 識別。這個版本會在載入共用程式庫前傳送載入設定字串 asanSanitizerCoverage 會在啟用後發布至資料接收器名稱 sancov,並使用包含程序 KOID 的 VMO 名稱。