---
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/)查尋使用系統呼叫時暫存器需要存放的變數
> 
# 參考資料
* [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)