Linux 系統呼叫

最後更新: 2024-07-15 08:53:17 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

Linux 系統呼叫在處理程序與核心之間形成架構特定的二進位介面。應用程式碼通常改以 C 函式庫 API 為目標,因為函式庫提供型別、功能測試巨集、相容性行為,以及機器特定的進入序列。

函式庫 API 與核心 ABI #

例如 open(path, flags) 這類原始碼層級的呼叫,並不承諾會執行名為 open 的核心系統呼叫。glibc 可以使用 openat 實作它、加入取消檢查,或選取與時間寬度相容的進入點。clock_gettime 等函式可以在 vDSO 中執行,而不必進入核心。反過來,Linux 也會公開 gettid 等系統呼叫,但較舊的 C 函式庫並未提供其包裝函式。

核心 ABI 包含:

  • 適用於處理程序架構與 ABI 的系統呼叫編號;
  • 以暫存器表示的引數,以及在部分 ABI 中部分位於堆疊上的引數;
  • 例如 syscall、int $0x80、svc 或 ecall 的進入指令;以及
  • 用於表示成功與錯誤的原始回傳慣例。

SYS_name 中的常數會從 <sys/syscall.h> 識別目標建置 ABI 所使用的編號。這些編號不會跨架構保持穩定:x86-64 的 write 是 1,而 i386 的 write 是 4。在同一個核心上,Compat 與 x32 處理程序還可能使用另一份表格。核心會透過該 ABI 的系統呼叫表分派此編號,交由通常以 SYSCALL_DEFINE* 巨集宣告的實作處理。

暫存器、結果與 errno #

兩種常見的 Linux x86 進入 ABI 展示了這種對應關係:

ABI進入方式編號引數 1–6結果
x86-64syscallraxrdi、rsi、rdx、r10、r8、r9rax
i386int $0x80eaxebx、ecx、edx、esi、edi、ebpeax

x86-64 指令會覆寫 rcx 與 r11,因此第四個引數使用 r10,而不是 C 函式 ABI 的 rcx。其他架構具有不同的對齊與暫存器配對規則;例如,某些 32 位元 ABI 會分割並對齊 64 位元引數。

在 x86 上,核心會在結果暫存器中,透過 -1 到 -4095 的負值回報錯誤。libc 包裝函式會將該原始結果轉換為 -1,並將正值錯誤編號儲存到執行緒區域的 errno。具有獨立錯誤指示器的架構則由其 libc 包裝函式正規化。因此,程式碼必須測試有文件說明的包裝函式回傳值,而不是在呼叫成功後讀取 errno。

直接使用 syscall #

glibc 為沒有一般包裝函式的進入點提供間接包裝函式:

c
#define _GNU_SOURCE
#include <sys/syscall.h>
#include <unistd.h>

long tid = syscall(SYS_gettid);
if (tid == -1) {
    /* errno describes the failure. */
}

原型是 long syscall(long number, ...);。此函式會執行架構的進入序列,並將核心錯誤轉換為 errno,但其可變引數不提供型別檢查。請將值轉型為確切的 ABI 寬度,並遵循分割 64 位元引數的架構特定規則。如果執行中的核心未實作其編號,新的呼叫也可能回傳 ENOSYS。

若存在 libc 包裝函式,應優先使用,因為直接使用可能繞過:

  • libc 實作的結構與時間寬度相容性;
  • 包裝函式定義的舊版系統呼叫回退機制;
  • POSIX 執行緒取消行為;以及
  • vDSO 加速。

原始內嵌組合語言還會引入暫存器限制、破壞項、記憶體排序與錯誤轉換等風險。只有在實作 C 函式庫或其他 ABI 層級執行階段時,使用它才有正當性。

架構與版本可攜性 #

除非更高層級的標準也定義了函式庫行為,否則核心系統呼叫就是 Linux 特有功能。編譯時 SYS_name 的可用性只能證明標頭檔描述了某個 ABI;不能證明執行中的核心實作了該呼叫。請透過呼叫它並處理 ENOSYS 來探測,然後在存在回退方案時使用語意正確的回退方案。

不要複製其他架構的編號,也不要將主機表格硬編碼到交叉編譯的程式中。透過邊界傳遞的結構也取決於所選 ABI 的欄位寬度、對齊方式、位元組順序與 UAPI 定義。基於相同原因,Seccomp 與 audit 會同時識別架構權杖與編號。

重新啟動、取消、追蹤與 Seccomp #

訊號可能會中斷阻塞中的呼叫。返回使用者空間期間,核心會使用內部的 -ERESTART* 結果來決定是否重新發出呼叫;使用者空間通常只會看到呼叫已完成,或看到 -1/EINTR。SA_RESTART、特定的系統呼叫,以及資料是否已經傳輸,會共同決定結果。因此,只有在操作的 API 允許安全重試時,應用程式才應重試 EINTR,而且必須先處理正值的部分結果,再進行重試。

POSIX 執行緒取消是 libc 的屬性,而不是核心系統呼叫的屬性。glibc 對取消點的包裝函式會在阻塞操作周圍協調取消;直接呼叫 syscall() 可能繞過這項協調。

strace 與 ptrace 會觀察核心進入與離開、引數以及原始結果。完全由 vDSO 處理的呼叫不會產生系統呼叫停止點。追蹤器也可能修改編號、引數或結果,因此經過追蹤的行為不是獨立的安全邊界。

Seccomp BPF 會在核心執行之前評估系統呼叫架構、編號與原始引數值。健全的允許清單會同時檢查架構權杖與編號,因為不同架構的編號空間可能重疊。篩選器無法解參照指標引數,而 libc 升級也可能選取不同於 API 名稱所暗示的底層系統呼叫。因此,Seccomp 能縮小核心攻擊面,但不構成完整的沙箱。

上游的 syscall(2) 頁面列出進入方式與暫存器慣例,而核心的 seccomp 文件 定義篩選語意。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)