---
# System prepended metadata

title: 第五講：賦予應用程式生命的系統呼叫
tags: [Linux]

---

# 第五講：賦予應用程式生命的系統呼叫


> 本筆記僅為個人紀錄，相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有
* 直播：==[Linux 核心設計：賦予應用程式生命的系統呼叫 (2018-12-07)](https://www.youtube.com/watch?v=rPWt6KgL8uQ)==
* 詳細共筆：[Linux 核心設計: 賦予應用程式生命的系統呼叫](https://hackmd.io/@sysprog/linux-syscall)
* 主要參考資料：
    * [Analyzing a Decade of Linux System Calls](http://research.cs.queensu.ca/~cordy/Papers/BKBHDC_ESE_Linux.pdf)
    * [System Calls Make the World Go Round](https://manybutfinite.com/post/system-calls/)
    * [Computer Science from the Bottom Up: System Calls](https://www.bottomupcs.com/system_calls.xhtml)
    * [Anatomy of a system call, part 1 (LWN.net)](https://lwn.net/Articles/604287/)
    * [Anatomy of a system call, part 2 (LWN.net)](https://lwn.net/Articles/604515/)
    * [什麼是 Linux vDSO 與 vsyscall？——發展過程](https://alittleresearcher.blogspot.com/2017/04/linux-vdso-and-vsyscall-history.html)

---

本筆記旨在闡述 Linux 核心中系統呼叫 (System Call) 的概念、機制、演進及其重要性。系統呼叫是應用程式與作業系統核心溝通的橋樑，賦予應用程式存取硬體資源和核心服務的能力。

---
## Linux 系統呼叫的發展

Linux 的「九二共識」，並非政治術語，而是指 1992 年 Linux 核心開始以 GNU GPLv2 (GNU General Public License version 2，GNU 通用公共授權條款第二版) 釋出原始程式碼這一里程碑事件。儘管 Linux 核心的原始程式碼在 1991 年已公開，但 GPLv2 的採用奠定了其自由開放的發展模式，吸引了全球大量開發者的投入，共同塑造了 Linux 的輝煌。

### 作業系統的使命
Linus Torvalds 在 2001 年的紀錄片《[Revolution OS](https://hackmd.io/s/SyuRJIPI-)》中精闢地指出：
> 「作業系統就是你永遠不會看到的東西，因為沒有人直接使用作業系統，人們使用的是程式。在他們的電腦上，**作業系統唯一的使命就是，幫助其它程式執行**，所以作業系統從未獨立運行，而僅是默默等待程式，來向它要求現有資源、某個存在硬碟上的檔案或要求其它程式將這個程式連接到外面去，然後作業系統再一步步地，試著讓人們寫程式容易一些。」

這段話深刻揭示了作業系統的本質：服務於應用程式。而實現這一服務的關鍵機制正是系統呼叫。應用程式透過系統呼叫請求作業系統提供檔案操作、網路通訊、記憶體管理等服務。

Linus Torvalds 在同年 10 月進一步闡述了他對 Linux 發展方向的看法，可視為對此「九二共識」的技術性重申：
> "From a technical standpoint, I believe the kernel will be "more of the same" and that **all the really interesting staff will be going out in user space**."

這句話強調，Linux 核心應保持其核心功能的穩定與精簡，而真正創新和有趣的發展將更多地出現在使用者空間 (User Space)。這意味著不應將過多的應用層級功能塞入核心，避免核心過於臃腫和複雜。

一個反面教材是早期曾有人嘗試將 Web 伺服器 (如 Tux 專案) 整合進 Linux 核心以期提升效能，但這種做法違背了核心設計原則，增加了核心的複雜性和潛在風險，最終未能普及。Windows NT 也曾為了圖形效能將顯示引擎移入核心，但也因此引入了安全漏洞。

### Linux 核心的系統呼叫
Linux 核心透過提供一組定義良好的系統呼叫介面 (約數百個，隨版本變動)，來支持 User Space 應用程式的運行。核心會儘可能將功能實現在 User Space，例如資料庫、繪圖引擎、視窗系統等。

然而，有時為了特定的效能優化，核心也會引入特殊的系統呼叫，例如 `sendfile` 系統呼叫，它允許在兩個檔案描述符之間直接傳輸資料 (例如從磁碟檔案到網路 socket)，避免了資料在 Kernel Space 和 User Space 之間的多次複製，從而顯著提升了網路伺服器傳輸靜態檔案的效率。
![](https://i.imgur.com/eoX2FDC.png)
上圖展示了應用程式 (Ring 3，使用者模式) 如何透過系統呼叫請求核心 (Ring 0，核心模式) 提供的服務，如檔案操作、網路通訊、硬體存取等。

> 延伸閱讀：[Analyzing a Decade of Linux System Calls](http://research.cs.queensu.ca/~cordy/Papers/BKBHDC_ESE_Linux.pdf)，該論文分析了從 2005 年 4 月到 2014 年 12 月的系統呼叫變化，Linux 系統呼叫的數量和功能在過去十年間持續演進、增減，以適應不斷變化的硬體和應用需求，但其核心介面保持了相對穩定。
> ![image](https://hackmd.io/_uploads/HJkERcNzxl.png)

---
## 從縮減 Hello World 程式談起

理解系統呼叫的一個實用方法是觀察一個簡單程式 (如 "Hello World") 在編譯和執行過程中的行為，並逐步移除對標準函式庫的依賴，直接使用系統呼叫。

### 1. 用 `ltrace` 觀察

給定一個標準的 `hello.c` 程式：
```c=
#include <stdio.h>
int main(void) {
    printf("hello, world!\n");
    return 0;
}
```

使用 `ltrace` 工具 (Library trace) 追蹤其執行時的函式庫呼叫，可以看到：
```shell
$ ltrace ./hello
__libc_start_main(0x55931d22e149, 1, 0x7ffd4f6d8fb8, ...(略)
puts("hello, world!"hello, world!
)                                                                            = 14
+++ exited (status 0) +++
```
觀察發現：
1.  `__libc_start_main` 是程式真正的進入點，它由 C 執行階段程式庫 (CRT, C Runtime Library) 提供，負責進行一些初始化工作，並呼叫 `main` 函式。
    > 延伸閱讀：[C 語言的執行階段程式庫 (CRT)](https://hackmd.io/@Jaychao2099/crt)
4.  儘管原始碼中使用的是 `printf`，但在編譯最佳化後，實際執行的是 `puts` 函式。這是因為對於不包含格式化字串的簡單字串輸出，`puts` 比 `printf` 更有效率，可以降低解析格式字串的成本。
5.  `puts` 函式的返回值 `14` 表示成功輸出的字串長度。
6.  `status 0` 是 `main` 函式的返回值，表示程式正常退出。

### 2. 用組合語言觀察

為了進一步理解底層機制，可以將 C 程式碼編譯為組合語言。GCC 編譯器可能會將 `printf()` 最佳化為 `puts()`。我們可以手動編寫更精簡的組合語言，直接使用系統呼叫來輸出字串並退出程式，從而避免連結 C 標準函式庫中的 `printf()` 或 `puts()`。

以下是一個直接使用 Linux 系統呼叫的 x86 (32位元) 組合語言範例 `hello.s`：
```asm=
.LC0:
	.string "Hello world!\xa\x0"  # \xa 是換行符 \n，\x0 是 null
	.text
.global _start
_start:
	# sys_write(fd, addr, len)
	movb   $4, %al          # eax = 4 (sys_write 的系統呼叫號碼)
	xorl   %ebx, %ebx
	incl   %ebx             # ebx = 1 (檔案描述符，標準輸出 stdout)
	movl   $.LC0, %ecx      # ecx = 字串 "Hello world!\n" 的位址
	xorl   %edx, %edx
	movb   $13, %dl         # edx = 13 (字串長度，不含 null terminator)
	int    $0x80            # 執行系統呼叫

	# sys_exit(status)
	xorl   %eax, %eax
	incl   %eax             # eax = 1 (sys_exit 的系統呼叫號碼)
	xorl   %ebx, %ebx       # ebx = 0 (退出狀態碼)
	int    $0x80            # 執行系統呼叫
```
組譯和連結指令：
```shell
$ as --32 -o hello.o hello.s
$ ld -melf_i386 -o hello hello.o
```
對於 x86_64 (64位元) 架構，系統呼叫的方式有所不同，通常使用 `syscall` 指令，且參數傳遞透過不同的暫存器：
```asm=
.data
msg:
    .ascii "Hello, world!\n"
    len = . - msg  # 計算字串長度
.text
    .global _start

_start:
    # sys_write(fd, buf, count)
    movq  $1, %rax      # rax = 1 (sys_write 的系統呼叫號碼)
    movq  $1, %rdi      # rdi = 1 (檔案描述符 stdout)
    movq  $msg, %rsi    # rsi = 字串位址
    movq  $len, %rdx    # rdx = 字串長度
    syscall             # 執行系統呼叫

    # sys_exit(error_code)
    movq  $60, %rax     # rax = 60 (sys_exit 的系統呼叫號碼)
    xorq  %rdi, %rdi    # rdi = 0 (退出狀態碼)
    syscall             # 執行系統呼叫
```
組譯和連結指令：
```shell
$ gcc -c hello.s         # 或者用 as
$ ld -o hello hello.o
```
無論是 x86 的 `int $0x80` 還是 x86_64 的 `syscall` 指令，它們都是觸發系統呼叫的入口 (軟體中斷)，常被稱為 **Call Gate**。這並非指 x86 特有的 `CALL FAR` 指令的 Call Gate，而是廣義上指代一種機制，**允許使用者模式的程式碼轉換到核心模式以執行特權指令**。Intel 後期也引入了 `SYSENTER` 和 `SYSEXIT` 等指令作為快速系統呼叫機制，以降低傳統中斷方式的開銷。

> 延伸閱讀：[System Calls Make the World Go Round](https://manybutfinite.com/post/system-calls/)
> * 系統呼叫作為應用程式和核心之間的橋樑：
> ![](https://i.imgur.com/eI0WONT.png)
> * 系統呼叫時的 instruction pointer 變化：
> ![未命名](https://hackmd.io/_uploads/SJANw2Nzlx.jpg)


#### ABI 比對
下表總結了不同硬體架構和 ABI (Application Binary Interface，應用程式二進位介面) 下，系統呼叫的指令、系統呼叫號碼傳遞方式及返回值所在暫存器 (參考 [`syscall(2)` man page](https://man7.org/linux/man-pages/man2/syscall.2.html))：

| Arch/ABI   | Instruction      | Syscall # Reg | Return Val Reg |
| -----------|-----------------:| -------------:| --------------:|
| arm/OABI   | `swi NR`         | (NR in opcode)| r0             |
| arm/EABI   | `swi 0x0`        | r7            | r0             |
| arm64      | `svc #0`         | x8            | x0             |
| i386       | `int $0x80`      | eax           | eax            |
| x86-64     | `syscall`        | rax           | rax            |
| x32        | `syscall`        | rax           | rax            |

> * [x32 ABI](https://en.wikipedia.org/wiki/X32_ABI) 是一種特殊的 ABI，它允許程式在 x86-64 環境下運行，利用 64 位元指令集的優勢 (如更多暫存器)，同時使用 32 位元指標，以減少 64 位元指標帶來的記憶體開銷。
> * [`_syscall(2)`](http://man7.org/linux/man-pages/man2/_syscall.2.html) 巨集已被棄用。

### 3. 用 syscall 觀察

我們也可以使用 C 語言的 `syscall(2)` 函式直接發起系統呼叫：
```c=
#define _GNU_SOURCE
#include <unistd.h>
#include <sys/syscall.h> // 包含 __NR_write 等巨集
                         // __NR_write 是 sys_write 的系統呼叫號碼
int main() {
    syscall(__NR_write, 1, "Hello, world!\n", 14);    // 1 = stdout
    syscall(__NR_exit, 0);
    return 0;    // 這行在呼叫 __NR_exit 後實際不會執行到
}
```
透過 `strace` 工具追蹤此程式的系統呼叫：
```shell
$ strace ./hello_syscall

...(略)

write(1, "Hello, world!\n", 14hello, world!
)         = 14
exit(0)                                 = ?
+++ exited with 0 +++
```
可以看到，程式直接呼叫了 `write` 系統呼叫。

> 建議閱讀：[Computer Science from the Bottom Up: System Calls](https://www.bottomupcs.com/system_calls.xhtml)

---
## 透過 kprobes + eBPF 來追蹤系統呼叫

傳統的系統呼叫追蹤工具如 **`strace` 會帶來較大的效能開銷**，且其 **基於 `ptrace` 的機制可能被某些程式繞過**。現代 Linux 核心提供了更強大且低開銷的追蹤機制，如 Kprobes (Kernel Probes) 和 eBPF (extended Berkeley Packet Filter)。

[Kprobes](https://www.kernel.org/doc/Documentation/kprobes.txt) 允許在核心函式的入口或特定指令處插入探測點，執行自訂的處理函式。Kprobes 僅能讀取系統呼叫的參數和返回值，無法變更暫存器內容。

eBPF 是一個 **核心內的虛擬機器**，可以執行 **經過驗證的 BPF 位元組碼**。透過將 eBPF 程式掛載到 Kprobes 等探測點，可以實現高度靈活和高效能的核心行為觀測與分析。
> 延伸閱讀：[第四講：透過 eBPF 觀察作業系統行為](https://hackmd.io/@Jaychao2099/Linux-kernel-4)。

### 用 eBPF 觀察
以下是一個使用 `bcc` (BPF Compiler Collection) 工具集中的 Python 函式庫來結合 Kprobes 和 eBPF 追蹤核心函式 `inet_listen` (與網路監聽相關) 的範例。

準備 `call.py`：
```python=
from bcc import BPF

# eBPF C 程式碼
bpf_text = """
#include <net/inet_sock.h> // 用於 inet_sk 巨集和 struct inet_sock
#include <bcc/proto.h>     // 用於 bpf_trace_printk 等

int kprobe__inet_listen(struct pt_regs *ctx, struct socket *sock, int backlog) {
    bpf_trace_printk("Hello World! from inet_listen\\n");
    return 0;
};
"""

# 載入 eBPF 程式
b = BPF(text=bpf_text)

# 持續讀取並印出追蹤訊息
print("Tracing inet_listen... Ctrl-C to exit.")
while True:
    try:
        print(b.trace_readline().decode('utf-8', 'replace'))
    except KeyboardInterrupt:
        exit()
```

執行步驟：
1.  在一個終端機中以 root 權限執行程式：
    ```shell
    $ sudo python call.py
    ```
2.  等待約五秒，在另一個終端機中執行Netcat 工具：
    ```shell
    $ nc -l -p 4242    # -l 表示監聽，-p 4242 指定監聽 4242 埠
    ```

預期在第一個終端機中會看到類似以下的輸出，表明 `inet_listen` 被呼叫：
```shell
nc-752     [006] ....1   367.091793: bpf_trace_printk: Hello World! from inet_listen
```
> [!Warning] 輸出格式可能因 bcc 版本和核心配置而略有不同。

### 印出更多資訊
可以修改 eBPF 程式以獲取更多有用的資訊，例如 `backlog` 參數：
將 `bpf_trace_printk("Hello World! from inet_listen\\n");` 修改為：
```c=9
bpf_trace_printk("Listening with %d pending connections!\\n", backlog);
```
重新執行實驗，預期可以看到 `backlog` 的值 (通常是 Netcat 預設的佇列大小)。

進一步，可以讀取 `sock` 參數指向的 `struct socket` 結構，並透過 `inet_sk` 巨集轉換為 `struct inet_sock` 來獲取監聽的 IP 位址和埠號：
```c=9
    struct inet_sock *inet = inet_sk(sock->sk);
    u32 laddr = 0;
    u16 lport = 0;
    
    // 安全地從核心記憶體讀取資料
    bpf_probe_read_kernel(&laddr, sizeof(laddr), &(inet->inet_rcv_saddr));
    bpf_probe_read_kernel(&lport, sizeof(lport), &(inet->inet_sport));
    
    // 將網路位元組序轉換為主機位元組序 (Big or Little Endian)
    bpf_trace_printk("Listening on %x %d\\n", ntohl(laddr), ntohs(lport));
```
> [!Warning]注意：
> 舊版 bcc 可能使用 `bpf_probe_read`，新版推薦使用 `bpf_probe_read_kernel` 或 `bpf_probe_read_user` 以明確區分讀取來源。

重新執行實驗，當執行 `nc -l -p 4242` 時，預期看到類似 "Listening on 0 4242" 的輸出 (0 通常表示監聽所有本地 IP 位址)。

> 延伸閱讀：
> *   [How to turn any syscall into an event: Introducing eBPF Kernel probes](https://blog.yadutaf.fr/2016/03/30/turn-any-syscall-into-event-introducing-ebpf-kernel-probes/)

---
## Linux 核心對系統呼叫的實作機制

Linux 核心內部透過一個稱為「系統呼叫表」 (System Call Table) 的結構來管理和分派系統呼叫。當使用者模式的程式觸發一個系統呼叫 (例如透過 `int $0x80` 或 `syscall` 指令) 後，CPU 會切換到核心模式，並執行一個預先定義好的入口函式。該入口函式會根據傳入的系統呼叫號碼 (System Call Number，通常存放在特定暫存器如 `eax` 或 `rax`) 在系統呼叫表中查詢對應的核心函式指標，然後跳轉執行該核心函式。

以 x86_64 架構為例，相關的實作可以在 4.16 版核心原始程式碼的 [arch/x86/entry/syscall_64.c](https://github.com/torvalds/linux/blob/f8781c4a226319fe60e652118b90cf094ccfe747/arch/x86/entry/syscall_64.c) 中找到。系統呼叫表 `sys_call_table` 是一個函式指標陣列：
```c=10
/* sys_ni_syscall 是一個預設的處理函式，用於處理未實現或無效的系統呼叫號碼 */
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include <asm/syscalls_64.h>    // 通常由工具生成，包含了所有系統呼叫的宣告
#undef __SYSCALL_64

#define __SYSCALL_64(nr, sym, qual) [nr] = sym, // 巨集定義，用於初始化陣列

// asmlinkage 是一個編譯器指令，告訴編譯器參數從 stack 傳遞
// sys_call_ptr_t 是 typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
	/**
	 * 使用 C99 的 designated initializer 特性，
	 * 將所有表項預設指向 sys_ni_syscall。
	 * 如果某個系統呼叫號碼沒有對應的實作，被呼叫時就會執行 sys_ni_syscall，
	 * 通常返回 -EINVAL。
	 */
	[0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>    // 再次包含，這次使用上面定義的巨集來填充表項
};
```
`__NR_syscall_max` 定義了系統呼叫號碼的最大值。`pt_regs` 結構包含了系統呼叫發生時 CPU 暫存器的狀態。

當一個系統呼叫被觸發，核心會：
1.  保存 User Space 的上下文 (暫存器等)。
2.  從特定暫存器 (如 `rax`) 獲取系統呼叫號碼。
3.  使用該號碼作為索引，在 `sys_call_table` 中找到對應的服務常式 (例如 `sys_read`, `sys_write`)。
4.  從其他暫存器 (如 `rdi`, `rsi`, `rdx` 等) 獲取系統呼叫的參數。
5.  執行該服務常式。
6.  將服務常式的返回值存入特定暫存器 (如 `rax`)。
7.  恢復 User Space 的上下文，並返回到使用者模式繼續執行。

> [!Tip] **Linux System Call 的三種路徑**
> ![image](https://hackmd.io/_uploads/rkCIAUBMxg.png)
> 在 x86 架構的 Linux 系統中，Userspace 請求核心服務有三種不同途徑。重點在於 **歷史演進** 與 **效能優化**。
> #### **1. 傳統 32-bit 路徑：軟體中斷 (The Legacy Way)**
> *   **指令：** `INT 0x80`
> *   **位置：** 圖中最左側（黃色區塊）。
> *   **機制：** 這是最古老的實作方式。程式將系統呼叫代號（如 `read` 為 3）放入 `EAX`，然後觸發 128 號中斷。
> *   **關鍵特徵：**
>     *   **經過 IDT：** CPU 必須查詢 **中斷描述符表 (IDT)** 中的 Trap Gate 來找到核心入口點。
>     *   **效能：** 由於涉及完整的中斷處理流程，權限切換與查表的開銷較大，速度最慢。
> #### **2. 快速 32-bit 路徑：快速呼叫 (The Fast 32-bit Way)**
> *   **指令：** `SYSENTER` (Intel) / `SYSCALL` (AMD)
> *   **位置：** 圖中中間路徑（從黃色區塊跨到橘色核心）。
> *   **機制：** 為了解決 `INT 0x80` 效能低落而引入的機制。主要用於 64 位元核心上執行 32 位元程式（Compatibility Mode）。
> *   **關鍵特徵：**
>     *   **繞過 IDT：** **完全不查詢 IDT**。
>     *   **使用 MSR：** CPU 直接讀取預先設定好的 **MSR (Model Specific Register)** 暫存器內容，直接跳轉到核心函數 (`ia32_sysenter_target`)，大幅提升速度。
> #### **3. 原生 64-bit 路徑：現代標準 (The Native 64-bit Way)**
> *   **指令：** `SYSCALL`
> *   **位置：** 圖中最右側（橘色區塊）。
> *   **機制：** 現代 64 位元 Linux 的標準呼叫方式。注意系統呼叫代號不同（在 64 位元下，`read` 的代號是 0，而非 3），且使用 `RAX` 暫存器。
> *   **關鍵特徵：**
>     *   **繞過 IDT：** 同樣不走 IDT。
>     *   **使用 MSR_LSTAR：** 利用 `LSTAR` (Long System Target Address Register) 暫存器直接跳轉至核心入口 (`system_call` in `entry_64.S`)。這是目前效能最好的方式。
> ### **總結核心觀念**
> 1.  **殊途同歸：** 無論是透過慢速的中斷（Trap Gate）還是快速的暫存器跳轉（MSR），這三條路徑最終都會彙整到同一個 C 語言函數 —— **`sys_read()`**（位於 `fs/read_write.c`）。這體現了核心如何將底層硬體差異抽象化。
> 2.  **IDT 的角色：** 只有最左邊的古老路徑使用 IDT；現代的快速路徑（中間與右邊）都透過硬體暫存器（MSR）直接進入核心，以減少 CPU Cycle 的消耗。


> 搭配閱讀：
> *   [How does the Linux kernel handle a system call (Linux Insides)](https://0xax.gitbooks.io/linux-insides/SysCall/linux-syscall-2.html)
> *   [Anatomy of a system call, part 1 (LWN.net)](https://lwn.net/Articles/604287/)

---
## vsyscall 和 vDSO

在某些情況下，即使系統呼叫的邏輯非常簡單 (例如讀取當前時間)，傳統的模式切換所帶來的開銷也可能相對較大。

為了優化這類系統呼叫的效能，Linux 引入了 vsyscall (Virtual System Call) 和 vDSO (Virtual Dynamic Shared Object) 機制。

當使用 `ldd` (List Dynamic Dependencies) 命令查看一個程式 (如 `/usr/bin/git`) 的動態連結依賴時，可能會看到一個名為 `linux-vdso.so.1` 的特殊共享物件：
```shell
$ ldd /usr/bin/git
        linux-vdso.so.1 (0x00007ffc2c2db000)
        libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007ff1d6bc5000)
        libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007ff1d6ba9000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff1d6997000)
        /lib64/ld-linux-x86-64.so.2 (0x00007ff1d706f000)
```
有趣的是，在檔案系統中通常找不到 `linux-vdso.so.1` 這個檔案。這是因為 vDSO 並不是一個普通的磁碟檔案，而是由核心在 **程式啟動時動態創建** 並 **映射到該程式的位址空間中** 的一段記憶體。

### vsyscall (Virtual System Call, 虛擬系統呼叫)：
>[!Warning] 已趨向廢棄，尤其在 x86-64 上由於安全原因和 ASLR 的衝突

vsyscall 最初是為 x86-64 設計的，核心將一小段實現了某些簡單系統呼叫 (如 `gettimeofday`, `time`, `getcpu`) 的程式碼 **放置在一個固定的、所有行程都可見的** 記憶體頁面。應用程式可以直接呼叫這段程式碼，而 **無需陷入核心**，從而避免了模式切換的開銷。

然而，vsyscall 頁面的固定位址與 ASLR (Address Space Layout Randomization，位址空間佈局隨機化，一種安全機制) 存在衝突，因為攻擊者可以利用這個固定位址。

### vDSO (Virtual Dynamic Shared Object, 虛擬動態共享物件)：
>[!Note] vDSO 是對 vsyscall 的改進和替代

它同樣由核心提供，將一些系統呼叫的實作 (通常是時間相關的，如 `gettimeofday`, `clock_gettime`) 作為一個迷你的共享函式庫 **映射到每個 user process 的位址空間**。與 vsyscall 不同的是：
1.  **位址隨機化**：vDSO 的映射位址是隨機的，與 ASLR 相容。
2.  **ELF 格式**：vDSO 是一個完整的 ELF 共享物件，這使得 debugger (如 GDB) 可以更好地處理它，並且標準的動態連結器可以解析其符號。
3.  **靈活性**：vDSO 可以包含更複雜的邏輯，並且更容易擴展。
![](https://i.imgur.com/1rtBs8n.png)
![image](https://hackmd.io/_uploads/ryxJTEUIWl.png)

當應用程式呼叫一個在 vDSO 中有實作的函式時 (例如 C 函式庫中的 `gettimeofday`)，C 函式庫會檢查是否存在 vDSO 並優先使用 vDSO 中的版本。這樣，呼叫就可以在 User Space 內完成，極大地提升了效能。例如，頻繁呼叫 `gettimeofday` 的應用程式可以從 vDSO 中獲益良多。
![](https://i.imgur.com/9UbtUo0.png)

> 搭配閱讀：
> *   [vDSO: 快速的 Linux 系統呼叫機制](https://hackmd.io/@sysprog/linux-vdso)
> *   [Anatomy of a system call, part 2 (LWN.net)](https://lwn.net/Articles/604515/)

> 參考資料：
> *   [什麼是 Linux vDSO 與 vsyscall？——發展過程](https://alittleresearcher.blogspot.com/2017/04/linux-vdso-and-vsyscall-history.html)
> *   [gettimeofday、clock_gettime 以及不同時鐘源的影響](https://www.cnblogs.com/raymondshiquan/articles/gettimeofday_vs_clock_gettime.html)
> *   ARM 架構上的 vDSO 考量，可參考簡報 [The vDSO on arm64](https://blog.linuxplumbersconf.org/2016/ocw/system/presentations/3711/original/LPC_vDSO.pdf)。該簡報指出 vDSO 是一個由核心提供的完整共享函式庫，映射到所有使用者行程，主要用於在 User Space 提供「虛擬系統呼叫」以提升速度，特別是**針對那些本身處理很快但核心進出 (kernel enter/exit) 開銷顯著的系統呼叫**。vDSO 相較於舊的 vsyscall page 機制更靈活，更易於除錯，且由於利用 ASLR 而更難被利用。在 arm64 上，vDSO 包含 `vdso_data` (核心與 User Space 共享的資料頁，vvar) 和包含函式實作的程式碼頁。

---
## 總結

系統呼叫是作業系統提供服務的基石，而 Linux 核心在系統呼叫的設計、實作和優化方面，展現了其不斷演進和追求效能與安全的努力。從傳統的中斷機制到現代的 `syscall` 指令，再到 Kprobes、eBPF 等高級追蹤技術，以及 vsyscall 和 vDSO 這類效能優化手段，都體現了 Linux 核心設計的精妙與複雜性。

---

回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)