淺談 Linux 核心:系統呼叫 (System Call)
淺談 Linux 核心:系統呼叫 (System Call)
我們可以將 Linux kernel 當做程式運行於特權模式 (privileged mode) 的函式庫,如果要使用這個函式庫,必須使用硬體提供的特殊指令。以 x86 為例,呼叫普通函式庫使用 call
和 ret
,呼叫 Linux kernel 則需要用 syscall
和 sysret
。
系統呼叫 (system call) 是 userspace 和 kernel 進行交互的界面,讓使用者的程式可以請求 kernel 進行更高權限的操作,例如硬體相關的操作 (e.g. 讀寫檔案) 、行程 (process) 的建立與執行等等。
一切的一切就從 Hello World 開始
參考 Linux 核心設計: 賦予應用程式生命的系統呼叫 文章中的範例,可以透過簡單的程式來了解系統呼叫的運作過程。
給定 hello.c
:
|
以 gcc 編譯 hello.c
,隨後用 ltrace 追蹤:
gcc -o hello hello.c |
上方的輸出結果包含了幾個觀察:
- 字串
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 對程式碼的最佳化情形:
int main(void) {
char *s = "hello, world!\n";
printf("%s", s);
return 0;
}利用不同最佳化
-O0
以及-O3
進行編譯,並且透過 ltrace 再次追蹤:
gcc -o hello -O0 hello.c
ltrace ./hello
printf("%s", "hello, world!\n"hello, world!
) = 14
+++ exited (status 0) +++
gcc -o hello -O3 hello.c
ltrace ./hello
puts("hello, world!"hello, world!
) = 14
+++ exited (status 0) +++從上方結果可以發現,當我們在
printf()
的部份加入了格式化符號%s
,並且在編譯時關閉最佳化,就會避免printf("hello, world!\n");
被替換為put("hello, world!\n")
。
接著透過 strace 來進行追蹤:
strace ./hello |
從 strace 追蹤的結果可以發現,程式最後會透過系統呼叫的 write
來將字串輸出。
系統呼叫表 (System Call Table)
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
中:
ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count) |
SYSCALL_DEFINE
是一個巨集,定義在 include/linux/syscalls.h
:
該巨集最後會擴展成 sys_write()
函式:
asmlinkage long sys_write(unsigned int fd, const char __user *buf, |
透過 API 使用系統呼叫
一般而言,應用程式會透過呼叫使用者空間 (user space) 的 API (Application Programming Interface) ,而不是直接呼叫系統呼叫。在 Linux 系統中的 API 通常是以 C 標準函式庫所提供,例如 Linux 中的 libC 函式庫。
如果我們想要在 Linux 核心中使用系統呼叫,可以直接呼叫 syscall()
函式來使用指定的系統呼叫。
|
syscall()
函式可以直接呼叫一個系統呼叫,第一個參數是系統呼叫編號,例如 write
的號碼為 1
,並且可根據系統呼叫的需求提供更多參數。以系統呼叫 write
為例,我們可以透過以下程式碼來直接使用:
|
NR_WRITE
系統呼叫編號,write
為1
。fd
file descriptor ,透過open()
、read()
、write()
等函式進行各種 I/O 操作時,都是以 file descriptor 為對象。以系統呼叫write
為例,1
表示 stdout ,2
為 stderrstring
用於輸出的字串內容string_length
字串長度
參考下方 write.c
,當我們想要使用核心系統呼叫的 write
時,我們可以透過 C 標準函式庫所提供的 syscall()
或是 write()
來實現:
|
接著進行編譯並且透過 strace 追蹤:
gcc -o write write.c |
透過組合語言使用系統呼叫
除了透過函式庫所提供的 API 之外,也可以透過組合語言使用系統呼叫。考慮以下程式碼 (以 x86-64 處理器為例):
1 | #include <unistd.h> |
在第 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 行印出回傳值。
可以透過系統呼叫表查尋使用系統呼叫時暫存器需要存放的變數