# 第五講:賦予應用程式生命的系統呼叫 > 本筆記僅為個人紀錄,相關教材之 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)