2. 系统调用表(sys_call_table)概念

系统调用表是 Linux 内核中用于将系统调用号映射到对应内核服务函数的核心数据结构。它是一个函数指针数组,数组的索引就是系统调用号,数组元素是对应的内核处理函数地址。理解系统调用表的定义、内存位置和架构差异,是深入分析系统调用流程的基础。

核心要点:系统调用表本质上是一个 const void * const 类型的函数指针数组,每个条目指向一个 sys_xxx() 或 SyS_xxx() 形式的系统调用处理函数。

2.1 系统调用表的定义

在 Linux 内核源码中,系统调用表的定义位于架构相关的目录下。以 x86 架构为例,其定义在 arch/x86/kernel/syscall_64.c 中(对于 64 位系统):

// arch/x86/kernel/syscall_64.c (Linux 5.x)
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

关键点解析:

  • sys_call_ptr_t:函数指针类型,定义为 typedef long (*sys_call_ptr_t)(const struct pt_regs *);
  • __NR_syscall_max:最大系统调用号,由内核配置决定
  • sys_ni_syscall:未实现系统调用的默认处理函数(返回 -ENOSYS)
  • asm/syscalls_64.h:由脚本自动生成的头文件,包含所有系统调用函数的 extern 声明和表项初始化
提示:系统调用表使用 const 修饰,意味着在正常内核运行期间,表本身是只读的。但在某些调试或安全场景(如 ftrace、kprobe)中,内核会临时修改页表属性以动态替换表项。

下表展示了系统调用表的核心结构字段:

字段/属性 说明 典型值(x86_64)
数组类型 函数指针数组 sys_call_ptr_t []
数组长度 最大系统调用号 + 1 约 450(Linux 5.x)
元素大小 每个函数指针占用的字节数 8 字节(64 位)
默认处理函数 未实现系统调用的兜底函数 sys_ni_syscall
内存属性 只读(.rodata 段) 可被 CR0.WP 位控制

2.2 系统调用表在内存中的位置

系统调用表被放置在内核镜像的只读数据段(.rodata)中。其具体内存地址可以通过以下方式查看:

# 通过 System.map 查看系统调用表地址
grep sys_call_table /boot/System.map-$(uname -r)

# 输出示例(x86_64):
# ffffffff82000300 R sys_call_table

# 通过 /proc/kallsyms 查看(需要 root 权限)
sudo grep sys_call_table /proc/kallsyms

# 输出示例:
# ffffffff82000300 R sys_call_table

地址中的 R 标志表示该符号位于只读数据段。系统调用表在内存中的布局如下图所示:

系统调用表在内存中的布局 内核代码段 (.text) 内核只读数据段 (.rodata) sys_call_table[] 函数指针数组 [0] [1] [2] ... [N-1] [N] sys_read() sys_write() sys_open() 系统调用表位于 .rodata 段,数组索引 = 系统调用号,元素 = 函数指针

2.3 架构差异:x86 vs arm64

x86 和 arm64 架构在系统调用表的实现上存在显著差异,主要体现在调用约定、表结构、系统调用号分配和入口处理流程上。下表对比了关键差异:

对比维度 x86(64 位) arm64(AArch64)
系统调用指令 syscall(64 位)
sysenter(32 位兼容)
SVC #0(Supervisor Call)
系统调用号传递 rax 寄存器 x8 寄存器
参数传递寄存器 rdi, rsi, rdx, r10, r8, r9 x0, x1, x2, x3, x4, x5
系统调用表定义文件 arch/x86/kernel/syscall_64.c arch/arm64/kernel/sys.c
表类型 sys_call_ptr_t(函数指针) syscall_fn_t(函数指针)
系统调用号范围 0 ~ 449(Linux 5.15) 0 ~ 445(Linux 5.15)
兼容层支持 ia32 兼容表(32 位系统调用) compat 表(32 位 AArch32 兼容)
入口函数 entry_SYSCALL_64 el0_svc
表查找方式 直接数组索引 直接数组索引

arm64 架构的系统调用表定义示例:

// arch/arm64/kernel/sys.c (Linux 5.x)
asmlinkage const syscall_fn_t sys_call_table[__NR_syscalls] = {
    [0 ... __NR_syscalls-1] = (syscall_fn_t)&sys_ni_syscall,
#include <asm/unistd.h>
};
架构差异注意:arm64 使用 x8 寄存器传递系统调用号,而 x86_64 使用 rax。此外,arm64 的系统调用号范围通常比 x86_64 略小,且 arm64 没有独立的 32 位兼容表(通过 AArch32 状态处理)。

下图展示了 x86 和 arm64 在系统调用表查找流程上的架构差异:

x86 vs arm64 系统调用表查找流程对比 x86_64 系统调用入口 从 rax 寄存器读取系统调用号 检查系统调用号是否超出 __NR_syscall_max sys_call_table[rax] 直接索引 跳转到对应 sys_xxx() 函数 arm64 系统调用入口 从 x8 寄存器读取系统调用号 检查系统调用号是否超出 __NR_syscalls sys_call_table[x8] 直接索引 跳转到对应 sys_xxx() 函数 关键架构差异总结 1. 系统调用号来源不同:x86 使用 rax,arm64 使用 x8 2. 参数传递寄存器不同:x86 使用 rdi/rsi/rdx/r10/r8/r9,arm64 使用 x0~x5 3. 入口函数名不同:x86 为 entry_SYSCALL_64,arm64 为 el0_svc

2.4 系统调用表的查找机制

无论是 x86 还是 arm64,系统调用表的查找都遵循以下通用流程:

  1. 保存上下文:进入内核态后,保存用户态寄存器状态到 pt_regs 结构
  2. 提取系统调用号:从特定寄存器(x86: rax, arm64: x8)读取系统调用号
  3. 合法性检查:验证系统调用号是否在有效范围内(0 ~ NR_syscalls-1)
  4. 查表索引:以系统调用号为索引,从 sys_call_table 中取出函数指针
  5. 间接调用:通过函数指针调用对应的系统调用处理函数
  6. 结果返回:将返回值写入指定寄存器(x86: rax, arm64: x0),恢复上下文并返回用户态
性能优化:现代内核使用 indirect_branch 和 retpoline 技术来缓解 Spectre v2 变种对间接调用的影响。系统调用表的间接调用也受到这些保护机制的影响。

系统调用表查找的核心代码路径(x86_64 汇编入口简化):

// arch/x86/entry/entry_64.S (简化)
ENTRY(entry_SYSCALL_64)
    swapgs
    movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
    /* 保存寄存器到 pt_regs */
    /* ... */
    movq    %rax, %rdi              /* 系统调用号作为第一个参数 */
    call    do_syscall_64           /* 调用 C 函数 */
    /* 恢复寄存器并返回 */
    /* ... */
END(entry_SYSCALL_64)

// do_syscall_64() 函数
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    struct thread_info *ti;
    /* ... */
    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
    }
}

上述代码中的 array_index_nospec() 是一个关键的安全宏,用于防止 Spectre v1 类型的边界检查绕过攻击,确保即使 CPU 预测执行错误,也不会访问越界的系统调用表条目。

调试技巧:可以通过 /proc/kallsyms 查看系统调用表的实际内存地址,然后使用 gdb 或 crash 工具直接读取该地址处的函数指针数组,验证系统调用号与函数的对应关系。