---
# System prepended metadata

title: 淺談 Linux 核心：系統呼叫 (System Call)
tags: [Linux, OS]

---

---
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 和對應處理的成本。


> 我們可以新增一個 `hello１.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)