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

x86中断驱动OS开发:IRQ0配置PIT触发三重故障求助

排查IRQ0(PIT)触发三重故障的问题

从你描述的情况来看,IDT和ISR已经能正常处理CPU异常(比如除以零),但配置IRQ0时触发三重故障,大概率是IRQ重映射未正确执行、IRQ存根的栈帧对齐错误或者IDT条目配置有误导致的。下面是具体的排查点和修复方案:

1. 先确认IRQ重映射(PIC初始化)是否正确

x86的两片PIC(主PIC 0x20,从PIC 0xA0)默认把IRQ0-7映射到INT8-15,这和CPU的异常中断(INT0-31)完全重叠——如果跳过重映射,IRQ0触发时会调用INT8的处理函数,而INT8原本是双精度浮点异常,没有正确处理逻辑就会直接炸锅。

正确的PIC重映射代码示例:

#define PIC1_CMD 0x20
#define PIC1_DATA 0x21
#define PIC2_CMD 0xA0
#define PIC2_DATA 0xA1

// 简单的IO等待,给PIC足够处理时间
static void io_wait(void) {
    outb(0x80, 0);
}

void pic_remap(void) {
    // 保存当前中断掩码,方便后续恢复
    uint8_t mask1 = inb(PIC1_DATA);
    uint8_t mask2 = inb(PIC2_DATA);

    // ICW1:初始化PIC,边沿触发,级联模式
    outb(PIC1_CMD, 0x11);
    io_wait();
    outb(PIC2_CMD, 0x11);
    io_wait();

    // ICW2:把主PIC起始中断号设为32,从PIC设为40(避开异常中断范围)
    outb(PIC1_DATA, 32);
    io_wait();
    outb(PIC2_DATA, 40);
    io_wait();

    // ICW3:告知主PIC从PIC连在IRQ2,从PIC告知自身身份
    outb(PIC1_DATA, 0x04);
    io_wait();
    outb(PIC2_DATA, 0x02);
    io_wait();

    // ICW4:启用8086模式
    outb(PIC1_DATA, 0x01);
    io_wait();
    outb(PIC2_DATA, 0x01);
    io_wait();

    // 恢复掩码(测试阶段也可以直接设为0,允许所有IRQ)
    outb(PIC1_DATA, mask1);
    outb(PIC2_DATA, mask2);
}

2. 检查IRQ存根的栈帧是否对齐

ISR里有些异常会自动压入错误码,但IRQ触发时不会。如果你的regs_t结构体包含err_code字段,那IRQ存根必须手动压入一个0作为占位错误码——否则handler_irq收到的寄存器结构会完全错位,访问错误内存地址直接触发三重故障。

正确的IRQ0存根示例(irq.asm):

global irq0
irq0:
    cli                     ; 先关中断
    push byte 0             ; 压入占位错误码(IRQ0无原生错误码)
    push byte 32            ; 压入重映射后的中断号(对应IRQ0)
    jmp irq_common_stub     ; 跳转到通用处理逻辑

; 通用IRQ存根,负责保存寄存器并调用C处理函数
irq_common_stub:
    pusha                   ; 保存所有通用寄存器
    push ds
    push es
    push fs
    push gs
    mov ax, 0x10            ; 加载内核数据段选择子(根据你的GDT调整)
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov eax, esp            ; 把寄存器结构体指针传给C函数
    push eax
    call handler_irq
    pop eax
    pop gs
    pop fs
    pop es
    pop ds
    popa                    ; 恢复寄存器
    add esp, 8              ; 弹出占位错误码和中断号
    sti                     ; 开中断
    iret                    ; 返回

所有IRQ存根(irq0-irq15)都要遵循这个逻辑:无错误码的IRQ必须push 0,再push对应的中断号(32-47)。

3. 检查IDT中IRQ条目的配置

在irq_install函数里,设置IDT条目时要注意三个关键点:

  • 中断号必须是重映射后的INT32-47(对应IRQ0-15)
  • 选择子sel是正确的内核代码段选择子(比如0x08,根据你的GDT配置来)
  • 类型属性tattr设为0x8E(32位中断门,特权级0,存在)

正确的IDT条目设置示例:

void irq_install() {
    pic_remap(); // 必须先执行PIC重映射!

    // 依次设置IRQ0-15对应的IDT条目
    idt_setentry(32, (uint32_t)irq0, 0x08, 0x8E);
    idt_setentry(33, (uint32_t)irq1, 0x08, 0x8E);
    // ... 继续设置irq2到irq15对应的条目(中断号34-47)

    // 开启IRQ0的中断(清除主PIC的IRQ0掩码)
    outb(PIC1_DATA, inb(PIC1_DATA) & ~0x01);
}

4. 修复IDT基地址的赋值问题

你的idt_install函数里,idtr.base = &idt;存在类型隐患:&idt是指向数组的指针(类型idtdesc_t (*)[256]),而idtr.base是uint32_t,虽然32位系统下地址值相同,但强制转换不明确可能导致编译警告或潜在问题。建议改成:

idtr.base = (uint32_t)idt; // 数组名本身就是首元素的地址

或者更明确的:

idtr.base = (uint32_t)&idt[0];

5. 确认PIT初始化逻辑正确

在timer_install函数里,初始化PIT时要发送正确的命令字和计数值:

void timer_install(uint32_t freq) {
    uint32_t divisor = 1193180 / freq; // PIT基础频率是1193180Hz

    // 发送命令字:通道0,低8位+高8位,模式3(方波)
    outb(0x43, 0x36);
    io_wait();
    // 发送低8位和高8位除数
    outb(0x40, divisor & 0xFF);
    io_wait();
    outb(0x40, (divisor >> 8) & 0xFF);
    io_wait();
}

注意:初始化PIT前必须完成IRQ重映射和IDT条目设置,并且已经开启了IRQ0的掩码。

验证步骤

  1. 先注释掉timer_install,确保IRQ重映射和IDT设置完成后系统能正常运行
  2. 逐步添加IRQ0的配置,先测试IRQ存根是否能正确触发(比如在handler_irq中打印调试信息)
  3. 最后添加PIT初始化,观察是否正常触发定时器中断

内容的提问来源于stack exchange,提问作者Safal Aryal

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:44:28