3. 系统调用编号:每个系统调用的唯一编号、在x86和ARM架构下的编号差异、如何查看系统调用表

系统调用编号(Syscall Number)是内核为每个系统调用分配的唯一整数标识符。用户态程序通过将参数和系统调用编号存入特定寄存器,然后触发软中断或使用专用指令陷入内核,内核根据该编号在系统调用表中索引对应的处理函数。不同架构(如x86与ARM)的编号分配存在显著差异,理解这些差异是编写跨平台系统级程序的基础。

核心要点:系统调用编号是用户态与内核态之间的“协议号”,架构不同则编号不同,同一架构内编号通常保持向后兼容。

3.1 系统调用编号的作用与分配原则

每个系统调用在编译时被赋予一个非负整数,该编号在系统调用表中作为数组下标使用。内核维护一个名为 sys_call_table 的函数指针数组,数组的第 n 项即对应编号为 n 的系统调用处理函数。

  • 唯一性:同一架构内,每个系统调用拥有唯一的编号,不可重复。
  • 稳定性:已发布的系统调用编号不会轻易变更,以保证应用程序二进制接口(ABI)的向后兼容。
  • 架构相关性:不同CPU架构(x86、ARM、RISC-V等)独立维护各自的编号空间,同一功能在不同架构下编号可能不同。
  • 预留与扩展:编号通常按功能模块分组(如文件操作、进程控制、网络等),并预留部分编号供未来扩展或特定架构使用。
提示:在编写汇编级系统调用代码时,必须使用目标架构对应的系统调用编号,否则会导致调用失败或执行错误的函数。

3.2 x86架构下的系统调用编号

x86架构(包括32位和64位)是Linux最早支持的架构之一,其系统调用编号体系经历了从32位到64位的演进。x86-64(amd64)架构重新定义了编号,与传统的x86-32(i386)编号完全不同。

3.2.1 x86-32(i386)编号特点

  • 编号范围:通常从0开始,最大编号约380左右(随内核版本增加)。
  • 调用约定:通过 int 0x80 软中断陷入内核,系统调用编号存放在 eax 寄存器中。
  • 典型编号示例:sys_read 编号为3,sys_write 编号为4,sys_open 编号为5。

3.2.2 x86-64(amd64)编号特点

  • 编号范围:从0开始,当前已分配至约450左右。
  • 调用约定:使用 syscall 指令,系统调用编号存放在 rax 寄存器中。
  • 典型编号示例:sys_read 编号为0,sys_write 编号为1,sys_open 编号为2。
  • 注意:x86-64的编号与i386完全不同,例如 sys_read 在i386中是3,在x86-64中是0。
常见系统调用在x86-32与x86-64下的编号对比
系统调用名称 功能描述 x86-32 (i386) 编号 x86-64 (amd64) 编号
sys_read 从文件描述符读取数据 3 0
sys_write 向文件描述符写入数据 4 1
sys_open 打开或创建文件 5 2
sys_close 关闭文件描述符 6 3
sys_fork 创建子进程 2 57
sys_execve 执行新程序 11 59
sys_exit 终止当前进程 1 60
注意:x86-64架构为了优化性能,重新编排了编号顺序,将最常用的系统调用(如read、write、open)分配了最小的编号(0、1、2),以加速系统调用表的索引。

3.3 ARM架构下的系统调用编号

ARM架构分为32位(ARM32)和64位(AArch64)两种模式,它们的系统调用编号体系同样存在差异。ARM32使用 swi(软件中断)指令,而AArch64使用 svc 指令。

3.3.1 ARM32(EABI)编号特点

  • 编号范围:从0开始,当前已分配至约400左右。
  • 调用约定:系统调用编号存放在 r7 寄存器中,通过 swi #0 陷入内核。
  • 典型编号示例:sys_read 编号为3,sys_write 编号为4,sys_open 编号为5。
  • 注意:ARM32的编号分配与x86-32非常相似,但并非完全一致。

3.3.2 AArch64(ARM64)编号特点

  • 编号范围:从0开始,当前已分配至约450左右。
  • 调用约定:系统调用编号存放在 x8 寄存器中,通过 svc #0 陷入内核。
  • 典型编号示例:sys_read 编号为63,sys_write 编号为64,sys_open 编号为56。
  • 注意:AArch64的编号与ARM32完全不同,且与x86-64也不同。
常见系统调用在ARM32与AArch64下的编号对比
系统调用名称 功能描述 ARM32 (EABI) 编号 AArch64 (ARM64) 编号
sys_read 从文件描述符读取数据 3 63
sys_write 向文件描述符写入数据 4 64
sys_open 打开或创建文件 5 56
sys_close 关闭文件描述符 6 57
sys_fork 创建子进程 2 220
sys_execve 执行新程序 11 221
sys_exit 终止当前进程 1 93
关键差异总结:同一功能(如 sys_read)在x86-64中编号为0,在AArch64中编号为63,在ARM32中编号为3。跨架构移植汇编级系统调用代码时,必须重新映射编号。

3.4 架构间系统调用编号差异对比图

下图展示了四种主流架构下,部分核心系统调用的编号分布情况,直观体现架构间的编号差异。

