libc 作為系統呼叫的包裝層(libc as a wrapper around syscalls)
/ glibc: GLIB-see /
想像核心只說一種簡短又龜毛的機器方言:把這個確切的編號放這裡、這幾個值放那四個暫存器、然後發出這一條指令,再從一個暫存器把答案讀回來。幾乎沒有程式設計師願意為自己開的每個檔案親手寫這些。於是有一個住在使用者空間、兩種語言都會說的翻譯者——你用友善的 C 呼叫它,它替你應付那龜毛的機器方言。這個翻譯者就是 C 標準函式庫 libc。
具體來說,libc 是隨 C 一起出廠的標準函式庫——它提供像 printf()、malloc()、strlen()、fopen()、read() 這些熟悉的函式。其中有些是純使用者模式的程式碼,完全不碰核心(strlen() 只是走過記憶體裡的位元組)。另一些則是薄薄的包裝:libc 的 read() 函式做的事不過是把系統呼叫編號與引數載入對的暫存器、執行陷阱指令、再把核心的原始結果翻譯成你預期的 C 慣例(回傳位元組數,或在失敗時回傳負一並設定 errno 變數)。「薄包裝」道出了重點——包裝幾乎不加入自己的東西;它主要只是整理引數並轉送請求。像 printf() 這類更高層的函式則是較厚的包裝:它們在使用者空間把文字格式化好,之後才呼叫底下那個薄薄的 write() 包裝。
為何重要:這種分層正是為什麼 C 程式設計師幾乎從不親自發出原始系統呼叫。函式庫給你一個穩定、可移植、好用的介面,並把因平台而異的暫存器與編號細節藏在後面。它也意味著一次 printf() 可能變成零次或好幾次實際的系統呼叫,取決於緩衝;而同一份 C 程式可以在另一個作業系統上重新編譯,那裡的 libc 會把同樣的呼叫名稱對應到底層另一個系統呼叫。當你明白 C 標準函式庫的大部分,要嘛是純使用者模式的工作、要嘛是薄薄地包在系統呼叫之上,整個介面就不再是魔法了。
libc 的 read() 包裝本質上是:把 read 的系統呼叫編號與三個引數搬進暫存器、執行 syscall、接著若核心回傳負值,就把它取負後存進 errno 並回傳負一;否則回傳該值。這幾乎就是整個包裝的全部了。
薄包裝負責整理引數、陷入核心、再把結果調整成 C 的慣例。
libc 不只一種:glibc、musl 等等都以不同方式實作同一套標準介面。「libc」指的是這個角色(包裝系統呼叫的 C 標準函式庫),而非某個特定檔案——儘管 C 標準函式庫與系統呼叫的包裝有時是分開包裝的。