自研操作系统配置IDT触发中断后循环出现一般保护错误
问题根因在汇编ISR桩的栈操作和调用约定错误,和C逻辑无关,具体有三个致命bug:
1. push byte 导致栈隐式错位
32位x86模式下push操作默认按4字节对齐,push byte不会真的压入1字节,而是会把立即数符号扩展为32位压栈,你写的push byte 0、push byte %1不仅会导致压入的值高位带垃圾数据,还容易算错栈偏移。
2. C函数传参完全不符合cdecl约定
你定义的C处理函数接收Reg_registers_t类型参数,但汇编桩里直接call IDT_IsrHandler,没有按约定把参数地址压栈,C函数会把栈上的返回地址、段寄存器值当成结构体成员读写,函数返回时栈帧直接错位。
3. 手动开中断位置错误
你在iret之前加了sti,此时中断返回帧还没恢复,开中断后如果有新中断(包括第一次返回错误触发的GPF)会直接嵌套,把栈彻底打坏进入死循环。
修复步骤
第一处:修正ISR宏,统一压入双字
把idt.asm里的两个ISR宏改成压4字节双字,避免栈对齐问题,同时进入ISR先关中断防止嵌套:
%macro ISR_NOERRCODE 1 [GLOBAL Idt_Isr%1] Idt_Isr%1: cli push dword 0 ; 无错误码的ISR手动压0占位,统一栈结构 push dword %1 ; 压入中断号,双字对齐 jmp isr_common_stub %endmacro %macro ISR_ERRCODE 1 [GLOBAL Idt_Isr%1] Idt_Isr%1: cli push dword %1 ; CPU已自动压入4字节错误码,这里只压中断号 jmp isr_common_stub %endmacro
第二处:重写isr_common_stub,符合C调用约定
原来的stub没有正确传参,替换成下面的代码:
[extern IDT_IsrHandler] isr_common_stub: pusha ; 按edi/esi/ebp/esp/ebx/edx/ecx/eax顺序压入通用寄存器,和你定义的Reg_registers_t结构顺序匹配 mov ax, ds push eax ; 保存原数据段选择子 mov ax, 0x10 ; 加载内核数据段选择子 mov ds, ax mov es, ax mov fs, ax mov gs, ax push esp ; 此时esp指向整个保存的寄存器栈帧,作为参数传给C函数 call IDT_IsrHandler add esp, 4 ; cdecl约定调用方平衡栈,弹出传入的指针参数 pop eax ; 恢复原段寄存器 mov ds, ax mov es, ax mov fs, ax mov gs, ax popa ; 恢复通用寄存器 add esp, 8 ; 清掉栈上的中断号和错误码,回到CPU压入的中断返回帧 iret ; 中断返回,CPU自动恢复eip/cs/eflags/esp/ss,同时根据原eflags的IF位恢复中断状态,不需要手动sti
把原来stub末尾的sti删掉,手动提前开中断是导致GPF死循环的直接诱因之一
第三处:修改C处理函数的参数类型
现在汇编传的是寄存器结构的指针,把C代码里的IDT_IsrHandler改成接收指针参数,头文件里的声明也要同步改:
// 函数定义改成指针参数 void IDT_IsrHandler(Reg_registers_t* reg) { Terminal_WriteString("Interrupt executed: "); Terminal_WriteHex(reg->int_number); Terminal_WriteString("\n"); }
第四处:初始化IDT前先清零
你定义的Idt_idt是全局数组,没有显式初始化的项会带随机值,CPU访问到未初始化的IDT门会直接触发GPF,在Idt_Install开头加一行清零:
void Idt_Install() { memset(Idt_idt, 0, sizeof(Idt_idt)); // 先把整个IDT清零,未注册的中断门默认置为不存在 idt_reg.base = (uint32_t)&Idt_idt; idt_reg.limit = IDT_ENTRIES * sizeof(Idt_gate_t) - 1; __asm__ volatile ("lidtl (%0)" : : "r" (&idt_reg)); }
现象解释
第一次中断触发时,栈还没被破坏,C函数刚好能读到正确的中断号,看起来执行正常,但函数返回时栈已经错位,popa恢复的寄存器、返回地址都是错的,回到被中断的上下文后立刻触发一般保护错误(13号中断)。而13号中断是带错误码的类型,你原来的ISR桩栈操作本身有问题,进入GPF处理函数后再次栈错位,返回又触发新的GPF,就进入了无限死循环。
内容的提问来源于stack exchange,提问作者mig21 bis

