4、系统调用表结构(x86_64):sys_call_table 数组结构、每个表项指向的 sys_xxx 函数指针
在 x86_64 架构的 Linux 内核中,系统调用表是连接用户空间系统调用号与内核服务例程的核心数据结构。理解 sys_call_table 的数组结构、每个表项的函数指针类型以及它们在内存中的布局,是深入分析系统调用执行路径的关键。
4.1 sys_call_table 数组结构
sys_call_table 本质上是一个函数指针数组,定义在 arch/x86/entry/syscall_64.c 中。数组的索引对应系统调用号,数组元素指向具体的系统调用处理函数。
核心定义:
sys_call_table 是一个 const sys_call_ptr_t 类型的数组,数组大小由 __NR_syscall_max 决定。
// arch/x86/entry/syscall_64.c
typedef asmlinkage long (*sys_call_ptr_t)(const struct pt_regs *);
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定义为asmlinkage long (*)(const struct pt_regs *),所有系统调用函数接收一个指向pt_regs结构的指针,返回long类型。 - 数组初始化: 使用 C99 指定初始化器语法,默认填充
__x64_sys_ni_syscall(未实现系统调用处理函数),然后通过#include <asm/syscalls_64.h>覆盖已实现的系统调用表项。 - 只读属性:
const关键字使数组位于只读内存区域,防止恶意修改。
架构差异提示: x86_64 使用
sys_call_ptr_t 类型,而 x86_32 使用 asmlinkage long (*sys_call_ptr_t)(unsigned long, ...) 传统类型。64 位系统统一通过 pt_regs 传递参数,简化了调用约定。
4.2 表项指向的 sys_xxx 函数指针
每个系统调用表项指向一个具体的处理函数,这些函数遵循统一的命名规范和签名。以 read 系统调用(编号 0)为例:
// 系统调用号定义 (include/uapi/asm-generic/unistd.h)
#define __NR_read 0
// 系统调用表项 (通过 asm/syscalls_64.h 生成)
__SYSCALL_64(__NR_read, sys_read, ...)
// 实际处理函数定义 (fs/read_write.c)
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
return ksys_read(fd, buf, count);
}
函数指针的解析流程:
- 宏展开:
SYSCALL_DEFINE3(read, ...)展开为asmlinkage long sys_read(const struct pt_regs *regs)。 - 参数提取: 函数内部从
regs中提取di(fd)、si(buf)、dx(count)寄存器值。 - 表项注册:
__SYSCALL_64宏将sys_read地址填入sys_call_table[0]。
命名约定: 现代内核(5.x+)中,x86_64 系统调用函数通常以
__x64_sys_ 为前缀(如 __x64_sys_read),而 sys_read 是通用别名。在 sys_call_table 中实际存储的是 __x64_sys_read 的地址。
4.3 系统调用表结构可视化
下图展示了 sys_call_table 的数组结构、索引与函数指针的映射关系:
4.4 表项与函数指针类型详解
| 组件 | 类型/定义 | 说明 |
|---|---|---|
| sys_call_table | const sys_call_ptr_t [] |
只读函数指针数组,位于 .rodata 段 |
| sys_call_ptr_t | asmlinkage long (*)(const struct pt_regs *) |
函数指针类型,所有系统调用统一签名 |
| pt_regs | struct pt_regs |
保存用户态寄存器状态,包含参数传递寄存器 |
| __x64_sys_* | asmlinkage long __x64_sys_xxx(...) |
x86_64 架构的具体系统调用实现函数 |
| sys_ni_syscall | asmlinkage long sys_ni_syscall(void) |
未实现系统调用的默认处理,返回 -ENOSYS |
4.5 系统调用表的内存布局
在 x86_64 内核中,sys_call_table 的内存布局具有以下特征:
- 只读段(.rodata): 由于
const修饰符,数组位于内核镜像的只读数据段,防止被篡改。 - 连续存储: 数组元素在内存中连续排列,每个元素占用 8 字节(64 位指针大小)。
- 对齐要求: 数组按 8 字节对齐,确保 CPU 高效访问。
- 地址范围: 可通过
/proc/kallsyms查看sys_call_table的虚拟地址。
# 查看系统调用表地址(需要 root 权限)
$ sudo grep sys_call_table /proc/kallsyms
ffffffff82000300 R sys_call_table
# 查看特定系统调用函数地址
$ sudo grep __x64_sys_read /proc/kallsyms
ffffffff81234560 T __x64_sys_read
调试技巧: 使用
gdb 或 crash 工具可以直接转储 sys_call_table 内容,验证函数指针是否正确指向预期的处理函数。
4.6 系统调用表查找流程
当用户程序触发系统调用时,内核通过以下步骤查找并执行对应的处理函数:
- 保存寄存器: 在
entry_SYSCALL_64入口点保存用户态寄存器到pt_regs结构。 - 获取系统调用号: 从
rax寄存器读取系统调用号。 - 边界检查: 验证系统调用号是否在
[0, __NR_syscall_max]范围内。 - 查表: 通过
sys_call_table[rax]获取函数指针。 - 间接调用: 通过函数指针调用对应的处理函数,传递
pt_regs指针。
// arch/x86/entry/common.c
static __always_inline void do_syscall_64(struct pt_regs *regs)
{
unsigned long nr = regs->orig_ax;
if (nr < __NR_syscall_max) {
regs->ax = sys_call_table[nr](regs);
} else {
regs->ax = __x64_sys_ni_syscall(regs);
}
}
安全注意事项: 系统调用表是内核安全的关键点。现代内核通过
CONFIG_MITIGATION_RETPOLINE 和 CONFIG_MITIGATION_IBRS 等机制保护间接调用,防止 Spectre 变种攻击。
4.7 系统调用表与架构适配
x86_64 架构的系统调用表与其他架构存在显著差异:
| 特性 | x86_64 | x86_32 | ARM64 |
|---|---|---|---|
| 函数指针类型 | asmlinkage long (*)(const struct pt_regs *) |
asmlinkage long (*)(unsigned long, ...) |
long (*)(const struct pt_regs *) |
| 参数传递 | 通过 pt_regs 结构 |
通过寄存器直接传递 | 通过 pt_regs 结构 |
| 表名 | sys_call_table |
ia32_sys_call_table |
sys_call_table |
| 最大系统调用数 | ~450(取决于内核版本) | ~380 | ~450 |