# 第五講:賦予應用程式生命的系統呼叫
> 本筆記僅為個人紀錄,相關教材之 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 之間的多次複製,從而顯著提升了網路伺服器傳輸靜態檔案的效率。

上圖展示了應用程式 (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 系統呼叫的數量和功能在過去十年間持續演進、增減,以適應不斷變化的硬體和應用需求,但其核心介面保持了相對穩定。
> 
---
## 從縮減 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/)
> * 系統呼叫作為應用程式和核心之間的橋樑:
> 
> * 系統呼叫時的 instruction pointer 變化:
> 
#### 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 的三種路徑**
> 
> 在 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 可以包含更複雜的邏輯,並且更容易擴展。


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

> 搭配閱讀:
> * [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)