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 数组的索引。内核通过以下两步完成系统调用分发:

  1. 验证范围:检查系统调用号是否小于 NR_syscalls(最大系统调用数),超出则返回 -ENOSYS。
  2. 数组索引:以系统调用号为偏移量,从 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);
系统调用号索引 sys_call_table 结构图 系统调用号索引 sys_call_table 结构图 syscall_nr (如 0 = read) × 8 字节 sys_call_table[] [0] → sys_read [1] → sys_write [2] → sys_open [3] → sys_close 执行内核函数 索引计算: 函数指针地址 = sys_call_table + syscall_nr × 8

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_* 宏。