3. 系统调用参数传递机制:参数寄存器约定、参数传递路径、参数类型与大小限制
系统调用是用户态应用程序与内核交互的唯一合法入口。理解参数如何在用户态与内核态之间传递,是进行参数校验的前提。本章深入剖析 x86-64 架构下 Linux 系统调用的参数传递机制,涵盖寄存器约定、传递路径以及类型与大小限制。
3.1 参数寄存器约定
在 x86-64 架构中,系统调用参数通过寄存器传递,而非栈。这种设计减少了内存访问延迟,提升了调用效率。Linux 内核遵循特定的寄存器约定,将参数按顺序放入固定寄存器。
| 参数序号 | 寄存器 | 用途说明 |
|---|---|---|
| 系统调用号 | rax |
标识具体的系统调用(如 __NR_read) |
| 第 1 个参数 | rdi |
通用参数传递 |
| 第 2 个参数 | rsi |
通用参数传递 |
| 第 3 个参数 | rdx |
通用参数传递 |
| 第 4 个参数 | r10 |
替代 rcx(rcx 被 syscall 指令用于保存返回地址) |
| 第 5 个参数 | r8 |
通用参数传递 |
| 第 6 个参数 | r9 |
通用参数传递 |
| 返回值 | rax |
系统调用返回结果或错误码 |
关键点: 与函数调用约定不同,系统调用使用
r10 而非 rcx 作为第 4 个参数寄存器。这是因为 syscall 指令会隐式使用 rcx 保存返回地址,若继续使用 rcx 传递参数会导致数据被覆盖。
以下 SVG 展示了系统调用参数在寄存器中的映射关系:
3.2 参数传递路径
系统调用参数从用户态到内核态的传递路径,涉及模式切换、寄存器保存与内核栈操作。理解这一路径有助于定位参数校验的插入点。
- 用户态准备: 应用程序将参数填入约定寄存器,并将系统调用号存入
rax,随后执行syscall指令。 - 模式切换: CPU 切换到内核态,将
rcx(返回地址)和r11(RFLAGS)保存到内核栈,并跳转到入口点entry_SYSCALL_64。 - 寄存器保存: 内核入口代码将用户态寄存器(包括参数寄存器)压入内核栈上的
pt_regs结构体。 - 参数提取: 内核从
pt_regs中读取参数,传递给具体的系统调用处理函数。 - 校验与处理: 处理函数执行参数校验,然后完成实际功能。
以下 SVG 展示了参数传递的完整路径:
3.3 参数类型与大小限制
系统调用参数本质上是 64 位无符号整数(unsigned long),但内核会根据具体系统调用的语义,对参数进行类型解释和大小限制。理解这些限制是编写正确校验逻辑的基础。
3.3.1 基本类型映射
| 用户态类型 | 内核接收类型 | 大小(位) | 校验要点 |
|---|---|---|---|
int |
long |
32 → 64 | 符号扩展可能导致意外值,需检查范围 |
unsigned int |
unsigned long |
32 → 64 | 零扩展,高位可能包含无效数据 |
void * |
unsigned long |
64 | 必须校验指针合法性(非空、可访问) |
size_t |
unsigned long |
64 | 防止整数溢出,检查是否超过内核限制 |
struct * |
unsigned long |
64 | 需使用 copy_from_user 安全拷贝 |
3.3.2 大小限制与校验策略
- 整数溢出: 用户提供的
size_t参数可能为 0 或极大值,导致内核缓冲区溢出。校验时必须检查size > 0且size <= max_size。 - 指针范围: 用户态指针必须位于用户地址空间(通常为
0x0000000000000000到0x00007fffffffffff),且不能指向内核空间。 - 对齐要求: 某些系统调用要求参数对齐(如
mmap的地址对齐),未对齐参数应返回-EINVAL。 - 负值处理: 用户态传递的
int类型负值,在寄存器中表现为高位为 1 的 64 位值,内核需正确识别并拒绝。
校验示例: 以下代码展示了对
write 系统调用参数的基本校验逻辑。
// 简化的 write 系统调用参数校验
ssize_t sys_write(unsigned int fd, const char __user *buf, size_t count) {
// 1. 检查 fd 范围
if (fd >= NR_OPEN_MAX)
return -EBADF;
// 2. 检查 buf 指针合法性
if (!buf || !access_ok(VERIFY_READ, buf, count))
return -EFAULT;
// 3. 检查 count 大小限制
if (count == 0 || count > MAX_RW_COUNT)
return -EINVAL;
// 4. 检查 count 是否导致整数溢出(与文件偏移结合时)
if (count > (size_t)-1 - offset)
return -EFBIG;
// 通过校验,继续执行实际写入操作
// ...
}
3.4 参数校验的常见陷阱
安全警告: 以下陷阱是内核模块开发中常见的参数校验漏洞,务必在代码审查中重点检查。
- TOCTOU 竞争: 在第一次校验和第二次使用之间,用户态可能修改参数指向的内存内容。解决方案:使用
copy_from_user将数据拷贝到内核缓冲区后再校验。 - 符号扩展混淆: 用户传递
-1作为int,寄存器中为0xFFFFFFFFFFFFFFFF,若内核将其视为unsigned long进行比较,可能绕过大小检查。 - 指针截断: 在 32 位兼容层中,用户态指针为 32 位,但内核以 64 位处理,高位可能包含残留数据,需显式掩码。
- 空指针解引用: 用户传递
NULL指针,内核在未校验的情况下直接使用,导致内核崩溃。
以下 SVG 总结了参数校验的常见陷阱与应对策略: