Loading content...
When an interrupt occurs with vector number 14 (a page fault), how does the CPU know where to find the page fault handler? When the timer fires on vector 32, how does the CPU locate the timer interrupt service routine?
The answer lies in interrupt vector tables—data structures that map interrupt numbers to handler addresses. In real mode, this is the simple Interrupt Vector Table (IVT). In protected and long mode, it's the more sophisticated Interrupt Descriptor Table (IDT).
These tables are among the first data structures initialized during boot. Without them, no interrupt can be handled—a single keystroke would crash the system. Understanding their structure is essential for OS development and hardware-level debugging.
By the end of this page, you will understand the complete structure of both the Real Mode IVT and Protected/Long Mode IDT. You'll learn descriptor formats, security attributes, and how to initialize these tables. You'll also understand the IDTR register and how the CPU locates these structures during interrupt handling.
In Real Mode (the mode x86 CPUs enter at boot), the interrupt system uses the simplest possible structure: the Interrupt Vector Table (IVT).
IVT Characteristics:
The BIOS initializes the IVT at boot with handlers for hardware interrupts and software services (INT 10h for video, INT 13h for disk, etc.).
| Offset | Size | Contents |
|---|---|---|
| 0x00 | 2 bytes | IP - Offset portion of handler address |
| 0x02 | 2 bytes | CS - Segment portion of handler address |
Calculating IVT Entry Address:
For interrupt vector n:
The handler's linear address = (CS × 16) + IP
Example:
Vector 8 (Timer interrupt):
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
; Real mode: Hooking interrupt vector 9 (keyboard); This demonstrates how DOS TSRs and viruses modify the IVT [BITS 16][ORG 0x100] main: ; Save the original INT 9 handler xor ax, ax mov es, ax ; ES = 0 (IVT segment) cli ; Disable interrupts during modification ; Read original vector from IVT[9] mov ax, [es:9*4] ; Original IP mov [original_ip], ax mov ax, [es:9*4+2] ; Original CS mov [original_cs], ax ; Install our handler mov word [es:9*4], new_keyboard_handler mov word [es:9*4+2], cs sti ; Re-enable interrupts ; Terminate and Stay Resident (keep handler in memory) mov dx, (end_resident - main + 0x100 + 15) / 16 mov ax, 0x3100 ; DOS TSR function int 0x21 new_keyboard_handler: push ax ; Read scan code from keyboard controller in al, 0x60 ; Do something with the keystroke ; (log it, filter it, modify it, etc.) pop ax ; Chain to original handler ; Simulate 'INT n' to call original pushf ; Push flags (original INT pushes this) db 0x9A ; FAR CALL opcodeoriginal_ip: dw 0original_cs: dw 0 iret ; Return from interrupt end_resident:The IVT has no protection. Any code can overwrite any vector, enabling DOS viruses to intercept all system calls and keystrokes. This lack of protection is one reason real mode is unsuitable for modern multitasking operating systems. Protected mode's IDT addresses this with privilege level checks.
Protected Mode (32-bit) and Long Mode (64-bit) replace the simple IVT with the Interrupt Descriptor Table (IDT). The IDT is far more sophisticated, supporting privilege levels, different gate types, and memory protection.
IDT vs IVT:
| Feature | Real Mode IVT | Protected/Long Mode IDT |
|---|---|---|
| Location | Fixed at 0x00000000 | Anywhere in memory (IDTR register) |
| Entry Size | 4 bytes | 8 bytes (32-bit) / 16 bytes (64-bit) |
| Entry Count | Fixed 256 | Up to 256 (can be shorter) |
| Entry Format | Simple FAR pointer | Descriptor with type, DPL, etc. |
| Protection | None | DPL field restricts who can call |
| Gate Types | N/A | Interrupt, Trap, Task (32-bit) |
| Segment Selector | CS value directly | Selector indexes into GDT/LDT |
The IDTR Register:
Unlike the IVT's fixed location, the IDT can be placed anywhere in memory. The CPU finds it using the IDTR (IDT Register), loaded with the LIDT instruction:
┌─────────────────────────────────────────┐
│ IDTR (10 bytes total) │
├─────────────────────────────────────────┤
│ Limit (16 bits) │ Base Address (64 bits)│
│ Size in bytes-1 │ Linear address of IDT │
└─────────────────────────────────────────┘
For 256 entries of 16 bytes each, limit = (256 × 16) - 1 = 4095 = 0xFFF
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// Setting up the IDT in a kernel // IDT entry structure (64-bit mode)struct idt_entry { uint16_t offset_low; // Handler offset bits 0-15 uint16_t selector; // Code segment selector in GDT uint8_t ist; // IST index (bits 0-2), reserved (bits 3-7) uint8_t type_attr; // Type (4 bits) and attributes (P, DPL) uint16_t offset_mid; // Handler offset bits 16-31 uint32_t offset_high; // Handler offset bits 32-63 uint32_t reserved; // Reserved, must be zero} __attribute__((packed)); // IDTR structurestruct idtr { uint16_t limit; // Size of IDT in bytes - 1 uint64_t base; // Virtual address of IDT} __attribute__((packed)); // Our IDT - 256 entriesstatic struct idt_entry idt[256] __attribute__((aligned(16)));static struct idtr idtr; // Create an IDT entryvoid set_idt_entry(uint8_t vector, void (*handler)(void), uint8_t type_attr, uint8_t ist) { uint64_t handler_addr = (uint64_t)handler; idt[vector].offset_low = handler_addr & 0xFFFF; idt[vector].offset_mid = (handler_addr >> 16) & 0xFFFF; idt[vector].offset_high = (handler_addr >> 32) & 0xFFFFFFFF; idt[vector].selector = KERNEL_CODE_SELECTOR; // Typically 0x08 idt[vector].ist = ist; idt[vector].type_attr = type_attr; idt[vector].reserved = 0;} // Type attribute values#define IDT_PRESENT (1 << 7) // Present bit#define IDT_DPL0 (0 << 5) // Ring 0#define IDT_DPL3 (3 << 5) // Ring 3#define IDT_INTERRUPT 0x0E // 64-bit interrupt gate#define IDT_TRAP 0x0F // 64-bit trap gate // Load the IDTvoid load_idt(void) { idtr.limit = sizeof(idt) - 1; // 4095 for 256 entries idtr.base = (uint64_t)&idt; // Use inline assembly to execute LIDT asm volatile ("lidt %0" :: "m"(idtr));} // Initialize all IDT entriesvoid init_idt(void) { // Clear all entries memset(idt, 0, sizeof(idt)); // CPU exceptions (vectors 0-31) set_idt_entry(0, isr_divide_error, IDT_PRESENT | IDT_INTERRUPT, 0); set_idt_entry(1, isr_debug, IDT_PRESENT | IDT_INTERRUPT, 3); // IST3 for debug set_idt_entry(2, isr_nmi, IDT_PRESENT | IDT_INTERRUPT, 2); // IST2 for NMI set_idt_entry(3, isr_breakpoint, IDT_PRESENT | IDT_DPL3 | IDT_TRAP, 0); // User callable set_idt_entry(6, isr_invalid_opcode,IDT_PRESENT | IDT_INTERRUPT, 0); set_idt_entry(8, isr_double_fault, IDT_PRESENT | IDT_INTERRUPT, 1); // IST1 for double fault set_idt_entry(13, isr_gpf, IDT_PRESENT | IDT_INTERRUPT, 0); set_idt_entry(14, isr_page_fault, IDT_PRESENT | IDT_INTERRUPT, 0); // ... more exceptions // Hardware interrupts (vectors 32-47 for remapped PIC) set_idt_entry(32, isr_timer, IDT_PRESENT | IDT_INTERRUPT, 0); set_idt_entry(33, isr_keyboard, IDT_PRESENT | IDT_INTERRUPT, 0); // ... // System call (vector 0x80) set_idt_entry(0x80, isr_syscall, IDT_PRESENT | IDT_DPL3 | IDT_TRAP, 0); load_idt();}The IDT descriptor (gate descriptor) contains all information the CPU needs to transfer control to an interrupt handler. Let's examine each field in detail.
64-bit Long Mode Gate Descriptor (16 bytes):
| Field | Bits | Description |
|---|---|---|
| Offset | 64 (split) | Linear address of handler entry point |
| Selector | 16 | Segment selector for handler code. In long mode, must select a 64-bit code segment. Indexes into GDT. |
| IST | 3 | Interrupt Stack Table index (1-7) or 0 for legacy stack switching |
| Type | 4 | 0xE = 64-bit Interrupt Gate, 0xF = 64-bit Trap Gate |
| DPL | 2 | Descriptor Privilege Level. Maximum ring from which INT n can invoke this handler. |
| P | 1 | Present bit. If 0, invoking this vector causes #NP (Not Present) fault. |
DPL controls who can use 'INT n' to invoke the handler. For most exceptions and hardware interrupts, DPL=0 (only kernel code can explicitly invoke). For INT 3 (breakpoint) and INT 0x80 (syscall), DPL=3 allows user programs to invoke. Hardware interrupts bypass DPL checking—they fire regardless of current privilege level.
Gate Types in Detail:
| Type | Value | Mode | IF Handling | Typical Use |
|---|---|---|---|---|
| Interrupt Gate | 0xE (1110) | Long Mode | Clears IF | Hardware interrupts, most exceptions |
| Trap Gate | 0xF (1111) | Long Mode | Preserves IF | System calls, breakpoints |
| Interrupt Gate | 0x6 (0110) | Protected 32-bit | Clears IF | 32-bit hardware interrupts |
| Trap Gate | 0x7 (0111) | Protected 32-bit | Preserves IF | 32-bit traps |
| Task Gate | 0x5 (0101) | Protected 32-bit only | N/A | Hardware task switching (obsolete) |
32-bit protected mode supported Task Gates for hardware task switching—the CPU would save the entire context to a TSS and load a new TSS on interrupt. This was slow and inflexible. Long Mode removed Task Gates entirely. Modern OSes perform software context switching, giving them full control over what's saved/restored.
The first 32 vectors (0-31) are reserved by Intel for CPU exceptions. These are architecturally defined—every x86 processor uses these vectors for the same purposes. Operating systems must set up handlers for all of them.
The Exception Vector Map:
| Vector | Mnemonic | Name | Type | Error Code |
|---|---|---|---|---|
| 0 | #DE | Divide Error | Fault | No |
| 1 | #DB | Debug Exception | Fault/Trap | No |
| 2 | NMI Interrupt | Interrupt | No | |
| 3 | #BP | Breakpoint | Trap | No |
| 4 | #OF | Overflow | Trap | No |
| 5 | #BR | BOUND Range Exceeded | Fault | No |
| 6 | #UD | Invalid Opcode | Fault | No |
| 7 | #NM | Device Not Available | Fault | No |
| 8 | #DF | Double Fault | Abort | Yes (0) |
| 9 | Coprocessor Segment Overrun | Fault | No | |
| 10 | #TS | Invalid TSS | Fault | Yes |
| 11 | #NP | Segment Not Present | Fault | Yes |
| 12 | #SS | Stack-Segment Fault | Fault | Yes |
| 13 | #GP | General Protection Fault | Fault | Yes |
| 14 | #PF | Page Fault | Fault | Yes |
| 15 | Reserved | |||
| 16 | #MF | x87 FPU Error | Fault | No |
| 17 | #AC | Alignment Check | Fault | Yes |
| 18 | #MC | Machine Check | Abort | No |
| 19 | #XM/#XF | SIMD Floating-Point | Fault | No |
| 20 | #VE | Virtualization Exception | Fault | No |
| 21 | #CP | Control Protection | Fault | Yes |
| 22-27 | Reserved | |||
| 28 | #HV | Hypervisor Injection | Fault | No |
| 29 | #VC | VMM Communication | Fault | Yes |
| 30 | #SX | Security Exception | Fault | Yes |
| 31 | Reserved |
Vector Assignment for Hardware Interrupts:
Vectors 32-255 are available for user-defined purposes—typically hardware interrupts and software traps. The PIC and APIC can be programmed to use any of these vectors.
Common Conventions:
In real mode, the BIOS maps IRQ0-7 to vectors 8-15 and IRQ8-15 to vectors 0x70-0x77. Vectors 8-15 conflict with CPU exceptions (Double Fault is vector 8!). Protected mode kernels MUST reprogram the PIC to use vectors 32+ before enabling interrupts, or hardware interrupts will trigger exception handlers.
When an interrupt occurs, the CPU performs a precise sequence of operations to locate and transfer control to the handler. This process involves multiple checks and potential faults.
Complete Lookup Sequence:
Detailed CPU Operations:
The IDT is a critical security structure. Improper configuration can allow privilege escalation, denial of service, or information leaks. Understanding these risks is essential for secure kernel development.
Key Security Principles:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Security-conscious IDT initialization void secure_idt_init(void) { // All exception handlers - DPL=0, Ring 0 only // Exceptions 8 (Double Fault), 2 (NMI), 18 (MCE) use IST set_idt_entry(0, exc_divide, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, 0); set_idt_entry(1, exc_debug, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, IST_DEBUG); set_idt_entry(2, exc_nmi, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, IST_NMI); // Breakpoint - DPL=3 so users can set breakpoints set_idt_entry(3, exc_breakpoint, IDT_PRESENT | IDT_DPL3 | IDT_TRAP, 0); // Double fault - MUST use IST (current stack may be corrupt) set_idt_entry(8, exc_double, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, IST_DOUBLE); // General Protection Fault - DPL=0 set_idt_entry(13, exc_gpf, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, 0); // Page Fault - DPL=0 set_idt_entry(14, exc_page_fault, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, 0); // Machine Check - use IST (may fire in inconsistent state) set_idt_entry(18, exc_mce, IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, IST_MCE); // Hardware interrupts - DPL=0 (software can't invoke) for (int i = 32; i < 256; i++) { set_idt_entry(i, hw_irq_handler[i-32], IDT_PRESENT | IDT_DPL0 | IDT_INTERRUPT, 0); } // System call - DPL=3 so users can invoke set_idt_entry(0x80, syscall_entry, IDT_PRESENT | IDT_DPL3 | IDT_TRAP, 0); // After setup, write-protect the IDT protect_idt_readonly();} void protect_idt_readonly(void) { // Find the page(s) containing the IDT uintptr_t idt_addr = (uintptr_t)&idt; uintptr_t idt_end = idt_addr + sizeof(idt); // Clear write bit in page table entries for (uintptr_t page = idt_addr & ~0xFFF; page < idt_end; page += PAGE_SIZE) { pte_t *pte = get_pte(page); *pte &= ~PTE_WRITE; } // Flush TLB for IDT pages flush_tlb_range(idt_addr, idt_end);}Years ago, some kernels set DPL=3 on exception handlers to 'simplify' testing—allowing user code to trigger exceptions directly. This enabled attacks where crafted exception frames led to kernel code executing with user-controlled data. Always set DPL=0 unless user invocation is intentionally required.
In Symmetric Multiprocessing (SMP) systems with multiple CPUs, IDT management requires additional consideration. While all CPUs can share the same IDT, each CPU needs its own TSS for stack pointers.
Shared IDT Model (Common):
Most operating systems use a single IDT shared by all CPUs:
Per-CPU TSS is Mandatory:
Even with a shared IDT, each CPU MUST have its own TSS:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
// Per-CPU TSS setup for SMP systems // Per-CPU data structurestruct cpu_data { struct tss64 tss; uint64_t kernel_stack[KERNEL_STACK_SIZE / 8]; uint64_t double_fault_stack[EXCEPTION_STACK_SIZE / 8]; uint64_t nmi_stack[EXCEPTION_STACK_SIZE / 8]; uint64_t mce_stack[EXCEPTION_STACK_SIZE / 8];} __attribute__((aligned(4096))); // Array of per-CPU data (one per CPU)static struct cpu_data percpu_data[MAX_CPUS]; void init_cpu_tss(int cpu_id) { struct cpu_data *cpu = &percpu_data[cpu_id]; struct tss64 *tss = &cpu->tss; memset(tss, 0, sizeof(*tss)); // Set kernel stack (top of stack = end of array) tss->rsp0 = (uint64_t)&cpu->kernel_stack[KERNEL_STACK_SIZE / 8]; // Set IST stacks tss->ist1 = (uint64_t)&cpu->double_fault_stack[EXCEPTION_STACK_SIZE / 8]; tss->ist2 = (uint64_t)&cpu->nmi_stack[EXCEPTION_STACK_SIZE / 8]; tss->ist3 = (uint64_t)&cpu->mce_stack[EXCEPTION_STACK_SIZE / 8]; // Each CPU needs its own GDT entry for its TSS // The TSS descriptor is 16 bytes in long mode setup_tss_gdt_entry(cpu_id, tss); // Load the TSS selector into the Task Register uint16_t tss_selector = TSS_SELECTOR(cpu_id); asm volatile("ltr %0" :: "r"(tss_selector));} // Boot CPU initializes IDT (shared)void init_idt_boot(void) { init_idt(); // Set up all handlers load_idt(); // Load IDTR} // Each AP CPU loads the shared IDT but its own TSSvoid init_idt_ap(int cpu_id) { // All APs share the same IDT load_idt(); // Same IDTR as boot CPU // But each AP has its own TSS init_cpu_tss(cpu_id);}Some specialized scenarios use per-CPU IDTs: performance monitoring with CPU-specific handlers, security hardening (different handler addresses per CPU make attacks harder), or specialized interrupt affinity. However, maintaining multiple IDTs adds complexity and is rarely necessary.
We've explored the data structures that organize interrupt handlers—from the simple Real Mode IVT to the sophisticated Protected/Long Mode IDT. These tables are the foundation of all interrupt-driven operations in x86 systems.
What's Next:
With the vector table and descriptor format understood, the final piece is interrupt priority—how the system decides which interrupt to service when multiple occur simultaneously, and how priority affects interrupt nesting and masking.
You now understand interrupt vector tables: their structure, format, lookup process, and security implications. This knowledge is essential for OS kernel development and understanding how CPUs locate and invoke interrupt handlers. Next, we'll explore interrupt priority and nesting.