核心內部與作業系統建構

系統呼叫的實作

處於使用者模式的程式被刻意圍在硬體之外——它無法直接碰觸磁碟、網路或另一個行程的記憶體。那麼當你的程式碼呼叫像 read 或 write 這樣的東西時,這個請求究竟如何跨進有特權的核心、又跨回來?系統呼叫的實作(system-call implementation)就是安全完成這趟跨越的整套機制。把它想成在銀行櫃檯辦事,櫃檯隔著一道厚厚的防彈玻璃:你伸手碰不到金庫,於是把請求寫在單子上、從縫隙遞進去,行員(以完整權限執行)替你辦好,再把結果遞回來。那道縫隙、那張單子,以及使用它們的規則,就是系統呼叫機制。

具體來說,這趟旅程有清楚的步驟。(1) 你的程式碼呼叫 C 程式庫裡一層薄薄的使用者空間包裝函式(例如 glibc 的 read)。(2) 包裝函式按照 ABI 定義的嚴格呼叫慣例,把系統呼叫編號與引數放到核心所預期的位置(在 x86-64 Linux 上:呼叫編號放在 rax 暫存器,引數放在 rdi、rsi、rdx 等等)。(3) 它執行特殊的 syscall(或 trap)指令,把 CPU 從使用者模式切換到核心模式,並跳到一個固定的核心進入點。(4) 核心保存使用者暫存器,用呼叫編號去索引系統呼叫表、找到正確的處理常式,並驗證與複製跨界的引數(它從不信任使用者指標——會檢查位址確實位於使用者空間,並用 copy_from_user 之類的安全常式把資料複製進來)。(5) 處理常式完成工作,核心把回傳值放進約定好的暫存器、還原狀態,並返回到使用者模式中 trap 指令的下一行。

它之所以重要,是因為這是兩個世界之間唯一獲准的門,而它的安全建立在從不信任使用者輸入之上:對複製進來的指標少做一次邊界檢查,就可能讓程式讀取或破壞核心記憶體。一個常見的誤解是把系統呼叫當成普通的函式呼叫——它其實重得多,因為涉及模式切換、暫存器的保存與還原以及驗證;這個成本正是為什麼批次處理(一次讀 64 KB,而不是一次一位元組讀 65536 次)會快得多。

你呼叫 read(fd, buf, 100)。glibc 把 read 的編號放進 rax,把 fd、buf、100 放進 rdi、rsi、rdx,然後執行 syscall。CPU 在 syscall 進入點進入核心模式;核心在表中查到 sys_read 這個項目,檢查 buf 是個能容納 100 位元組的有效使用者位址,從檔案讀取,用 copy_to_user 把位元組複製進 buf,把 rax 設為實際讀到的位元組數,然後返回。對你的程式而言,它看起來就像一個回傳計數的普通函式。

包裝函式 -> 設定暫存器 -> 陷入核心 -> 驗證、分派、執行工作 -> 返回。

首要原則是:核心絕不可信任使用者程式碼遞進來的指標或長度——在解參考之前必須逐一驗證。許多嚴重的核心漏洞,恰恰就是在這個邊界上漏檢了一個使用者提供的位址或大小。

又稱
syscall mechanismhow a system call works系統呼叫機制