You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自研操作系统配置IDT触发中断后循环出现一般保护错误

自研OS IDT首次中断后循环触发GPF问题修复

问题根因在汇编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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 10:30:53