3. 系统调用号(syscall number):分配机制与常见列表
系统调用号(syscall number)是内核为每个系统调用分配的唯一整数标识符。在 x86-64 架构的 Linux 中,系统调用号从 0 开始顺序分配,用户态程序通过将系统调用号放入 rax 寄存器来告诉内核要执行哪个系统调用。系统调用号的分配遵循“一次分配、永久稳定”的原则,已分配的系统调用号不会在后续内核版本中变更或回收,以保证二进制兼容性。
核心原则:系统调用号是用户态与内核态之间的“协议编号”。一旦分配,永不改变。应用程序编译后依赖的是系统调用号,而非系统调用名称。
3.1 系统调用号的分配机制
系统调用号的分配由 Linux 内核社区统一管理,遵循以下规则:
- 顺序递增:新系统调用在现有最大值基础上递增分配,不会插入到已有编号之间。
- 架构独立 vs 架构特定:通用系统调用在所有架构上使用相同编号(如
read在 x86-64、ARM64 上均为 0);架构特定系统调用(如arch_prctl)只在特定架构上分配。 - 预留与废弃:已废弃的系统调用号会被标记为
sys_ni_syscall(空函数),但编号不会重新分配给其他系统调用。 - 最大编号限制:x86-64 架构当前支持最多 512 个系统调用(编号 0–511),实际已使用约 450 个。
查看当前系统调用号:在终端执行
ausyscall --dump(需安装 auditd)或查看 /usr/include/asm/unistd_64.h 头文件,可获取完整列表。
3.2 常见系统调用号列表(x86-64)
下表列出了 x86-64 架构中最常用的系统调用号及其对应函数。这些编号在 /usr/include/asm/unistd_64.h 中定义,是系统编程的基础知识。
| 系统调用号 | 名称 | 函数原型 | 功能简述 |
|---|---|---|---|
| 0 | read |
ssize_t read(int fd, void *buf, size_t count) |
从文件描述符读取数据 |
| 1 | write |
ssize_t write(int fd, const void *buf, size_t count) |
向文件描述符写入数据 |
| 2 | open |
int open(const char *pathname, int flags, mode_t mode) |
打开或创建文件 |
| 3 | close |
int close(int fd) |
关闭文件描述符 |
| 9 | mmap |
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) |
内存映射文件或设备 |
| 12 | brk |
int brk(void *addr) |
改变数据段结束位置(堆管理) |
| 39 | getpid |
pid_t getpid(void) |
获取当前进程 ID |
| 56 | clone |
int clone(unsigned long flags, void *child_stack, ...) |
创建子进程/线程 |
| 59 | execve |
int execve(const char *pathname, char *const argv[], char *const envp[]) |
执行新程序 |
| 60 | exit |
void exit(int status) |
终止当前进程 |
注意:不同架构的系统调用号可能不同。例如,
exit 在 x86-64 上是 60,但在 x86(32位)上是 1。编写跨架构汇编代码时,必须使用条件编译或动态判断。
3.3 系统调用号在 syscall table 中的索引作用
系统调用号直接作为内核中 sys_call_table 数组的索引。内核通过以下两步完成系统调用分发:
- 验证范围:检查系统调用号是否小于
NR_syscalls(最大系统调用数),超出则返回-ENOSYS。 - 数组索引:以系统调用号为偏移量,从
sys_call_table中取出对应的函数指针并调用。
以下代码展示了 x86-64 内核中系统调用入口的核心逻辑(简化版):
// arch/x86/entry/entry_64.S 中的系统调用入口
call *sys_call_table(, %rax, 8) // rax = 系统调用号,每个指针 8 字节
// 对应的 C 语言语义:
// typedef long (*syscall_fn_t)(struct pt_regs *);
// syscall_fn_t *sys_call_table = ...;
// long ret = sys_call_table[syscall_nr](regs);
3.4 系统调用号的查看与调试
在实际开发中,可以通过以下方式查看系统调用号:
- 头文件:
/usr/include/asm/unistd_64.h(x86-64)或/usr/include/asm/unistd_32.h(x86)。 - 命令行工具:
ausyscall --dump列出所有系统调用号与名称的对应关系。 - 动态追踪:使用
strace -e raw=all可显示每次系统调用的原始编号。 - 内核源码:
arch/x86/entry/syscalls/syscall_64.tbl是 x86-64 系统调用表的权威定义文件。
实战技巧:在编写内核模块或汇编代码时,应始终使用
__NR_read 等宏定义而非硬编码数字。这些宏在 <sys/syscall.h> 中定义,可确保跨架构兼容性。
3.5 系统调用号的稳定性与兼容性
系统调用号的稳定性是 Linux 内核“稳定 ABI”承诺的核心部分。具体表现为:
| 特性 | 说明 | 示例 |
|---|---|---|
| 永不改变 | 已分配的系统调用号在所有后续内核版本中保持不变 | read 永远是 0,write 永远是 1 |
| 永不回收 | 废弃的系统调用号保留为 sys_ni_syscall,不重新分配 |
uselib(134)已废弃但编号保留 |
| 向后兼容 | 新内核必须支持所有旧系统调用号 | Linux 6.x 仍支持 Linux 1.0 的 sys_write |
| 架构差异 | 不同架构的同名系统调用可能编号不同 | exit:x86-64=60,ARM=1 |
兼容性陷阱:不要假设系统调用号在不同架构上相同。例如,
openat 在 x86-64 上是 257,但在 ARM64 上是 56。编写可移植代码时,务必使用 syscall() 函数或 __NR_* 宏。