2. 内核异常处理机制
2.1 x86 架构异常向量表
x86 架构定义了 256 个中断/异常向量(0~255),其中前 32 个(0~31)为 保留异常 和 NMI,由 Intel/AMD 硬编码。内核启动时通过 set_intr_gate / set_trap_gate 等宏将每个向量与对应的 入口函数 绑定。
| 向量号 | 异常名称 | 类型 | 错误码 | 内核入口符号 |
|---|---|---|---|---|
| 0 | #DE — 除法错误 | Fault | 无 | divide_error |
| 1 | #DB — 调试异常 | Fault/Trap | 无 | debug |
| 3 | #BP — 断点 | Trap | 无 | int3 |
| 6 | #UD — 无效操作码 | Fault | 无 | invalid_op |
| 8 | #DF — 双重故障 | Abort | 有(0) | double_fault |
| 13 | #GP — 通用保护 | Fault | 有 | general_protection |
| 14 | #PF — 页错误 | Fault | 有 | page_fault |
关键区分:Fault 异常返回后重新执行触发指令;Trap 异常返回后执行下一条指令;Abort 不返回,系统通常直接 panic。
2.2 异常处理流程全景
当 CPU 检测到异常时,硬件自动完成以下步骤:
- 压栈:SS、RSP、RFLAGS、CS、RIP,若异常有错误码则额外压入。
- 查 IDT:根据向量号从中断描述符表(IDT)中取出门描述符。
- 跳转:加载 CS:RIP 到对应的内核入口函数(如
page_fault)。 - 保存现场:入口函数调用
error_entry/paranoid_entry保存所有通用寄存器。 - 调用 C 处理函数:如
do_page_fault、do_general_protection。 - 恢复与返回:若可修复则恢复现场并
iretq;否则进入die()或panic()。
2.3 do_trap 函数:异常分发中枢
do_trap 是大多数 Fault/Trap 类异常(#UD、#NM、#AC 等)的 C 语言入口。其核心逻辑:
// arch/x86/kernel/traps.c
static void do_trap(int trapnr, const char *str, struct pt_regs *regs,
long error_code, int sicode, void __user *addr)
{
struct task_struct *tsk = current;
if (!user_mode(regs)) {
// 内核态异常 → 直接 die()
die(str, regs, error_code);
return;
}
// 用户态异常 → 发送信号
if (show_unhandled_signals && unhandled_signal(tsk, SIGSEGV))
pr_info_ratelimited("%s[%d] trap %s ip:%lx sp:%lx error:%lx",
tsk->comm, task_pid_nr(tsk), str,
regs->ip, regs->sp, error_code);
force_sig_fault(sicode, addr ? : (void __user *)regs->ip, tsk);
}
内核态 vs 用户态分流:若异常发生在内核态(
user_mode(regs)==false),说明内核自身触发了异常,属于严重故障,直接调用 die() 输出 Oops 信息;若发生在用户态,则向当前进程发送信号(如 SIGSEGV、SIGILL),由用户态处理。
2.4 die() 函数:Oops 的诞生地
die() 是内核异常时输出 Oops 信息 的核心函数。它负责:
- 打印异常描述:异常名称、错误码、CPU 编号。
- 打印寄存器现场:调用
__show_regs()输出所有通用寄存器、段寄存器、RFLAGS、RSP、RIP。 - 打印栈回溯:调用
show_stack()输出内核调用栈。 - 打印模块信息:若异常发生在内核模块中,输出模块名、偏移、符号。
- 触发 kexec/kdump:若配置了
panic_on_oops,则调用panic()触发 kdump。
// arch/x86/kernel/dumpstack.c
void die(const char *str, struct pt_regs *regs, long err)
{
unsigned long flags;
raw_spin_lock_irqsave(&die_lock, flags);
console_verbose(); // 提升控制台级别
bust_spinlocks(1); // 解锁自旋锁死锁
pr_emerg("%s: %04lx [#%d] SMP\n", str, err & 0xffff, ++die_counter);
__show_regs(regs, 0); // 打印寄存器
show_stack(NULL, NULL); // 打印栈回溯
bust_spinlocks(0);
add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);
raw_spin_unlock_irqrestore(&die_lock, flags);
if (panic_on_oops)
panic("Fatal exception");
if (in_interrupt())
panic("Fatal exception in interrupt");
do_exit(SIGSEGV); // 杀死当前进程
}
注意:
die() 使用 die_lock 自旋锁确保多 CPU 同时异常时串行输出,避免 Oops 信息交错。但这也意味着如果持有 die_lock 的 CPU 死锁,其他 CPU 的异常将无法输出。
2.5 异常处理关键数据结构
| 数据结构 | 作用 | 关键字段 |
|---|---|---|
struct pt_regs |
保存异常时的 CPU 寄存器现场 | bx, cx, dx, si, di, bp, ax, ds, es, fs, gs, orig_ax, ip, cs, flags, sp, ss |
struct desc_ptr |
IDT 基址与限长 | address, size |
struct gate_struct |
IDT 门描述符 | offset_low, segment, ist, type, dpl, offset_middle, offset_high |
struct exception_table_entry |
内核异常修复表(fixup) | insn, fixup |
2.6 异常处理中的特殊路径
- #DF 双重故障:当 CPU 在处理一个异常时又发生另一个异常,且两个异常无法嵌套处理时触发。内核为 #DF 分配独立的 IST(Interrupt Stack Table) 栈,防止栈溢出。若 #DF 再次失败,CPU 进入 Shutdown 状态。
- #MC 机器检查:硬件错误(如内存 ECC 错误、总线错误),通过
do_machine_check()处理,可能触发panic()或仅记录错误。 - #NMI 不可屏蔽中断:硬件 watchdog、I/O 错误等触发,使用独立 IST 栈,处理函数
nmi_handler必须极其谨慎,避免死锁。
2.7 实战排查要点
- 查看异常向量号:Oops 信息中
#PF对应向量 14,#GP对应向量 13,快速定位异常类型。 - 检查错误码:#PF 错误码低 4 位标识(0=读/1=写/2=执行/4=用户态),#GP 错误码标识段选择子。
- 关注 RIP 地址:若 RIP 指向
0x0或0xffffffff...附近,通常是空指针解引用或函数指针被篡改。 - 利用
show_stack回溯:从栈回溯中找到最后一个内核函数调用,定位故障模块。 - 检查
panic_on_oops:若系统配置了panic_on_oops=1,则 Oops 会直接触发 panic 并 kdump,便于事后分析。
调试技巧:在
do_trap() 或 die() 中插入 dump_stack() 可以手动触发栈回溯,适合在开发阶段定位异常入口。