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)

注册过程的核心步骤:

  1. 定义中断向量号: 宏 IA32_SYSCALL_VECTOR 定义为 0x80。
  2. 设置门描述符: 使用 set_system_intr_gate 在 IDT 中写入一个陷阱门(trap gate),特权级 DPL 设置为 3,允许用户态程序触发。
  3. 关联入口函数: 门描述符的段选择子指向内核代码段,偏移量指向 entry_INT80_32 函数的起始地址。
中断向量 0x80 门描述符关键字段
字段 值 说明
向量号 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 入口函数核心步骤

  1. 保存用户态寄存器: 将用户态的通用寄存器(eax, ebx, ecx, edx, esi, edi, ebp)、段寄存器(ds, es, fs)以及返回地址压入内核栈。
  2. 切换内核栈: 从 TSS(任务状态段)中加载内核栈指针(ss0:esp0),完成栈的切换。
  3. 设置内核段寄存器: 将 ds, es, fs 设置为内核数据段选择子。
  4. 获取系统调用号: 从保存的 eax 寄存器中提取系统调用号。
  5. 参数传递: 将 ebx, ecx, edx, esi, edi, ebp 作为前 6 个参数传递给后续的 C 函数。
  6. 调用 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。
int 0x80 软中断入口:用户态到内核态切换流程 用户态 (Ring 3) 执行 int 0x80 指令 系统调用号 → eax 参数 → ebx, ecx, edx, esi, edi, ebp CPU 硬件自动操作 从 TSS 加载 ss0:esp0 保存 ss/esp, eflags, cs, eip 特权级切换 Ring 3 → Ring 0 内核态 (Ring 0) 跳转到 entry_INT80_32 entry_INT80_32 内部步骤 ① 保存用户态通用寄存器 (pushl eax, ecx, edx, ebx, ebp, esi, edi) ② 设置内核段寄存器 (ds, es, fs → __KERNEL_DS) ③ 调用 do_int80_syscall_32(pt_regs) do_int80_syscall_32 从 eax 提取系统调用号 查找 sys_call_table 并执行具体函数 RESTORE_REGS + iret 返回用户态

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 字段,确保特权级正确降级。