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 (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 |
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 (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 架构间系统调用编号差异对比图
下图展示了四种主流架构下,部分核心系统调用的编号分布情况,直观体现架构间的编号差异。
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架构为例,生成流程如下:
- 源文件:
arch/x86/entry/syscalls/syscall_64.tbl定义编号与函数名的映射。 - 脚本处理:内核构建系统使用
scripts/syscalltbl.sh脚本解析.tbl文件。 - 生成头文件:生成
arch/x86/include/generated/uapi/asm/unistd_64.h,其中定义__NR_read等宏。 - 生成汇编入口:生成
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的编号体系。
下图展示了系统调用编号在内核中的索引与分发流程: