2. 系统调用号基础:定义与内核位置
系统调用号(syscall number)是用户态程序与内核通信的“门牌号”。每个系统调用在全局范围内被分配一个唯一的整数标识符,内核通过该号码在系统调用表中索引对应的处理函数。本章聚焦于系统调用号的定义方式及其在 x86-64 架构下的具体存放位置。
核心概念:系统调用号是用户态与内核态之间的协议编号。用户态通过
rax 寄存器传递系统调用号,内核根据该号码查找并执行对应的内核函数。
2.1 系统调用号的定义规则
Linux 内核为每个架构维护独立的系统调用号分配表。x86-64 架构的系统调用号定义遵循以下规则:
- 唯一性:每个系统调用号在同一个架构内全局唯一,不可重复。
- 稳定性:已分配的系统调用号一旦发布,在后续内核版本中不会更改,以保证用户态程序的二进制兼容性。
- 预留范围:0–300 为常用系统调用(如 read、write、open),300–500 为扩展系统调用(如 epoll、signalfd),500 以上为架构特定或实验性调用。
- 废弃处理:废弃的系统调用号不会立即删除,而是标记为
sys_ni_syscall(空函数),防止被重新分配。
| 范围 | 用途 | 示例 |
|---|---|---|
| 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
该文件是一个纯文本表格,每行定义一个系统调用,包含四个字段:
| 字段 | 含义 | 示例 |
|---|---|---|
<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)。用户态发起系统调用时,内核通过以下步骤完成映射:
- 用户态将系统调用号放入
rax寄存器,执行syscall指令。 - 内核入口(entry_SYSCALL_64)保存寄存器状态,从
rax提取系统调用号。 - 合法性检查:判断系统调用号是否在有效范围内(0 ≤ nr < NR_syscalls)。
- 表索引:以系统调用号为索引,从
sys_call_table数组中取出对应的函数指针。 - 函数调用:通过函数指针跳转到具体的系统调用实现函数(如
sys_read)。
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,硬编码将导致程序在其他架构上崩溃。