x86中断驱动OS开发: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的掩码。
验证步骤
- 先注释掉
timer_install,确保IRQ重映射和IDT设置完成后系统能正常运行 - 逐步添加IRQ0的配置,先测试IRQ存根是否能正确触发(比如在handler_irq中打印调试信息)
- 最后添加PIT初始化,观察是否正常触发定时器中断
内容的提问来源于stack exchange,提问作者Safal Aryal

