--- title: 淺談 Linux 核心:系統呼叫 (System Call) tags: - Linux - OS categories: - Linux description: 我們可以將 Linux kernel 當做程式運行於特權模式 (privileged mode) 的函式庫,如果要使用這個函式庫,必須使用硬體提供的特殊指令。 --- # 淺談 Linux 核心:系統呼叫 (System Call) 我們可以將 Linux kernel 當做程式運行於特權模式 (privileged mode) 的函式庫,如果要使用這個函式庫,必須使用硬體提供的特殊指令。以 x86 為例,呼叫普通函式庫使用 `call` 和 `ret` ,呼叫 Linux kernel 則需要用 `syscall` 和 `sysret`。 系統呼叫 (system call) 是 user space 和 kernel 進行交互的界面,讓使用者的程式可以請求 kernel 進行更高權限的操作,例如硬體相關的操作 (e.g. 讀寫檔案) 、行程 (process) 的建立與執行等等。 # 一切的一切就從 Hello World 開始 參考 [Linux 核心設計: 賦予應用程式生命的系統呼叫](https://hackmd.io/@sysprog/linux-syscall) 文章中的範例,可以透過簡單的程式來了解系統呼叫的運作過程。 給定 `hello.c`: ```c #include <stdio.h> int main(void) { printf("hello, world!\n"); return 0; } ``` 以 gcc 編譯 `hello.c` ,隨後用 [ltrace](https://man7.org/linux/man-pages/man1/ltrace.1.html) 追蹤: ```shell $ gcc -o hello hello.c $ ltrace ./hello puts("hello, world!"hello, world! ) = 14 +++ exited (status 0) +++ ``` 上方的輸出結果包含了幾個觀察: - 字串 `hello, world!` 作為 `puts` 函式的參數 - `puts` 的返回值為 `14` - 最後的 `status 0` 為 status code ,也就是 `main` 函式的 `return` 值 在 [你所不知道的 C 語言:編譯器和最佳化原理篇](https://hackmd.io/@sysprog/c-compiler-optimization) 中提到, gcc 會將 `printf("hello, world!\n");` 最佳化為 `put("hello, world!\n")` ,以降低解析 format string 和對應處理的成本。 > 我們可以新增一個 `hello1.c` 來觀察在不同最佳化條件下, gcc 對程式碼的最佳化情形: > ```c > #include <stdio.h> > int main(void) { > char *s = "hello, world!\n"; > printf("%s", s); > return 0; > } > ``` > > 利用不同最佳化 `-O0` 以及 `-O3` 進行編譯,並且透過 [ltrace](https://man7.org/linux/man-pages/man1/ltrace.1.html) 再次追蹤: > > ```shell > $ 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](https://man7.org/linux/man-pages/man1/strace.1.html) 來進行追蹤: ```shell $ strace ./hello write(1, "hello, world!\n", 14hello, world! ) = 14 exit_group(0) = ? +++ exited with 0 +++ ``` 從 strace 追蹤的結果可以發現,程式最後會透過系統呼叫的 `write` 來將字串輸出。 # 系統呼叫表 (System Call Table) Linux 核心為提供每個系統呼叫提供一個獨一無二的系統呼叫編號 (system call number)。以 x86_64 為例,Linux 核心在 `arch/x86/entry/syscalls/syscall_64.tbl` 提供了每個系統呼叫所對應的編號以及函式所對應的進入點 (entry point) 。 ``` # # 64-bit system call numbers and entry vectors # # The format is: # <number> <abi> <name> <entry point> # # The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls # # The abi is "common", "64" or "x32" for this file. # 0 common read sys_read 1 common write sys_write 2 common open sys_open 3 common close sys_close 4 common stat sys_newstat 5 common fstat sys_newfstat 6 common lstat sys_newlstat 7 common poll sys_poll 8 common lseek sys_lseek 9 common mmap sys_mmap 10 common mprotect sys_mprotect ``` 例如 `write` 的系統呼叫編號為 `1` ,因此在所有的 x86_64 架構系統中,這個系統呼叫編號是不能夠被更改的。 `write` 最終的實作方式在 `fs/read_write.c` 中: ```c ssize_t ksys_write(unsigned int fd, const char __user *buf, size_t count) { struct fd f = fdget_pos(fd); ssize_t ret = -EBADF; if (f.file) { loff_t pos, *ppos = file_ppos(f.file); if (ppos) { pos = *ppos; ppos = &pos; } ret = vfs_write(f.file, buf, count, ppos); if (ret >= 0 && ppos) f.file->f_pos = pos; fdput_pos(f); } return ret; } SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) { return ksys_write(fd, buf, count); } ``` `SYSCALL_DEFINE` 是一個巨集,定義在 `include/linux/syscalls.h` : ```c #define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__) #define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__) #define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__) #define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__) #define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__) #define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__) #define SYSCALL_DEFINE_MAXARGS 6 #define SYSCALL_DEFINEx(x, sname, ...) \ SYSCALL_METADATA(sname, x, __VA_ARGS__) \ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) ``` 該巨集最後會擴展成 `sys_write()` 函式: ```c asmlinkage long sys_write(unsigned int fd, const char __user *buf, size_t count); ``` # 透過 API 使用系統呼叫 一般而言,應用程式會透過呼叫使用者空間 (user space) 的 API (Application Programming Interface) ,而不是直接呼叫系統呼叫。在 Linux 系統中的 API 通常是以 C 標準函式庫所提供,例如 Linux 中的 libC 函式庫。 如果我們想要在 Linux 核心中使用系統呼叫,可以直接呼叫 `syscall()` 函式來使用指定的系統呼叫。 ```c #include <unistd.h> #include <sys/syscall.h> long syscall(long number, ...); ``` `syscall()` 函式可以直接呼叫一個系統呼叫,第一個參數是系統呼叫編號,例如 `write` 的號碼為 `1`,並且可根據系統呼叫的需求提供更多參數。以系統呼叫 `write` 為例,我們可以透過以下程式碼來直接使用: ```c #define NR_WRITE 1 syscall(NR_WRITE, fd, string, string_length) ``` - `NR_WRITE` 系統呼叫編號,`write` 為 `1`。 - `fd` file descriptor ,透過 `open()` 、 `read()` 、 `write()` 等函式進行各種 I/O 操作時,都是以 file descriptor 為對象。以系統呼叫 `write` 為例, `1` 表示 stdout , `2` 為 stderr - `string` 用於輸出的字串內容 - `string_length` 字串長度 參考下方 `write.c` ,當我們想要使用核心系統呼叫的 `write` 時,我們可以透過 C 標準函式庫所提供的 `syscall()` 或是 `write()` 來實現: ```c #include <unistd.h> #include <sys/syscall.h> int main(void){ syscall(1, 1, "hello, world!\n", 14); write(1, "Hello, World!\n", 14); } ``` 接著進行編譯並且透過 strace 追蹤: ```shell $ gcc -o write write.c $ strace ./write write(1, "hello, world!\n", 14hello, world! ) = 14 write(1, "Hello, World!\n", 14Hello, World! ) = 14 exit_group(0) = ? +++ exited with 0 +++ ``` # 透過組合語言使用系統呼叫 除了透過函式庫所提供的 API 之外,也可以透過組合語言使用系統呼叫。考慮以下程式碼 (以 x86-64 處理器為例): ```clike= #include <unistd.h> #include <stdio.h> #include <string.h> int main() { char *hello_str = "hello world\n"; long len = strlen(hello_str) + 1; long ret; __asm__ volatile ( "mov $1, %%rax\n" // system call number "mov $2, %%rdi\n" // unsigned int fd (stdout: 1, stderr: 2) "mov %1, %%rsi\n" // const char *buf "mov %2, %%rdx\n" // size_t count "syscall\n" "mov %%rax, %0" : "=m"(ret) : "g" (hello_str), "g" (len) : "rax", "rbx", "rcx", "rdx"); printf("return value: %ld\n", ret); } ``` 在第 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 行印出回傳值。 > 可以透過[系統呼叫表](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/)查尋使用系統呼叫時暫存器需要存放的變數 > ![image](https://hackmd.io/_uploads/S1UQQEuLkg.png) # 參考資料 * [Linux System Call Table for x86 64](https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) * [打造史上最小可執行 ELF 文件(45 字節,可打印字符串)](https://github.com/tinyclub/open-c-book/blob/master/zh/chapters/02-chapter8.markdown) * [Linux 核心設計: System call](https://hackmd.io/@RinHizakura/S1wfy6nQO) * [動態庫注入、ltrace、strace、Valgrind](https://jasonblog.github.io/note/linux_system/1526.html) * [Linux 核心設計: 賦予應用程式生命的系統呼叫](https://hackmd.io/@sysprog/linux-syscall)