四种架构系统调用编号对比图 四种架构核心系统调用编号对比 x86-32 x86-64 ARM32 AArch64 系统调用 sys_read sys_write sys_open sys_close sys_fork sys_execve sys_exit x86-32 3 4 5 6 2 11 1 x86-64 0 1 2 3 57 59 60 ARM32 3 4 5 6 2 11 1 AArch64 63 64 56 57 220 221 93 观察要点 • x86-64 将常用调用编号压缩到 0-3 • ARM32 与 x86-32 编号高度相似 • AArch64 编号整体偏大且独立 图:四种主流架构下7个核心系统调用的编号对比

3.5 如何查看系统调用表

系统调用表是内核源代码中的核心数据结构,开发者可以通过多种方式查看当前内核支持的完整系统调用列表及其编号。

3.5.1 通过内核源代码查看

Linux内核源代码中,系统调用表定义在架构相关的汇编文件或C文件中。以下是各架构的典型路径:

各架构系统调用表定义位置
架构 系统调用表文件路径 文件类型
x86-32 (i386) arch/x86/entry/syscalls/syscall_32.tbl 表格文件
x86-64 (amd64) arch/x86/entry/syscalls/syscall_64.tbl 表格文件
ARM32 (EABI) arch/arm/tools/syscall.tbl 表格文件
AArch64 (ARM64) include/uapi/asm-generic/unistd.h 头文件

以x86-64为例,syscall_64.tbl 文件的内容格式如下:

#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point> <compat entry point>
#
0	common	read			sys_read
1	common	write			sys_write
2	common	open			sys_open
3	common	close			sys_close
...
57	common	fork			sys_fork
59	common	execve			sys_execve
60	common	exit			sys_exit
...

3.5.2 通过 /proc/kallsyms 查看运行时系统调用表地址

在运行中的Linux系统上,可以通过 /proc/kallsyms 查看系统调用表 sys_call_table 的内存地址:

# 查看 sys_call_table 的地址(需要 root 权限)
sudo grep sys_call_table /proc/kallsyms

# 输出示例(x86-64):
# ffffffff82000200 R sys_call_table
提示:/proc/kallsyms 中的地址是内核符号的虚拟地址,可用于内核调试或动态跟踪(如使用kprobe)。

3.5.3 通过 ausyscall 工具查看用户态编号映射

ausyscall 是 auditd 软件包提供的工具,可以列出当前系统支持的完整系统调用编号与名称映射:

# 安装 auditd 软件包(如未安装)
sudo apt-get install auditd   # Debian/Ubuntu
sudo yum install audit        # RHEL/CentOS

# 列出所有系统调用编号
ausyscall --dump

# 输出示例(部分):
# 0   read
# 1   write
# 2   open
# 3   close
# 4   stat
# 5   fstat
# ...

3.5.4 通过 strace 跟踪实际系统调用编号

strace 可以跟踪进程执行时的系统调用,并显示每个调用的编号(使用 -e raw 选项):

# 跟踪 ls 命令的系统调用,显示原始编号
strace -e raw=all ls /tmp

# 输出示例(部分):
# execve(0x7ffd..., 0x7ffd..., 0x7ffd...) = 0
# brk(NULL)                          = 0x55...
# arch_prctl(0x3001, 0x7ffd...)      = 0
# ...
注意:strace -e raw 显示的是系统调用的原始编号,而非名称。结合 ausyscall --dump 可以对照编号与名称。

3.6 系统调用编号的生成机制

内核在编译时,通过脚本自动从 .tbl 表格文件生成头文件(如 asm/unistd.h)和汇编入口代码。以x86架构为例,生成流程如下:

  1. 源文件:arch/x86/entry/syscalls/syscall_64.tbl 定义编号与函数名的映射。
  2. 脚本处理:内核构建系统使用 scripts/syscalltbl.sh 脚本解析 .tbl 文件。
  3. 生成头文件:生成 arch/x86/include/generated/uapi/asm/unistd_64.h,其中定义 __NR_read 等宏。
  4. 生成汇编入口:生成 arch/x86/entry/syscall_64.c,其中包含 sys_call_table 数组的定义。

用户态程序通过包含 <sys/syscall.h> 或 <asm/unistd.h> 来获取系统调用编号宏定义:

#include <sys/syscall.h>   // 用户态标准头文件
#include <stdio.h>
#include <unistd.h>

int main() {
    // 直接使用系统调用编号宏
    long ret = syscall(__NR_write, STDOUT_FILENO, "Hello\n", 6);
    printf("syscall returned: %ld\n", ret);
    return 0;
}
最佳实践:在编写可移植代码时,始终使用 __NR_xxx 宏而非硬编码数字,因为宏会根据目标架构自动展开为正确的编号。

3.7 系统调用编号的向后兼容与演进

Linux内核在演进过程中,系统调用编号遵循以下原则:

  • 已分配编号永不回收:即使某个系统调用被废弃(如 sys_old_readdir),其编号也不会被重新分配给新系统调用,以避免破坏旧二进制程序。
  • 新系统调用追加到末尾:新增加的系统调用通常分配当前最大编号+1,确保不影响已有编号。
  • 架构特定编号空间:每个架构独立管理编号,例如x86-64的编号0-450与AArch64的编号0-450含义完全不同。
  • 兼容层支持:x86-64内核通过 ia32_sys_call_table 兼容32位系统调用,该表使用x86-32的编号体系。

下图展示了系统调用编号在内核中的索引与分发流程:

系统调用编号索引与分发流程 用户态程序 编号存入寄存器 (rax/r7/x8) 陷入内核 (syscall/int 0x80/svc) sys_call_table ← 上一章 📖 返回目录 下一章 →