3. 系统调用表(sys_call_table):结构、索引与处理函数映射
系统调用表是内核将系统调用号(syscall number)映射到对应内核处理函数的核心数据结构。它是一个函数指针数组,数组的索引就是系统调用号,数组元素是对应的内核服务例程(kernel service routine)地址。理解 sys_call_table 的结构与索引机制,是自定义系统调用实现的基础。
核心概念:系统调用表本质上是一个
const void * 类型的函数指针数组,每个元素指向一个 asmlinkage long (*fn)(struct pt_regs *) 类型的函数。系统调用号作为数组下标,直接定位到对应的处理函数入口。
3.1 sys_call_table 的数据结构
在 x86-64 架构(Linux 5.x 内核)中,系统调用表定义在 arch/x86/entry/syscall_64.c 文件中。其核心结构如下:
// arch/x86/entry/syscall_64.c
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
关键要素说明:
- sys_call_ptr_t:函数指针类型,定义为
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);。 - __NR_syscall_max:当前架构支持的最大系统调用号,x86-64 上通常为 547(Linux 6.x 内核)。
- 默认填充:未使用的表项初始化为
__x64_sys_ni_syscall(not implemented),返回 -ENOSYS。 - asm/syscalls_64.h:由脚本生成的头文件,包含所有已注册系统调用的函数指针初始化项。
架构差异提示:ARM64 架构的 sys_call_table 定义在
arch/arm64/kernel/sys.c,使用 sys_call_table[NR_syscalls] 数组;x86-32 则使用 ia32_sys_call_table 兼容 32 位程序。不同架构的系统调用号范围和处理函数命名规则略有不同。
3.2 系统调用号到处理函数的索引流程
当用户态程序执行 syscall 指令时,内核通过以下步骤完成索引:
- 保存寄存器上下文:CPU 自动将系统调用号存入 RAX 寄存器,参数依次存入 RDI、RSI、RDX、R10、R8、R9。
- 进入入口函数:
entry_SYSCALL_64汇编入口保存 pt_regs 结构,调用do_syscall_64()。 - 边界检查:检查系统调用号是否在
[0, __NR_syscall_max]范围内。 - 表项索引:以系统调用号为下标,从
sys_call_table中取出函数指针。 - 调用处理函数:通过函数指针调用对应的内核服务例程,传入 pt_regs 指针。
安全边界:系统调用号必须经过严格的范围检查,防止越界访问 sys_call_table 导致内核崩溃或信息泄露。Linux 内核在
do_syscall_64() 中通过 unlikely(nr >= __NR_syscall_max) 进行快速路径检查。
3.3 索引流程的 SVG 架构图
3.4 系统调用表的内存布局
sys_call_table 在内存中是连续存储的函数指针数组。以下表格展示了 x86-64 架构下部分系统调用号的映射关系:
| 系统调用号 | 系统调用名称 | 处理函数 | 功能描述 |
|---|---|---|---|
| 0 | read | __x64_sys_read | 从文件描述符读取数据 |
| 1 | write | __x64_sys_write | 向文件描述符写入数据 |
| 2 | open | __x64_sys_open | 打开或创建文件 |
| 3 | close | __x64_sys_close | 关闭文件描述符 |
| 39 | getpid | __x64_sys_getpid | 获取当前进程 PID |
| 60 | exit | __x64_sys_exit | 终止当前进程 |
| 335 | statx | __x64_sys_statx | 获取文件扩展状态 |
函数命名规则:x86-64 架构的系统调用处理函数统一使用
__x64_sys_<name> 前缀,x86-32 兼容层使用 __ia32_sys_<name>。ARM64 架构则使用 __arm64_sys_<name>。这种命名约定通过 SYSCALL_DEFINEx() 宏自动生成。
3.5 系统调用表的初始化与动态修改
系统调用表在编译时通过 asm/syscalls_64.h 头文件静态初始化。该头文件由内核构建系统根据 syscall_64.tbl 表格自动生成。以下是初始化过程的 SVG 示意图:
3.6 系统调用表的只读保护
从 Linux 4.17 内核开始,sys_call_table 被放置在只读内存段(.rodata),防止恶意模块或 rootkit 篡改系统调用表。这一保护机制通过以下方式实现:
- const 限定符:数组声明为
asmlinkage const sys_call_ptr_t sys_call_table[],编译器将其放入 .rodata 段。 - 页表保护:内核将包含 sys_call_table 的内存页标记为只读,任何写入操作会触发页错误。
- CR0.WP 位:即使通过
__change_cr0()临时禁用写保护,也需要谨慎操作,避免竞态条件。
实践建议:在自定义系统调用开发中,不要尝试直接修改 sys_call_table。正确的做法是通过
register_syscall() 或 SYSCALL_DEFINEx() 宏在编译时注册新的系统调用,或者使用 kprobe/ftrace 等安全机制进行动态跟踪。
3.7 多架构系统调用表对比
| 架构 | 表名称 | 最大系统调用号 | 处理函数前缀 | 定义文件 |
|---|---|---|---|---|
| x86-64 | sys_call_table | __NR_syscall_max (约 547) | __x64_sys_ | arch/x86/entry/syscall_64.c |
| x86-32 | ia32_sys_call_table | IA32_NR_syscalls (约 447) | __ia32_sys_ | arch/x86/entry/syscall_32.c |
| ARM64 | sys_call_table | NR_syscalls (约 547) | __arm64_sys_ | arch/arm64/kernel/sys.c |
| RISC-V | sys_call_table | __NR_syscalls (约 547) | __riscv_sys_ | arch/riscv/kernel/sys_riscv.c |
兼容层说明:x86-64 内核通过
ia32_sys_call_table 支持 32 位应用程序的兼容运行。当 32 位程序执行 int 0x80 或 sysenter 指令时,内核使用 ia32_sys_call_table 进行索引,确保系统调用号映射正确。
3.8 系统调用表索引的汇编实现
在 x86-64 入口代码中,系统调用表的索引通过简单的数组寻址完成。以下是 entry_SYSCALL_64 中核心汇编逻辑的简化表示:
// arch/x86/entry/entry_64.S (简化)
ENTRY(entry_SYSCALL_64)
/* 保存寄存器上下文 */
swapgs
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
/* 构建 pt_regs 结构 */
pushq $__USER_DS
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
pushq %r11
pushq $__USER_CS
pushq %rcx
pushq %rax
pushq %rdi
pushq %rsi
pushq %rdx
pushq %r10
pushq %r8
pushq %r9
/* 调用 do_syscall_64 */
movq %rsp, %rdi /* rdi = pt_regs 指针 */
call do_syscall_64
/* 恢复上下文并返回 */
RESTORE_REGS
swapgs
sysretq
ENDPROC(entry_SYSCALL_64)
在 do_syscall_64() 函数中,索引逻辑如下:
// arch/x86/entry/common.c (简化)
static __always_inline void do_syscall_64(struct pt_regs *regs)
{
unsigned long nr = regs->orig_ax;
/* 系统调用号边界检查 */
if (unlikely(nr >= __NR_syscall_max))
return syscall_return_slowpath(regs, -ENOSYS);
/* 从 sys_call_table 索引并调用 */
regs->ax = sys_call_table[nr](regs);
}
性能关键路径:系统调用入口是内核中最频繁执行的代码路径之一。索引操作被优化为单次内存读取:
call *sys_call_table(, %rax, 8)(x86-64 上每个指针 8 字节)。任何对 sys_call_table 的修改都会影响所有系统调用的性能。