Debugging: The Reboot Problem
Symptoms
After fixing the interrupt storm, a new problem appeared:
- Kernel boots
- Shows welcome message
- Keyboard works briefly
- System reboots
- SeaBIOS screen appears
- Cycle repeats every few seconds
This meant the kernel was running — interrupts were working — but something was causing it to triple-fault, which resets the CPU.
Diagnosing
Added incremental diagnostic output to trace exactly where the system was getting to:
#![allow(unused)]
fn main() {
println!("Step 1: Initializing IDT...");
idt::init();
println!("Step 2: Remapping PIC...");
pic::remap();
println!("Step 3: Verifying PIC masks...");
let (mask1, mask2) = pic::read_masks();
println!(" -> PIC1 mask: {:08b}", mask1);
println!(" -> PIC2 mask: {:08b}", mask2);
println!("Step 4: Enabling interrupts...");
idt::enable_interrupts();
}
Output:
PIC1 mask: 11111101 ← Correct! Only IRQ1 enabled
PIC2 mask: 11111111 ← Correct! All slave IRQs masked
But it still rebooted.
The Root Cause: The Timer
Even with the correct mask, the timer (IRQ0) was occasionally firing. Here’s why:
When the PIC is first powered on or reset, it has its own internal state. During the remapping sequence, there’s a window where the mask register is being written. If the timer fires in that window, it might sneak through.
The sequence was:
- Keyboard interrupt (IRQ1) fires → handler runs → counter increments (proof: we saw this working)
- Timer interrupt (IRQ0) fires → CPU looks up IDT entry 32
- IDT entry 32 was… not set. Empty. Zero.
- CPU tries to call address 0x0000 → invalid memory → general protection fault
- GPF handler wasn’t installed yet → double fault
- Double fault handler not installed → triple fault
- CPU resets → reboot
The Fix: Handle Every Interrupt
#![allow(unused)]
fn main() {
// Exception handlers for CPU faults
IDT.entries[0].set_handler(divide_by_zero_handler);
IDT.entries[6].set_handler(invalid_opcode_handler);
IDT.entries[8].set_handler(double_fault_handler);
IDT.entries[13].set_handler(general_protection_fault_handler);
IDT.entries[14].set_handler(page_fault_handler);
// DEFAULT HANDLER FOR EVERYTHING ELSE — this is the fix
for i in 1..256 {
if i != 0 && i != 6 && i != 8 && i != 13 && i != 14 && i != 33 {
IDT.entries[i].set_handler(default_interrupt_handler);
}
}
IDT.entries[33].set_handler(keyboard_interrupt_handler);
}
Now when the timer fires:
- Timer interrupt (IRQ0) → IDT entry 32 →
default_interrupt_handler - Handler displays “UNHANDLED INTERRUPT!” in red
- System halts gracefully
No more triple fault. No more reboot.
Key Insight
Fill the entire IDT. You don’t know what interrupts will fire — spurious IRQs, timer overflow, hardware sending unexpected signals. Every empty IDT slot is a potential triple-fault waiting to happen.
The cost of a default handler is minimal. The cost of leaving slots empty is mysterious reboots that are extremely hard to debug (because the machine resets before you can read the error).
Other Lessons from This Bug
- “The mask looks right” doesn’t mean an interrupt can’t slip through during initialisation
- Diagnostic printing is essential — adding explicit step-by-step output was what revealed the problem was after the PIC remapping, not during
- Start with the complete IDT — install default handlers for all 256 slots before handling specific ones