| name | osdev-interrupts |
| description | Interrupt handling for x86/x86-64 kernels. PIC 8259 programming, APIC/IO-APIC setup, ISR stubs, IRQ remapping, and CPU exception handlers. Use when writing interrupt handlers, remapping IRQs, configuring APIC, or handling exceptions. |
| origin | MCC |
Interrupt Handling for x86 Kernels
Complete reference for interrupt controllers, ISR construction, and CPU exception handling in bare-metal x86 and x86-64 kernels.
When to Use
- Setting up the IDT and writing ISR stubs
- Remapping PIC IRQs away from CPU exception vectors
- Enabling and configuring the Local APIC or IO-APIC
- Writing exception handlers (page fault, GPF, double fault)
- Debugging triple faults or unexpected reboots
- Transitioning from PIC to APIC for multiprocessor support
Interrupt Overview
The CPU has 256 interrupt vectors (0-255). Three sources feed into them:
| Source | Vectors | Trigger |
|---|
| CPU exceptions | 0-31 (reserved by Intel) | Internal faults, traps, aborts |
| Hardware IRQs | 32-47 (conventional PIC), or APIC-assigned | External device signals via PIC or APIC |
| Software interrupts | Any (commonly 0x80) | INT n instruction (syscalls) |
Why interrupts exist: Without them, the CPU must poll every device to check for events. Interrupts let hardware say "I need attention now" -- the CPU stops, handles the event, and resumes. This is fundamental to responsive OS design.
Interrupt flow (simplified):
- Event occurs (device signal, CPU fault, or
INT instruction)
- CPU pushes state onto the stack (SS, RSP, RFLAGS, CS, RIP; plus error code for some exceptions)
- CPU looks up the vector in the IDT
- CPU jumps to the handler address from the IDT entry
- Handler runs, sends EOI (for hardware IRQs), executes
iretq/iret
PIC 8259
The legacy Programmable Interrupt Controller. Two chips (master + slave) provide 15 usable IRQs.
Why Remapping Is Mandatory
The BIOS maps master PIC IRQs 0-7 to vectors 0x08-0x0F. This was an IBM design mistake -- vectors 0-31 are reserved for CPU exceptions. In protected mode, IRQ 0 (timer) collides with vector 8 (Double Fault). You cannot tell if vector 8 means "timer tick" or "your kernel just double-faulted." Remap IRQs to vectors 32+ immediately.
Initialization Sequence
The PIC requires a specific 4-step init via I/O ports. You must send all 4 ICWs (Initialization Command Words) in order:
void pic_remap(uint8_t offset1, uint8_t offset2) {
outb(0x20, 0x11);
io_wait();
outb(0xA0, 0x11);
io_wait();
outb(0x21, offset1);
io_wait();
outb(0xA1, offset2);
io_wait();
outb(0x21, 0x04);
io_wait();
outb(0xA1, 0x02);
io_wait();
outb(0x21, 0x01);
io_wait();
outb(0xA1, 0x01);
io_wait();
}
End of Interrupt (EOI)
After handling a hardware IRQ, you must tell the PIC you are done. If you forget, the PIC will never send that IRQ (or lower-priority ones) again.
void pic_send_eoi(uint8_t irq) {
if (irq >= 8)
outb(0xA0, 0x20);
outb(0x20, 0x20);
}
Spurious IRQs -- Why They Matter
A race condition in the PIC can produce fake IRQs (IRQ 7 on master, IRQ 15 on slave). If you send EOI for a spurious IRQ, you corrupt the PIC's in-service tracking. The fix: read the ISR register and check if the IRQ bit is actually set before sending EOI. For spurious IRQ 15, still send EOI to the master (it saw a real signal from the slave's cascade line).
See references/pic-programming.md for full port maps, masking, and spurious detection code.
APIC (Advanced PIC)
Why Upgrade from PIC
The PIC is single-processor only and limited to 15 IRQ lines. The APIC architecture provides:
- Per-CPU local APICs (required for SMP)
- 224 interrupt vectors (vs 15 IRQs)
- Inter-Processor Interrupts (IPIs) for cross-CPU signaling
- Built-in timer per core (no shared PIT resource contention)
Local APIC Enable Sequence
- Detect: CPUID with EAX=1, check EDX bit 9
- Find base address: Read MSR 0x1B (IA32_APIC_BASE), mask bits 12-35 for the physical page. Default is 0xFEE00000. Map this page as uncacheable in your page tables.
- Enable: Set bit 8 of the Spurious Interrupt Vector Register (offset 0xF0). Set the spurious vector to 0xFF (low 4 bits should be set on older CPUs).
- Disable PIC: Mask all PIC interrupts (outb 0xA1, 0xFF; outb 0x21, 0xFF) and remap it so spurious PIC IRQs land on unused vectors, not exceptions.
void apic_enable(void) {
uint32_t svr = apic_read(0xF0);
apic_write(0xF0, svr | 0x1FF);
}
APIC EOI
Simpler than PIC: write 0 to offset 0xB0. No need to check master/slave.
APIC Timer
Each Local APIC has a built-in timer. Modes: periodic (auto-reload), one-shot (single fire), TSC-deadline (newest CPUs). You must calibrate it against a known clock source (PIT or HPET) since the frequency varies per machine.
See references/apic-setup.md for register offsets, timer calibration, and IPI details.
IO-APIC
The IO-APIC replaces the PIC for routing external device IRQs to specific CPUs. It sits on the chipset (default MMIO at 0xFEC00000) and typically handles 24 inputs.
Redirection Table
Each IRQ input has a 64-bit redirection entry controlling:
- Vector (bits 0-7): which IDT entry fires
- Delivery mode (bits 8-10): Fixed, Lowest Priority, NMI, etc.
- Destination (bits 56-63): target CPU's APIC ID
- Mask (bit 16): disable this IRQ
- Trigger mode (bit 15): edge or level triggered
- Pin polarity (bit 13): active high or low
Discovery
Parse the ACPI MADT (Multiple APIC Description Table) to find IO-APIC base addresses, their GSI (Global System Interrupt) bases, and interrupt source overrides that remap ISA IRQs to different pins or polarities.
Indirect Register Access
IO-APIC uses two MMIO registers: IOREGSEL (base+0x00) for selecting the register index, and IOWIN (base+0x10) for reading/writing the value. Each 64-bit redirection entry requires two 32-bit accesses.
ISR Stub Pattern
An interrupt can fire between any two instructions. The handler must save the complete CPU state, or you will corrupt whatever code was running.
Annotated x86-64 ISR Stub
; ISR stub for interrupts that do NOT push an error code
isr_stub_no_err:
push 0 ; push dummy error code for uniform stack frame
push rax ; save all general-purpose registers
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
mov rdi, rsp ; pass pointer to saved registers as arg1
call interrupt_handler ; C handler
pop r15 ; restore all registers in reverse
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rbp
pop rbx
pop rdx
pop rcx
pop rax
add rsp, 8 ; remove error code
iretq ; return from interrupt
Why save everything: The interrupted code could be using any register. If your handler clobbers RAX, the code that was running before the interrupt silently gets a wrong value. This is a Heisenbug -- it manifests randomly depending on exactly when the interrupt fires.
Error code asymmetry: Some exceptions push an error code, some do not. Your stub macros must handle both cases so the stack frame is always uniform. Use a macro that pushes a dummy 0 for exceptions without error codes.
Exception Handling
Error Code Exceptions
Only these exceptions push an error code: #DF (8), #TS (10), #NP (11), #SS (12), #GP (13), #PF (14), #AC (17), #CP (21), #VC (29), #SX (30). Double Fault always pushes zero. All others push nothing -- your ISR stub must push a dummy value to keep the stack layout consistent.
Page Fault (#PF, Vector 14)
The most complex and most useful exception. CR2 holds the faulting virtual address. The error code tells you what happened:
| Bit | Name | Meaning when set |
|---|
| 0 | P | Protection violation (page was present) |
| 1 | W | Caused by a write |
| 2 | U | Occurred in user mode (CPL=3) |
| 3 | R | Reserved bit set in page table entry |
| 4 | I | Instruction fetch (NX violation) |
A page fault with P=0 means the page is not mapped -- this is your demand paging / mmap signal. P=1 means the page exists but access was denied (write to read-only, user accessing kernel page, etc.).
Double Fault (#DF, Vector 8)
Occurs when an exception fires while the CPU is already handling an exception, and the combination is unrecoverable. The most common cause: your kernel stack overflows, causing a page fault, but the page fault handler also needs the (now-broken) stack. You must use the IST (Interrupt Stack Table) mechanism in 64-bit mode to give #DF its own known-good stack. Without IST, a double fault from stack overflow causes a triple fault.
Triple Fault
When an exception occurs during the double fault handler, the CPU gives up and resets. There is no vector -- you just see a reboot. Triple faults mean your IDT, GDT, or TSS is corrupt, or your double fault handler's IST stack is bad.
See references/exception-list.md for the complete vector table with error code details.
Common Pitfalls
| Pitfall | Consequence | Fix |
|---|
| Forgetting to remap PIC | Timer IRQ looks like Double Fault | Remap to vectors 32-47 before enabling interrupts |
| Missing EOI | PIC/APIC stops sending that IRQ | Always send EOI at end of hardware IRQ handler |
| Not saving all registers in ISR | Random corruption of running code | Save/restore every GP register |
| Inconsistent stack frame for error code | Handler reads garbage | Use stub macros that normalize the frame |
| No IST for Double Fault | Stack overflow becomes triple fault | Configure TSS IST entry for vector 8 |
| Sending EOI for spurious PIC IRQ | Corrupts PIC in-service register | Check ISR bit before EOI |
| APIC MMIO not mapped uncacheable | APIC reads stale values | Map 0xFEE00000 page with cache-disable |
| Not disabling PIC when using APIC | Ghost PIC spurious interrupts hit exception vectors | Mask all PIC IRQs and remap before enabling APIC |
| Forgetting to read CR2 in #PF handler early | Nested interrupt overwrites CR2 | Read CR2 first thing in page fault handler |
Related Skills
osdev-toolchain -- cross-compiler setup, linker scripts, QEMU debugging
osdev-memory -- paging setup (needed for page fault handling)
osdev-boot -- GDT, IDT table setup, entering protected/long mode