2. 系统调用号基础:定义与内核位置

系统调用号(syscall number)是用户态程序与内核通信的“门牌号”。每个系统调用在全局范围内被分配一个唯一的整数标识符,内核通过该号码在系统调用表中索引对应的处理函数。本章聚焦于系统调用号的定义方式及其在 x86-64 架构下的具体存放位置。

核心概念:系统调用号是用户态与内核态之间的协议编号。用户态通过 rax 寄存器传递系统调用号,内核根据该号码查找并执行对应的内核函数。

2.1 系统调用号的定义规则

Linux 内核为每个架构维护独立的系统调用号分配表。x86-64 架构的系统调用号定义遵循以下规则:

  • 唯一性:每个系统调用号在同一个架构内全局唯一,不可重复。
  • 稳定性:已分配的系统调用号一旦发布,在后续内核版本中不会更改,以保证用户态程序的二进制兼容性。
  • 预留范围:0–300 为常用系统调用(如 read、write、open),300–500 为扩展系统调用(如 epoll、signalfd),500 以上为架构特定或实验性调用。
  • 废弃处理:废弃的系统调用号不会立即删除,而是标记为 sys_ni_syscall(空函数),防止被重新分配。
系统调用号分配范围(x86-64)
范围 用途 示例
0–100 基础 I/O 与进程控制 read(0), write(1), open(2), close(3)
101–200 文件系统与内存管理 mmap(9), mprotect(10), stat(4)
201–300 网络与信号 socket(41), connect(42), kill(62)
301–400 高级 I/O 多路复用 epoll_create(213), eventfd(284)
401–500 命名空间与安全 clone3(435), faccessat2(439)
开发提示:在编写用户态程序时,应始终使用 sys/syscall.h 中定义的宏(如 SYS_read),而非直接使用数字编号。宏定义会随架构自动适配,保证代码可移植性。

2.2 系统调用号在内核中的位置

在 x86-64 架构下,系统调用号的定义文件位于内核源码的固定路径:

arch/x86/entry/syscalls/syscall_64.tbl

该文件是一个纯文本表格,每行定义一个系统调用,包含四个字段:

syscall_64.tbl 字段结构
字段 含义 示例
<number> 系统调用号(十进制整数) 0
<abi> 调用约定(common / 64 / x32) common
<name> 系统调用名称(小写) read
<entry point> 内核入口函数名(以 sys_ 或 __x64_sys_ 开头) sys_read

以下是 syscall_64.tbl 文件的部分真实内容示例:

#
# 64-bit system call numbers and entry vectors
#
0	common	read			sys_read
1	common	write			sys_write
2	common	open			sys_open
3	common	close			sys_close
4	common	stat			sys_stat
5	common	fstat			sys_fstat
...
435	common	clone3			__x64_sys_clone3
439	common	faccessat2		__x64_sys_faccessat2
注意:不要直接编辑 syscall_64.tbl 文件来添加自定义系统调用。该文件由内核构建系统自动处理,生成 arch/x86/include/generated/asm/syscalls_64.h 头文件。自定义系统调用应通过内核模块或补丁方式实现,后续章节会详细讲解。

2.3 系统调用号到函数指针的映射流程

内核在启动时或编译时,将 syscall_64.tbl 中的定义转换为一个函数指针数组——系统调用表(sys_call_table)。用户态发起系统调用时,内核通过以下步骤完成映射:

  1. 用户态将系统调用号放入 rax 寄存器,执行 syscall 指令。
  2. 内核入口(entry_SYSCALL_64)保存寄存器状态,从 rax 提取系统调用号。
  3. 合法性检查:判断系统调用号是否在有效范围内(0 ≤ nr < NR_syscalls)。
  4. 表索引:以系统调用号为索引,从 sys_call_table 数组中取出对应的函数指针。
  5. 函数调用:通过函数指针跳转到具体的系统调用实现函数(如 sys_read)。
系统调用号到函数指针的映射流程 系统调用号映射流程(x86-64) 用户态程序 rax = 系统调用号 syscall 指令 CPU 切换到内核态 entry_SYSCALL_64 保存寄存器 合法性检查 0 ≤ nr < NR_syscalls sys_call_table[nr] 获取函数指针 执行内核函数 如 sys_read() 说明: sys_call_table 是一个函数指针数组,在 arch/x86/kernel/syscall_64.c 中定义。 数组大小由 NR_syscalls 决定,未使用的槽位填充为 sys_ni_syscall(返回 -ENOSYS)。

2.4 系统调用表的生成机制

内核构建系统通过脚本自动从 syscall_64.tbl 生成头文件和 C 代码。生成流程如下:

  • 输入:arch/x86/entry/syscalls/syscall_64.tbl
  • 脚本:arch/x86/entry/syscalls/syscalltbl.sh
  • 输出 1:arch/x86/include/generated/asm/syscalls_64.h(宏定义头文件)
  • 输出 2:arch/x86/include/generated/asm/syscall_table.h(函数声明数组)

生成的 syscalls_64.h 内容示例:

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
...
#define __NR_clone3 435
#define __NR_faccessat2 439

#ifdef __KERNEL__
#define NR_syscalls 440
#endif
内核调试技巧:可以通过 /proc/kallsyms 查看系统调用表的实际内存地址:sudo grep sys_call_table /proc/kallsyms。该地址在开启 KASLR 时每次启动都会变化。

2.5 架构差异与兼容性

不同架构的系统调用号定义相互独立,即使功能相同的系统调用在不同架构下也可能使用不同的编号。以下是常见架构的系统调用号文件位置:

各架构系统调用号定义文件
架构 文件路径 调用约定
x86-64 arch/x86/entry/syscalls/syscall_64.tbl syscall 指令,rax 传号
x86-32 arch/x86/entry/syscalls/syscall_32.tbl int 0x80,eax 传号
ARM-64 arch/arm64/include/asm/unistd.h svc 指令,x8 传号
RISC-V arch/riscv/include/asm/unistd.h ecall 指令,a7 传号
兼容性警告:在编写跨架构的用户态程序时,必须使用 sys/syscall.h 中的宏定义(如 SYS_read),而不是硬编码数字。例如,x86-64 中 read 的系统调用号为 0,但在 ARM-64 中为 63,硬编码将导致程序在其他架构上崩溃。