4. int 0x80 软中断入口:传统32位系统调用入口
在 x86 32 位架构中,int 0x80 是用户态程序发起系统调用的传统软件中断指令。当 CPU 执行 int 0x80 时,会触发中断向量 0x80 对应的中断处理程序,该处理程序在 Linux 内核中由 entry_INT80_32 函数实现。本节将深入分析中断向量 0x80 的注册机制、entry_INT80_32 函数的入口流程,以及从用户态切换到内核态的关键步骤。
4.1 中断向量 0x80 的注册
Linux 内核在初始化阶段,通过 trap_init() 函数将系统调用入口函数注册到中断描述符表(IDT)的第 0x80 号向量上。这一过程确保了当用户态程序执行 int 0x80 时,CPU 能够正确跳转到内核预置的处理程序。
关键注册函数:
set_system_intr_gate(IA32_SYSCALL_VECTOR, &entry_INT80_32)
注册过程的核心步骤:
- 定义中断向量号: 宏
IA32_SYSCALL_VECTOR定义为 0x80。 - 设置门描述符: 使用
set_system_intr_gate在 IDT 中写入一个陷阱门(trap gate),特权级 DPL 设置为 3,允许用户态程序触发。 - 关联入口函数: 门描述符的段选择子指向内核代码段,偏移量指向
entry_INT80_32函数的起始地址。
| 字段 | 值 | 说明 |
|---|---|---|
| 向量号 | 0x80 (128) | 传统 32 位系统调用专用向量 |
| 门类型 | 陷阱门 (Trap Gate) | 进入后自动关中断(IF 清零) |
| DPL | 3 | 允许 Ring 3 用户态程序调用 |
| 段选择子 | __KERNEL_CS | 内核代码段,特权级 0 |
| 偏移量 | entry_INT80_32 | 汇编入口函数地址 |
设计考量: 使用陷阱门而非中断门,是因为系统调用完成后需要返回用户态继续执行,陷阱门在进入时自动清除 IF 标志位,防止嵌套中断干扰系统调用流程,但保留了通过
iret 指令恢复现场的能力。
4.2 entry_INT80_32 函数入口流程
entry_INT80_32 是用汇编语言编写的入口函数,位于 arch/x86/entry/entry_32.S 文件中。它负责完成从用户态到内核态的完整上下文切换,并将控制权移交给系统调用分发函数 do_int80_syscall_32。
4.2.1 入口函数核心步骤
- 保存用户态寄存器: 将用户态的通用寄存器(eax, ebx, ecx, edx, esi, edi, ebp)、段寄存器(ds, es, fs)以及返回地址压入内核栈。
- 切换内核栈: 从 TSS(任务状态段)中加载内核栈指针(ss0:esp0),完成栈的切换。
- 设置内核段寄存器: 将 ds, es, fs 设置为内核数据段选择子。
- 获取系统调用号: 从保存的 eax 寄存器中提取系统调用号。
- 参数传递: 将 ebx, ecx, edx, esi, edi, ebp 作为前 6 个参数传递给后续的 C 函数。
- 调用 do_int80_syscall_32: 执行系统调用表查找和具体函数调用。
注意: 在 32 位系统中,系统调用参数最多支持 6 个,分别通过 ebx, ecx, edx, esi, edi, ebp 传递。超过 6 个参数的系统调用需要借助内存缓冲区传递。
4.2.2 入口汇编代码片段
ENTRY(entry_INT80_32)
/* 保存用户态寄存器到内核栈 */
pushl %eax /* 系统调用号 */
pushl %ecx /* 参数 2 */
pushl %edx /* 参数 3 */
pushl %ebx /* 参数 1 */
pushl %ebp /* 参数 6 */
pushl %esi /* 参数 5 */
pushl %edi /* 参数 4 */
/* 切换段寄存器 */
movl $(__KERNEL_DS), %edx
movl %edx, %ds
movl %edx, %es
movl %edx, %fs
/* 调用 C 函数处理系统调用 */
movl %esp, %eax /* 传递 pt_regs 指针 */
call do_int80_syscall_32
/* 恢复用户态寄存器并返回 */
RESTORE_REGS
iret
END(entry_INT80_32)
4.3 从用户态到内核态的切换过程
当用户态程序执行 int 0x80 时,CPU 硬件自动完成以下操作,随后跳转到 entry_INT80_32:
- 从 TSS 加载内核栈: CPU 读取 TR 寄存器指向的 TSS 段,获取 ss0(内核栈段)和 esp0(内核栈指针)。
- 保存用户态栈信息: 将用户态的 ss 和 esp 压入内核栈。
- 保存 EFLAGS、CS、EIP: 将当前 EFLAGS 寄存器、用户态代码段选择子 cs 以及返回地址 eip 依次压栈。
- 切换特权级: CPU 将当前特权级从 Ring 3 切换到 Ring 0。
- 跳转到入口函数: 从 IDT 门描述符中取出段选择子和偏移量,跳转到
entry_INT80_32。
4.4 内核栈布局与 pt_regs 结构
在 entry_INT80_32 中,所有用户态寄存器被顺序压入内核栈,形成一个 pt_regs 结构。该结构是内核访问用户态寄存器现场的标准方式,也是后续系统调用处理函数读取参数和返回结果的基础。
| 偏移量 | 寄存器 | 说明 |
|---|---|---|
| +0x24 | ss | 用户态栈段(CPU 自动压入) |
| +0x20 | esp | 用户态栈指针(CPU 自动压入) |
| +0x1c | eflags | 标志寄存器(CPU 自动压入) |
| +0x18 | cs | 用户态代码段(CPU 自动压入) |
| +0x14 | eip | 返回地址(CPU 自动压入) |
| +0x10 | eax | 系统调用号(手动压入) |
| +0x0c | ecx | 参数 2(手动压入) |
| +0x08 | edx | 参数 3(手动压入) |
| +0x04 | ebx | 参数 1(手动压入) |
| +0x00 | ebp | 参数 6(手动压入) |
| -0x04 | esi | 参数 5(手动压入) |
| -0x08 | edi | 参数 4(手动压入) |
关键点: 内核栈中的
pt_regs 结构指针被传递给 do_int80_syscall_32,C 函数通过该指针访问用户态传递的系统调用号和参数。系统调用返回值通过修改 pt_regs 中的 eax 字段传递回用户态。
4.5 返回路径:RESTORE_REGS 与 iret
系统调用处理完成后,entry_INT80_32 执行返回流程:
- RESTORE_REGS 宏: 从内核栈中弹出之前保存的用户态寄存器,恢复 edi, esi, ebp, ebx, edx, ecx, eax 的值。
- iret 指令: CPU 从内核栈中弹出 eip, cs, eflags, esp, ss,恢复用户态的执行环境,并将特权级从 Ring 0 切换回 Ring 3。
安全注意事项: 在返回用户态之前,内核必须确保所有内核态使用的段寄存器(ds, es, fs)已恢复为用户态的值,否则用户态程序可能因访问非法段而崩溃。此外,
iret 指令会检查 cs 选择子的 RPL 字段,确保特权级正确降级。