我們可以將 Linux kernel 當做程式運行於特權模式 (privileged mode) 的函式庫,如果要使用這個函式庫,必須使用硬體提供的特殊指令。以 x86 為例,呼叫普通函式庫使用 call
和 ret
,呼叫 Linux kernel 則需要用 syscall
和 sysret
。
系統呼叫 (system call) 是 userspace 和 kernel 進行交互的界面,讓使用者的程式可以請求 kernel 進行更高權限的操作,例如硬體相關的操作 (e.g. 讀寫檔案) 、行程 (process) 的建立與執行等等。
參考 Linux 核心設計: 賦予應用程式生命的系統呼叫 文章中的範例,可以透過簡單的程式來了解系統呼叫的運作過程。
給定 hello.c
:
以 gcc 編譯 hello.c
,隨後用 ltrace 追蹤:
上方的輸出結果包含了幾個觀察:
hello, world!
作為 puts
函式的參數puts
的返回值為 14
status 0
為 status code ,也就是 main
函式的 return
值在 你所不知道的 C 語言:編譯器和最佳化原理篇 中提到, gcc 會將 printf("hello, world!\n");
最佳化為 put("hello, world!\n")
,以降低解析 format string 和對應處理的成本。
我們可以新增一個
hello1.c
來觀察在不同最佳化條件下, gcc 對程式碼的最佳化情形:利用不同最佳化
-O0
以及-O3
進行編譯,並且透過 ltrace 再次追蹤:從上方結果可以發現,當我們在
printf()
的部份加入了格式化符號%s
,並且在編譯時關閉最佳化,就會避免printf("hello, world!\n");
被替換為put("hello, world!\n")
。
接著透過 strace 來進行追蹤:
從 strace 追蹤的結果可以發現,程式最後會透過系統呼叫的 write
來將字串輸出。
Linux 核心為提供每個系統呼叫提供一個獨一無二的系統呼叫編號 (system call number)。以 x86_64 為例,Linux 核心在 arch/x86/entry/syscalls/syscall_64.tbl
提供了每個系統呼叫所對應的編號以及函式所對應的進入點 (entry point) 。
例如 write
的系統呼叫編號為 1
,因此在所有的 x86_64 架構系統中,這個系統呼叫編號是不能夠被更改的。 write
最終的實作方式在 fs/read_write.c
中:
SYSCALL_DEFINE
是一個巨集,定義在 include/linux/syscalls.h
:
該巨集最後會擴展成 sys_write()
函式:
一般而言,應用程式會透過呼叫使用者空間 (user space) 的 API (Application Programming Interface) ,而不是直接呼叫系統呼叫。在 Linux 系統中的 API 通常是以 C 標準函式庫所提供,例如 Linux 中的 libC 函式庫。
如果我們想要在 Linux 核心中使用系統呼叫,可以直接呼叫 syscall()
函式來使用指定的系統呼叫。
syscall()
函式可以直接呼叫一個系統呼叫,第一個參數是系統呼叫編號,例如 write
的號碼為 1
,並且可根據系統呼叫的需求提供更多參數。以系統呼叫 write
為例,我們可以透過以下程式碼來直接使用:
NR_WRITE
write
為 1
。fd
open()
、 read()
、 write()
等函式進行各種 I/O 操作時,都是以 file descriptor 為對象。以系統呼叫 write
為例, 1
表示 stdout , 2
為 stderrstring
string_length
參考下方 write.c
,當我們想要使用核心系統呼叫的 write
時,我們可以透過 C 標準函式庫所提供的 syscall()
或是 write()
來實現:
接著進行編譯並且透過 strace 追蹤:
除了透過函式庫所提供的 API 之外,也可以透過組合語言使用系統呼叫。考慮以下程式碼 (以 x86-64 處理器為例):
在第 10 行首先將系統呼叫編號存放至 rax
暫存器,第 11 行將 file descriptor 號碼寫入 rdi
暫存器。在第 12 行表示將第 16 ~ 18 行中的第 1 個變數 "g" (hello_str)
,也就是字串的 buffer 寫入至 rsi
暫存器,最後在第 13 行則是將將第 16 ~ 18 行中的第 2 個變數 "g" (len)
,即字串的長度寫入 rdx
暫存器。在第 18 行中,系統呼叫的回傳值會被存放在 rax
暫存器,因此我們將暫存器的值存放至變數 ret
並且在第 19 行印出回傳值。
可以透過系統呼叫表查尋使用系統呼叫時暫存器需要存放的變數