中断底层,exception_hand,手动保存寄存器入栈 esp、eflags、cs、eip、error code 最先自动入栈,和函数调用入栈稍有区别

你这段宏写得很棒,是在实现 异常/中断统一入口处理宏,非常适合操作系统内核!👏

下面我来详细讲解这段汇编宏 exception_handler 的核心逻辑、各部分的作用,帮你真正搞清楚它为什么这么写、哪些寄存器被保存/恢复、为什么 iret 是必须的👇


✅ 这段宏的作用是什么?

用一个宏模板,快速生成多个中断/异常处理入口函数(例如除零异常、页错误、非法指令等),并统一保存现场、调用 C 函数、恢复现场。


🧩 宏定义结构说明:

.macro exception_handler name num with_err_code
参数含义
name异常名,比如 divide, page_fault
num异常号,比如 0, 14
with_err_code是否有 error code(1 = 有,0 = 没有)

✅ 每个 handler 生成一个这样的函数名:

.global exception_handler_divide
exception_handler_divide:

这是你要挂接到 IDT(中断描述符表)里的真正入口。


🔒 现场保存部分解释:

🔹 pusha

pusha
  • 保存通用寄存器 eax, ecx, edx, ebx, esp, ebp, esi, edi
  • 顺序固定:依次压入 8 个通用寄存器的值

💡 保存当前运行上下文


🔹 error code 填充逻辑(有些异常没有 error code)

.if \with_err_code == 0
    push $0
.endif
  • 如果该异常没有 error code,就人为 push 一个 0
  • 这样后面处理函数就可以统一读取“异常号 + error code”

🔹 push $\num

push $\num
  • 把异常号也压栈,这样你可以在 C 函数中知道是哪个异常

🔹 再次保存段寄存器等

pushal      ; GCC 的命名,等同于 pusha
push %ds
push %es
push %fs
push %gs

这些段寄存器不会被 pusha 保存,但在某些平台(特别是 32 位 x86)中断会切换段寄存器,为了保险,手动也保存。


🔹 保存当前 esp,调用 C 函数处理异常

push %esp
call do_handler_\name
pop %esp

你传的是 %esp —— 也就是“栈顶指针”,这是整个中断现场的栈帧指针,C 函数 do_handler_xxx() 就可以这样使用:

void do_handler_divide(uint32_t *esp) {
    regs_t *regs = (regs_t *)esp; // 栈帧结构体
}

🔁 恢复现场部分

pop %gs
pop %fs
pop %es
pop %ds
popal

依次恢复段寄存器 + 通用寄存器。


🔄 跳过异常号和错误码

add $(2*4), %esp  ; 跳过 num + err_code(2个DWORD)

你压了 2 个额外的值:异常号、错误码(即使没 error code,我们也补了 0),所以要把栈顶“抬”回去,恢复成 iret 要求的格式。


✅ 最重要的收尾:iret

iret

你不能用 ret,必须用 iret

指令功能
ret仅恢复 eip(返回地址)
iret恢复 eipcseflags,如果权限级变化,还会恢复 espss(即用户态 → 内核态切换时)

所以只有 iret 才能正确跳回中断前的位置,同时恢复执行上下文。


📊 总结流程图(简要)

[中断进入]
 ↓
push 自动保存(eip, cs, eflags, esp, ss...)
 ↓
exception_handler_xxx:
   pusha         ; 保存通用寄存器
   push $errcode ; 手动填补
   push $num     ; 异常号
   push 段寄存器
   push %esp     ; 传给 C 函数
   call do_handler_xxx
   pop %esp
   pop 段寄存器
   pop 通用寄存器
   add $8, %esp  ; 跳过异常号+错误码
   iret

🧠 面试总结金句:

中断/异常发生时,CPU 会自动保存最小上下文(eip/cs/eflags),必要时还有 esp/ss。为统一处理,我们额外保存所有通用寄存器和段寄存器,并通过 iret 恢复完整上下文,这正是中断处理函数宏的核心逻辑。


espeflagscseiperror code 这些寄存器啥时候一起出现?为啥有时你看不到它们?
这确实让很多人疑惑,其实这背后是 中断/异常触发后,CPU 自动入栈机制的结果


✅ 一句话总结:

当中断(包括硬件中断、异常、系统调用等)发生时,CPU 会自动把当前执行上
下文的一些关键寄存器压入栈中
,包括 EIPCSEFLAGS,在某些情况下还包括 ESPSSError Code


📌 什么时候会看到这些寄存器?

发生中断/异常/陷阱(trap)的时候

比如:

  • 硬件中断(键盘、时钟等)
  • 异常(除零、页错误、非法指令)
  • 系统调用(int 0x80 / syscall)
  • Ring3 → Ring0 的权限级别切换(比如用户态 → 内核态)

🔧 CPU 自动压栈机制(中断/异常时)

🔺 普通中断(CPL 没变,比如 Ring 0 → Ring 0)

CPU 自动压栈内容(从栈顶开始):

eflags
cs
eip
[error code] ← 仅某些异常有

🔻 权限级别切换(比如 Ring3 → Ring0)

CPU 自动多压两个寄存器(从用户态换内核栈):

ss      ← 用户态栈段选择子
esp     ← 用户态栈指针
eflags
cs
eip
[error code]

💡 这时候 CPU 会使用 TSS.esp0 和 ss0 自动切换到内核态栈!


✅ error code 是哪来的?

某些特定异常会自动压入一个 error code,它告诉你异常类型的具体错误原因,比如:

异常错误码?示例
页错误(Page Fault)✅ 有访问了不存在的页
段不存在(Segment Not Present)✅ 有无效段访问
除零异常(#DE)❌ 无不会压 error code
一般保护异常(#GP)✅ 有访问非法段

💡 举个例子:页错误异常触发时

触发 #PF,CPU 会自动压入以下内容(从高地址到低地址):

用户态:
+----------+
| SS       | ← 只有从用户态切内核态才有
| ESP      |
+----------+
| EFLAGS   |
| CS       |
| EIP      |
| ERROR CODE |
+----------+ ← 当前 ESP(内核栈)

你在中断处理函数中可以直接读取这些值,或者通过一个中断栈帧结构体访问它们。


✅ 为什么你能看到它们?

  • 如果你是在 OS 实验(如 xv6、Linux、裸机)中看到它们,是因为:

    中断/异常发生,CPU 自动压入了它们 → 然后你打印或调试栈,就能看到!

  • 比如中断处理函数这样定义:

void page_fault_handler(struct interrupt_frame *frame) {
    printf("EIP = %x\n", frame->eip);
    printf("CS  = %x\n", frame->cs);
    printf("ESP = %x\n", frame->esp);  // 如果有
    printf("Err = %x\n", frame->error_code);
}

✅ 中断门(Interrupt Gate)会自动设置这些:

中断描述符表(IDT)中定义中断门时,CPU知道该切哪种栈,是否跨特权级,并自动决定压哪些寄存器。


🎯 总结一句话:

当发生中断或异常时,CPU 会根据是否跨权限、是否需要错误码,自动将当前执行状态(如 eip、cs、eflags、esp、ss、error code)压入栈,供内核态处理后恢复上下文。这是中断/异常机制的底层核心。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值