Writing an OS Kernel from Scratch | Interrupt and Exception Handling — How the CPU Gets “Interrupted” by Peripherals
Your program is running, and suddenly you press a key on the keyboard — how does the operating system know about it? How does the CPU instantly switch from executing code to handling keyboard input? The answer is interrupts.
Interrupts are the core mechanism of hardware-software interaction: peripherals notify the CPU via interrupts “I have something to handle here,” the CPU pauses its current work, jumps to execute the corresponding Interrupt Service Routine (ISR), and returns after processing.
But interrupts aren’t just for hardware — the CPU itself can trigger Exceptions, such as division by zero, accessing invalid memory, illegal instructions, etc. These are also “interruptions,” but the source is internal to the CPU rather than external hardware.
Today, let’s figure out the complete chain of interrupt and exception handling.
1. Essential Differences Between Interrupts and Exceptions
Hardware Interrupts:
- Source: External hardware (keyboard, NIC, hard drive, timer, etc.)
- Trigger condition: Peripheral sends signal to CPU via interrupt pin (IRQ)
- Handling: Pause current task, execute ISR, resume after completion
CPU Exceptions:
- Source: CPU itself detects errors during instruction execution
- Trigger condition: Division by zero, out-of-bounds access, invalid opcode, page fault, etc.
- Handling: Recoverability depends on exception type (fault/trap/abort)
Classification of Interrupts and Exceptions:
Hardware Interrupts (Asynchronous)
├── Maskable Interrupt (IRQ) — responds when IF=1, ignores when IF=0
└── Non-Maskable Interrupt (NMI) — hardware failure, e.g. memory parity error
CPU Exceptions (Synchronous)
├── Fault — recoverable, can return to triggering instruction and re-execute
│ ├── #DE (Divide-by-Zero)
│ ├── #PF (Page Fault)
│ └── #GP (General Protection)
├── Trap — intentionally triggered, continues to next instruction after execution
│ ├── #DB (Debug Breakpoint)
│ └── #OF (Overflow)
└── Abort — unrecoverable, log error, terminate process
└── #DF (Double Fault)2. IDT: Interrupt Descriptor Table
x86 uses the IDT (Interrupt Descriptor Table) to describe the handling function for each interrupt/exception. Similar to GDT, the IDT is also a table, but each entry has a different format.
2.1 IDT Entry Format (Gate Descriptor)
IDT Gate Descriptor (8 bytes = 64 bits) format:
┌──────────────────────────────────────────────────────────────┐
│ Bit 63-48: Offset [31:16] (Handler address high 16 bits) │
│ Bit 47-40: Reserved / Selector (Code segment selector) │
│ Bit 39-36: Zero │
│ Bit 35-32: Gate Type │
│ Bit 31-16: Offset [15:0] (Handler address low 16 bits) │
│ Bit 15-8: Selector (Code segment selector) │
│ Bit 7-0: IST / Reserved │
└──────────────────────────────────────────────────────────────┘
Gate Type Values:
0x5E = 64-bit Interrupt Gate
0x6E = 64-bit Trap Gate
0x7E = 64-bit Task Gate (rarely used)
Difference between Interrupt Gate and Trap Gate:
Interrupt Gate: Automatically disables interrupts (IF=0) on jump, prevents nesting
Trap Gate: Keeps IF unchanged on jump, allows nested interrupts (but be careful)2.2 IDT Vector Assignment (Common Exceptions and Interrupts)
IDT Vector Allocation (x86 common mappings):
Vector Description Source Type
0 #DE — Divide-by-Zero Error CPU Fault
1 #DB — Debug Exception CPU Fault/Trap
2 NMI — Non-Maskable Interrupt External NMI
3 #BP — Breakpoint (INT3) CPU Trap
4 #OF — Overflow (INTO) CPU Trap
5 #BR — Bound Range Exceeded CPU Fault
6 #UD — Invalid Opcode CPU Fault
7 #NM — Device Not Available CPU Fault
8 #DF — Double Fault CPU Abort
10 #TS — Invalid TSS CPU Fault
11 #NP — Segment Not Present CPU Fault
12 #SS — Stack Segment Fault CPU Fault
13 #GP — General Protection Fault CPU Fault
14 #PF — Page Fault CPU Fault
16 #MF — x87 FPU Error CPU Fault
17 #AC — Alignment Check CPU Fault
18 #MC — Machine Check CPU Abort
19 #XM — SIMD Floating-Point Fault CPU Fault
20 #VE — Virtualization Exception CPU Fault
32-255 IRQ 0-223 (hardware interrupts) External Interrupt
IRQ 0 — PIT Timer
IRQ 1 — Keyboard
IRQ 3 — COM2
IRQ 4 — COM1
IRQ 8 — RTC
IRQ 12 — PS/2 Mouse
IRQ 14 — Primary IDE
IRQ 15 — Secondary IDE3. How Interrupts Propagate: From Hardware to IDT
When a peripheral triggers an interrupt, the CPU goes through the following steps:
Full interrupt response process:
1. Device triggers interrupt signal
└── Keyboard sends signal on IRQ 1
└── PIC (8259A or APIC) receives the signal
└── PIC converts IRQ number to vector number (e.g. IRQ 1 → Vector 33)
└── PIC sends signal to CPU's INTR pin
2. CPU checks IF flag (on x86: EFLAGS bit 9)
├── IF=1 → handle interrupt
└── IF=0 → ignore (except NMI)
3. CPU saves current execution state (hardware saves)
├── Push SS / RSP (current stack pointer)
├── Push RFLAGS
├── Push CS / RIP (return address)
└── Push error code (some exceptions push an error code)
4. CPU looks up IDT by vector number
└── IDT entry points to the handler
5. Jump to handler (Interrupt Service Routine)
├── Save remaining registers
├── Call specific handler logic
├── Send EOI (End of Interrupt) to PIC
└── Restore registers
6. IRETQ returns to original execution point4. Real-World Code: Setting Up IDT
Let’s look at a minimal IDT initialization code:
// Minimal IDT initialization (x86_64)
#define IDT_ENTRIES 256
struct idt_entry {
uint16_t offset_low; // Handler address low 16 bits
uint16_t selector; // Code segment selector (Kernel CS)
uint8_t ist; // Interrupt Stack Table offset
uint8_t type_attr; // Gate type + attributes
uint16_t offset_mid; // Handler address mid 16 bits
uint32_t offset_high; // Handler address high 32 bits
uint32_t zero; // Reserved
} __attribute__((packed));
struct idt_ptr {
uint16_t limit; // IDT size - 1
uint64_t base; // IDT base address
} __attribute__((packed));
struct idt_entry idt[IDT_ENTRIES];
struct idt_ptr idtp;
// Set IDT entry
void idt_set_entry(int vector, uint64_t handler, uint8_t type) {
idt[vector].offset_low = handler & 0xFFFF;
idt[vector].offset_mid = (handler >> 16) & 0xFFFF;
idt[vector].offset_high = (handler >> 32) & 0xFFFFFFFF;
idt[vector].selector = KERNEL_CS; // Kernel code segment
idt[vector].ist = 0; // No IST switching
idt[vector].type_attr = type | 0x80; // Present + Gate type
idt[vector].zero = 0;
}
// Load IDT
void idt_init() {
idtp.limit = sizeof(struct idt_entry) * IDT_ENTRIES - 1;
idtp.base = (uint64_t)&idt;
asm volatile("lidt %0" : : "m"(idtp));
}5. Interrupt Nesting and Reentrancy
5.1 Nested Interrupts
What happens if another interrupt arrives while handling one interrupt?
Scenario:
CPU executing user code
→ IRQ 1 (keyboard) arrives
→ Enter keyboard ISR
→ IRQ 3 (COM2) arrives
→ Should we handle it now, or wait?There are two strategies:
Interrupt Gate (IF=0) strategy:
- Automatically close interrupts, disable nested interrupts
- Ensure the current ISR runs uninterrupted
- But: The next interrupt is delayed, which may affect real-time performance
Trap Gate (IF unchanged) strategy:
- Keep IF unchanged, allow nested interrupts
- High-priority interrupts can be handled immediately
- But: Must ensure reentrancy — ISR is like a function called by itself
5.2 Reentrancy Requirements
Interrupt handlers that allow nesting must:
- Save and restore all used registers
- Use separate stacks to avoid stack corruption
- Lock shared data (use spinlock)
- Ensure all operations are atomic or access critical sections safely
6. Double Fault and Triple Fault
The most painful situation: an exception occurs while handling an exception.
#DF (Double Fault) Trigger Path:
Scenario: IDT entry for vector 0 (Divide by Zero) is not set up
Program executes DIV instruction
→ CPU tries to read IDT[0], finds it invalid
→ CPU triggers #GP (General Protection, vector 13)
→ CPU tries to read IDT[13], also invalid
→ CPU triggers #DF (Double Fault, vector 8)
→ If IDT[8] is also invalid...
→ TRIPLE FAULT: CPU reset!Triple Fault = CPU detects a double fault while trying to process a double fault. The CPU has no way out and directly triggers a restart.
Prevention: At minimum, set up valid IDT entries for:
-
DE (0), #GP (13), #DF (8)
- At least one hardware interrupt entry (for PIC timer)
7. Summary
Understanding the interrupt/exception mechanism is the foundation for mastering operating systems. Key takeaways:
- Interrupts are from external hardware, exceptions are from the CPU itself
- IDT is the mapping table from vector numbers to handlers
- Interrupt Gates disable nests, Trap Gates allow nests
- Double Fault is an exception in an exception; must set up at least the three key entries
- The first thing in an ISR is to save the scene, the last thing is to restore it
Next up: How system calls cross the Ring 3 ↔ Ring 0 boundary — stay tuned!
Comments