Focus: Interrupt Descriptor Table (IDT), 8259 dual-PIC hardware remapping (vectors 32–47), End of Interrupt (EOI) signaling, hardware exception dispatching, and interrupt-disabled locking to eliminate VGA console deadlocks.
| Subsystem Component | Source Location | Design Decision | Key Systems Benefit |
|---|---|---|---|
| IDT Initialization | src/interrupts.rs (L45) | Static InterruptDescriptorTable with lidt loader |
Installs ring-0 exception gates and hardware IRQ entrypoints. |
| Dual 8259 PIC | src/interrupts.rs (L27) | Remapped offsets: Master=32, Slave=40 | Separates hardware IRQs from x86 CPU exception vectors (0–31). |
| Deadlock-Free Print | src/vga_buffer.rs (L211) | interrupts::without_interrupts(|| WRITER.lock()) |
Eliminates re-entrant spinlock deadlocks when ISRs invoke println!. |
| Page Fault Handler | src/interrupts.rs (L161) | Reads CR2 register & decodes PageFaultErrorCode |
Provides forensic diagnostics for null pointer dereferences and protection faults. |
"Handling hardware interrupts and CPU exceptions on bare-metal x86 requires solving two distinct systems engineering challenges: hardware line collisions and concurrency deadlocks.
First, on IBM PC architecture, the 8259 Programmable Interrupt Controller routes hardware interrupts (like timer ticks and keyboard scancodes) to vectors 0 through 15 by default. But Intel reserved vectors 0 through 31 for CPU exceptions like Divide-by-Zero, Page Faults, and Double Faults. If an IRQ fires without remapping, a timer interrupt arrives as a Vector 8 Double Fault exception! In RustOS, I remapped the dual 8259 PICs using four initialization command words, moving master IRQs to vector 32 and slave IRQs to vector 40.
Second, we encountered a classic kernel concurrency trap: the interrupt deadlock. The VGA text-mode driver is protected by a global spin::Mutex<Writer>. If a thread is executing println! and holds the mutex lock, and the hardware timer interrupt fires right in the middle, the timer ISR might also try to log something via println!. Because the lock is already held by the interrupted thread, the ISR spins forever waiting for the lock to release, but the thread can never release the lock because the CPU is trapped spinning inside the ISR!
I eliminated this completely in RustOS by designing the print macros to wrap lock acquisition inside interrupts::without_interrupts. This temporarily clears the CPU interrupt flag (CLI), takes the spinlock, writes to the VGA memory buffer at 0xb8000, releases the lock, and restores interrupts (STI). This guarantees that interrupt handlers can never preempt a thread while it holds the console lock."
The 8259 PIC setup is defined in src/interrupts.rs:
// src/interrupts.rs
pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8; // 40
pub static PICS: spin::Mutex =
spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
The PIC initialization writes command words across I/O ports 0x20 (Master Control), 0x21 (Master Data), 0xA0 (Slave Control), and 0xA1 (Slave Data):
0x20 (32) for Master, 0x28 (40) for Slave.The IDT is populated with exception and interrupt gates in src/interrupts.rs:
// src/interrupts.rs
lazy_static! {
static ref IDT: InterruptDescriptorTable = {
let mut idt = InterruptDescriptorTable::new();
idt.breakpoint.set_handler_fn(breakpoint_handler);
unsafe {
idt.double_fault
.set_handler_fn(double_fault_handler)
.set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX);
}
idt[InterruptIndex::Timer.as_usize()].set_handler_fn(timer_interrupt_handler);
idt[InterruptIndex::Keyboard.as_usize()].set_handler_fn(keyboard_interrupt_handler);
idt.page_fault.set_handler_fn(page_fault_handler);
idt
};
}
Hardware interrupts must acknowledge receipt of the signal by writing an End of Interrupt (EOI) byte (0x20) to the PIC command port. If the interrupt originated from the slave PIC (IRQs 8–15), both slave and master must receive an EOI.
In src/vga_buffer.rs, printing is implemented without interrupt vulnerability:
// src/vga_buffer.rs
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
use x86_64::instructions::interrupts;
// Mutex lock acquired strictly inside interrupt-disabled closure
interrupts::without_interrupts(|| {
WRITER.lock().write_fmt(args).unwrap();
});
}
The without_interrupts helper reads the current RFLAGS register. If interrupts were enabled, it executes cli to disable them, runs the closure, and then re-enables interrupts (sti) if they were originally active. This preserves interrupt state even when nested.
During initial keyboard driver development, typing the first key printed the character to screen, but the kernel completely froze on all subsequent keystrokes and timer ticks stopped advancing.
Root Cause: The keyboard handler read the scancode from port 0x60 but returned without notifying the PIC via PICS.lock().notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8()). The 8259 PIC maintains an In-Service Register (ISR). Until the CPU sends the EOI command, the PIC refuses to forward any further interrupts of equal or lower priority on the cascade line.
Fix: Ensured every interrupt handler sends an EOI before returning or context switching.
Hardware interrupts and exception handling are verified by executing the unit test suite:
cargo test --test basic_boot
Output:
Running 1 tests
test test_println ... [ok]
1 passed; 0 failed
Furthermore, triggering an intentional software breakpoint via x86_64::instructions::interrupts::int3(); produces the expected stack frame diagnostic over the serial port:
EXCEPTION: BREAKPOINT
InterruptStackFrame {
instruction_pointer: VirtAddr(0x205a10),
code_segment: 0x8,
cpu_flags: 0x246,
stack_pointer: VirtAddr(0x444444458f20),
stack_segment: 0x0,
}
Tactical Answer: "The IBM PC BIOS originally wired the PIC to emit interrupt vectors 0x08–0x0F for IRQs 0–7. However, on protected and long-mode x86 processors, Intel designated vectors 0x00–0x1F as CPU architecture exceptions (e.g., 0x08 is Double Fault, 0x0D is General Protection Fault, 0x0E is Page Fault). If left unmapped, hardware timer ticks on IRQ 0 trigger Vector 8, corrupting the exception pipeline. Remapping offsets Master to 32 and Slave to 40 moves hardware IRQs safely into vectors 32–47."
Tactical Answer: "An EOI tells the PIC that the current interrupt service routine has finished processing, clearing the corresponding bit in the PIC's In-Service Register (ISR) and allowing lower-priority interrupts to trigger. If you forget to send it, the PIC blocks all subsequent interrupts on that line and any lower-priority lines. If you send it too early before finishing critical state updates, the same device can fire an immediate re-entrant interrupt, overflowing the kernel stack."