1、引言
本篇文章以 ARM 架構為例,進行講解。需要讀者有一定的 ARM 架構基礎
??在操作系統的世界中,系統調用(System Call)是用戶空間與內核空間溝通的橋梁。用戶態程序如 ls、cp 或你的 C 程序,無權直接操作硬件、訪問文件系統或調度進程,它們必須通過系統調用,讓內核代為完成這些敏感任務。
a. 為什么需要系統調用?
??計算機系統通常被分為兩個運行級別:
- 內核態(Kernel Mode):有最高權限,可以直接操作硬件資源
- 用戶態(User Mode):權限受限,只能通過內核提供的接口間接訪問資源
??如果允許用戶程序直接訪問硬件,系統將變得混亂、極不安全。因此,操作系統提供了一套安全的、受控的接口:系統調用。這既保證了安全性,也為跨平臺移植性打下基礎。
b. 系統調用的作用與設計目的
??系統調用的設計是為了解決以下核心問題:
- 權限隔離:防止用戶程序破壞系統穩定性
- 抽象封裝:統一對硬件的訪問接口(如網絡、磁盤、內存)
- 標準化接口:方便編譯器、語言運行時與硬件解耦
??例如,當你在 C 程序中調用 write(),它本質上會通過系統調用將數據寫入文件描述符對應的設備或文件。這背后,Linux 內核通過系統調用號查找對應的內核函數,在內核態完成實際的寫入工作。
c. 一個簡單示例:write()
#include <unistd.h>int main() {write(1, "Hello, ARM-Linux!\n", 18);return 0;
}
這個程序將字符串輸出到標準輸出(文件描述符 1,對應終端)。雖然我們只寫了一行代碼,實際發生的事情包括:
- 編譯器將 write() 轉換為一次系統調用指令(如 ARM 的 svc #0)
- 系統調用號被放入特定寄存器(如 r7),參數通過寄存器傳遞
- CPU 從用戶態切換到內核態,執行系統調用號對應的 sys_write 函數
- 執行完成后返回結果,再切回用戶態
2、系統調用的初始化
2.1 sys_call_table 系統調用符號表
??了解了系統調用的基本概念之后,我們同樣需要知道,系統調用有哪些?
??系統調用由內核中定義的一個靜態數組描述的,這個數組名為 sys_call_table
,系統調用的數量由 NR_syscalls
這個宏描述,默認為 400,也可以手動地修改,這些系統調用的定義在 entry-common.S
中:
Linux/arch/arm/kernel/entry-common.S 文件中有這樣一段代碼:
/** This is the syscall table declaration for native ABI syscalls.* With EABI a couple syscalls are obsolete and defined as sys_ni_syscall.*/syscall_table_start sys_call_table
#define COMPAT(nr, native, compat) syscall nr, native
#ifdef CONFIG_AEABI
#include <calls-eabi.S>
#else
#include <calls-oabi.S>
#endif
#undef COMPATsyscall_table_end sys_call_table
??calls-eabi.S
是在構建過程中由 Linux 內核構建系統根據 syscall.tbl
自動生成的系統調用表定義文件,并不會在源碼中直接出現。它為 ARM 架構的 EABI 系統提供系統調用分發表,通過 sys_call_table
引用,用于系統調用調度。
??以 ARM 架構為例:
輸入文件:
arch/arm/tools/syscall.tbl
格式是這樣的:
nr abi name entry-point [compat]
0 common restart_syscall sys_restart_syscall
1 common exit sys_exit
2 common fork sys_fork
3 common read sys_read
4 common write sys_write
5 common open sys_open
6 common close sys_close
...
腳本生成:
使用構建腳本如:
scripts/syscall-generate.sh
生成結果放在:
arch/arm/kernel/calls-eabi.S
生成結果如下:
NATIVE(0, sys_restart_syscall)
NATIVE(1, sys_exit)
NATIVE(2, sys_fork)
NATIVE(3, sys_read)
NATIVE(4, sys_write)
NATIVE(5, sys_open)
NATIVE(6, sys_close)
......
2.2 sys_call_table 的創建
??Linux/arch/arm/kernel/entry-common.S 文件中,有三個部分需要關注,syscall_table_start
、syscall_table_end
和 NATIVE
這三個宏,接下來就一個個解析。
syscall_table_start:
.macro syscall_table_start, sym.equ __sys_nr, 0.type \sym, #object
ENTRY(\sym)
.endm
含義:
- 宏定義名:syscall_table_start,帶一個參數 sym(表名)
- __sys_nr 是當前的系統調用編號,從 0 開始
- .type \sym, #object 告訴調試器這是一個對象變量,例如數組、變量等(符號表使用)
- ENTRY(\sym) 展開為 .global \sym; \sym:,即導出符號
📌 示例:
如果寫 syscall_table_start sys_call_table,就等價于:
.global sys_call_table
.type sys_call_table, #object
sys_call_table:
__sys_nr = 0
簡要概括就是:
- 創建一個 sys_call_table 的符號并使用 .globl 導出到全局符號
- sys_call_table 是一個對象變量,可以理解為一個數組的首地址
- 定義一個內部符號 __sys_nr,初始化為 0,這個變量主要用于后續系統調用好的計算和判斷
NATIVE:
.macro syscall, nr, func.ifgt __sys_nr - \nr.error "Duplicated/unorded system call entry".endif.rept \nr - __sys_nr.long sys_ni_syscall.endr.long \func.equ __sys_nr, \nr + 1
.endm#define NATIVE(nr, func) syscall nr, func
含義:
- 插入一個系統調用函數(func)到編號為 nr 的位置
- 做檢查:如果當前編號比傳入編號大,說明 syscall 編號是無序或重復的,編譯報錯
- 如果 nr > __sys_nr,則插入空洞(sys_ni_syscall),表示之前的系統調用未定義
- 然后插入 .long \func,即 syscall 的函數地址
- 最后更新 __sys_nr
📌 示例:
NATIVE(5, sys_open)
如果當前編號是 0,則展開為:
.long sys_restart_syscall; index 0
.long sys_exit; index 1
......
.long sys_open; index 5
.long sys_open 表示: “生成一個占用 4 字節的值,這個值是 sys_open 的地址”。
接下來就是系統調用的收尾部分:syscall_table_end sys_call_table,傳入的參數為 sys_call_table,它也是通過宏實現的:
.macro syscall_table_end, sym.ifgt __sys_nr - __NR_syscalls.error "System call table too big".endif.rept __NR_syscalls - __sys_nr.long sys_ni_syscall.endr.size \sym, . - \sym
.endm
含義:
- 檢查當前 syscall 編號是否超過 __NR_syscalls(系統調用總數)
- 如果沒有填滿表,則用 sys_ni_syscall 補齊
- 最后通過 .size 設置
sys_call_table
對象變量的大小(也就是數組的大小)
總結:
??上面的這套宏系統的作用,在匯編文件 entry-common.S
中定義了一個 sys_call_table
表。表中是各樣的系統調用號以及其對應的內核接口地址。
3、系統調用的產生
??系統調用盡管是由用戶空間產生的,但是在日常的編程工作中我們并不會直接使用系統調用,只知道在使用諸如 read、write 函數時,對應的系統調用就會產生,實際上,發起系統調用的真正工作封裝在 C 庫中,要查看系統調用的產生細節,一方面可以查看 C 庫,另一方面也可以查看編譯時的匯編代碼。
如果想通過查看編譯時的匯編代碼,找到系統調用的細節,編譯時必須使用靜態鏈接 libc.a 庫
3.1 glibc
??既然系統調用基本都是封裝在 glibc 中,最直接的方法就是看看它們的源代碼實現,因為只是探究系統調用的流程,找一個相對簡單的函數作為示例即可,這里以 close 為例,下載的源代碼版本為 glibc-2.30。
glibc 作為 GNU 的標準 C 程序庫,在使用 gcc 編譯目標文件時,默認是會去鏈接 libc.so/libc.a 這種 glibc 庫的
close 的定義在 close.c 中:
int __close (int fd)
{return SYSCALL_CANCEL (close, fd);
}
SYSCALL_CANCEL
是一個宏,被定義在 sysdeps/unix/sysdep.h
中,由于該宏的嵌套有些復雜,全部貼上來進行解析并沒有太多必要,就只貼上它的調用路徑:
SYSCALL_CANCEL->INLINE_SYSCALL_CALL->__INLINE_SYSCALL_DISP->__INLINE_SYSCALLn(n=1~7)->INLINE_SYSCALL
??對于不同的架構,INLINE_SYSCALL
有不同的實現,畢竟系統調用指令完全是硬件相關指令,可以想到其最后的定義肯定是架構相關的,而 arm 平臺的 INLINE_SYSCALL
實現在 sysdeps/unix/sysv/linux/arm/sysdep.h:
INLINE_SYSCALL->INTERNAL_SYSCALL->INTERNAL_SYSCALL_RAW
??整個實現流程幾乎全部由宏實現,在最后的 INTERNAL_SYSCALL_RAW
中,執行了以下的指令:
...
# define INTERNAL_SYSCALL_RAW(name, err, nr, args...) \({ \register int _a1 asm ("r0"), _nr asm ("r7"); \LOAD_ARGS_##nr (args) \_nr = name; \asm volatile ("swi 0x0 @ syscall " #name \: "=r" (_a1) \: "r" (_nr) ASM_ARGS_##nr \: "memory"); \_a1; })
...
??其中的 swi 指令正是執行系統調用的軟中斷指令,在新版的 arm 架構中,使用 svc 指令代替 swi,這兩者是別名的關系,沒有什么區別。
??這里需要區分系統調用和普通函數調用,對于普通函數調用而言,前四個參數被保存在 r0~r3 中,其它的參數被保存在棧上進行傳遞。
??但是在系統調用中,swi(svc) 指令將會引起處理器模式的切換,user->svc,而 svc 模式下的 sp 和 user 模式下的 sp 并不是同一個,因此無法使用棧直接進行傳遞,從而需要將所有的參數保存在寄存器中進行傳遞,在內核文件 include/linux/syscall.h 中定義了系統調用相關的函數和宏,其中 SYSCALL_DEFINE_MAXARGS
表示系統調用支持的最多參數值,在 arm 下為 6,也就是 arm 中系統調用最多支持 6 個參數,分別保存在 r0~r5 中。
glibc 庫是如何知道每個函數對應的系統調用號的呢?在內核構建過程中,有一個腳本叫 scripts/syscallhdr.sh 會將 syscall.tbl 中的內容轉成 unistd.h 中的一行行:
#define __NR_close 6
#define __NR_open 5
#define __NR_openat 56
…
glibc 通過包含 Linux 內核提供的頭文件(unistd.h),在編譯時就知道每個系統調用對應的 syscall number
3.2 Linux 內核中系統調用的處理
??svc 指令實際上是一條軟件中斷指令,也是從用戶空間主動到內核空間的唯一通路(被動可以通過中斷、其它異常) 相對應的處理器模式為從 user 模式到 svc 模式,svc 指令執行系統調用的大致流程為:
- 執行 svc 指令,產生軟中斷,跳轉到系統中斷向量表的 svc 向量處執行指令
- 保存用戶模式下的程序斷點信息,以便系統調用返回時可以恢復用戶進程的執行
- 根據傳入的系統調用號(r7)確定內核中需要執行的系統調用,比如 read 對應 sys_read(從第二章節我們創建的 sys_call_table 符號表中尋找函數入口)
- 執行完系統調用之后返回到用戶進程,繼續執行用戶程序
??上述只是一個簡化的流程,省去了很多的實現細節以及在真實操作系統中的多進程環境,不過通過這些可以建立一個對于系統調用大致的概念。
4、拓展
4.1 ARM64 架構下 sys_call_table 的創建
??和 ARM 不同,ARM64 采取頭文件聲明(數組)的方式,看起來更簡單、更清晰。對于 ARM64 架構,sys_call_table
的構建如下:
arch/arm64/kernel/sys.c:
/** Wrappers to pass the pt_regs argument.*/
#define __arm64_sys_personality __arm64_sys_arm64_personality#undef __SYSCALL
#define __SYSCALL(nr, sym) asmlinkage long __arm64_##sym(const struct pt_regs *);
#include <asm/unistd.h>#undef __SYSCALL
#define __SYSCALL(nr, sym) [nr] = __arm64_##sym,const syscall_fn_t sys_call_table[__NR_syscalls] = {[0 ... __NR_syscalls - 1] = __arm64_sys_ni_syscall,
#include <asm/unistd.h>
};
arch/arm64/include/asm/unistd.h:
#ifndef __COMPAT_SYSCALL_NR
#include <uapi/asm/unistd.h>
#endif
arch/arm64/include/uapi/asm/unistd.h:
#define __ARCH_WANT_RENAMEAT
#define __ARCH_WANT_NEW_STAT
#define __ARCH_WANT_SET_GET_RLIMIT
#define __ARCH_WANT_TIME32_SYSCALLS
#define __ARCH_WANT_SYS_CLONE3#include <asm-generic/unistd.h>
include/uapi/asm-generic/unistd.h:(這里是 Linux 默認的 unistd.h 文件,ARM 架構沒有使用這個)
......
#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
......
/* fs/read_write.c */
#define __NR3264_lseek 62
__SC_3264(__NR3264_lseek, sys_llseek, sys_lseek)
#define __NR_read 63
__SYSCALL(__NR_read, sys_read)
#define __NR_write 64
__SYSCALL(__NR_write, sys_write)
......
上面的頭文件相互包含,關系有點亂,需要仔細理一下。這里直接給出結論,上面文件展開如下:
const syscall_fn_t sys_call_table[__NR_syscalls] = {[0] = __arm64_compat_sys_io_setup,[1] = __arm64_sys_io_destroy,...[63] = __arm64_sys_read,...[__NR_syscalls - 1] = xxx,};
至此我們就得到了完整的 sys_call_table
。
關于表中為什么沒有 sys_open,Linux 內核從 2.6.16 起,就逐步推薦使用 openat() 代替 open()。所以只有 sys_openat。在用戶空間仍可用 open 函數,由 libc 實現為對 openat() 的封裝
4.2 關于 unistd.h 文件
關于上面眾多的 unistd.h
,這里簡要說明一下
- unistd = UNIX + standard
- 所以 unistd.h = “UNIX 標準頭文件”
它是 POSIX 標準中定義的頭文件之一,用于提供 UNIX 系統調用接口的聲明。
unistd.h 里面都包含什么?
它主要包含:
- 系統調用聲明(如 read, write, fork, exec, pipe, chdir, getpid, 等等)
- 標準常量(如 _POSIX_VERSION)
- 宏定義(如 STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO)
- 系統調用號(某些平臺上,如 Linux 下的 _NR* 宏)
它可以看作是與操作系統交互的通用入口。在不同的 Unix-like 系統中,unistd.h 提供一致的編程接口。
4.3 Linux 系統下查看系統調用號
usr/include/asm-generic/
是供用戶空間程序使用的 頭文件集合。其中的 unistd.h
定義系統調用號。
Linux 為了支持多種 CPU 架構(x86、ARM、MIPS、RISCV 等),采用以下策略:
頭文件查找順序(以 #include <asm/unistd.h> 為例):
- /usr/include/asm/unistd.h(如果目標架構提供了自定義)
- 否則 fallback 到:/usr/include/asm-generic/unistd.h
例如,以 rockchip rk3568 為例:
root@firefly:/usr/include/asm-generic# cat unistd.h
/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#include <asm/bitsperlong.h>/** This file contains the system call numbers, based on the* layout of the x86-64 architecture, which embeds the* pointer to the syscall in the table.** As a basic principle, no duplication of functionality* should be added, e.g. we don't use lseek when llseek* is present. New architectures should use this file* and implement the less feature-full calls in user space.*/#ifndef __SYSCALL
#define __SYSCALL(x, y)
#endif#if __BITS_PER_LONG == 32 || defined(__SYSCALL_COMPAT)
#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _32)
#else
#define __SC_3264(_nr, _32, _64) __SYSCALL(_nr, _64)
#endif#ifdef __SYSCALL_COMPAT
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _comp)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SYSCALL(_nr, _comp)
#else
#define __SC_COMP(_nr, _sys, _comp) __SYSCALL(_nr, _sys)
#define __SC_COMP_3264(_nr, _32, _64, _comp) __SC_3264(_nr, _32, _64)
#endif#define __NR_io_setup 0
__SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
#define __NR_io_destroy 1
__SYSCALL(__NR_io_destroy, sys_io_destroy)
#define __NR_io_submit 2
__SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
#define __NR_io_cancel 3
__SYSCALL(__NR_io_cancel, sys_io_cancel)
#if defined(__ARCH_WANT_TIME32_SYSCALLS) || __BITS_PER_LONG != 32
#define __NR_io_getevents 4
__SC_3264(__NR_io_getevents, sys_io_getevents_time32, sys_io_getevents)
#endif/* fs/xattr.c */
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSCALL(__NR_lsetxattr, sys_lsetxattr)
#define __NR_fsetxattr 7
__SYSCALL(__NR_fsetxattr, sys_fsetxattr)
#define __NR_getxattr 8
__SYSCALL(__NR_getxattr, sys_getxattr)
#define __NR_lgetxattr 9
__SYSCALL(__NR_lgetxattr, sys_lgetxattr)
#define __NR_fgetxattr 10
__SYSCALL(__NR_fgetxattr, sys_fgetxattr)
#define __NR_listxattr 11
__SYSCALL(__NR_listxattr, sys_listxattr)
#define __NR_llistxattr 12
__SYSCALL(__NR_llistxattr, sys_llistxattr)
#define __NR_flistxattr 13
__SYSCALL(__NR_flistxattr, sys_flistxattr)
#define __NR_removexattr 14
__SYSCALL(__NR_removexattr, sys_removexattr)
#define __NR_lremovexattr 15
__SYSCALL(__NR_lremovexattr, sys_lremovexattr)
#define __NR_fremovexattr 16
__SYSCALL(__NR_fremovexattr, sys_fremovexattr)