When I first started learning Assembly language, one topic that genuinely confused me for weeks was how a CPU “knows” what to do the instant a keyboard key is pressed, a timer ticks, or a division by zero happens mid-program. The answer to all of that lives in one quiet, unassuming structure sitting in memory: the Interrupt Vector Table (IVT). Once I understood it, half of low-level programming suddenly made sense — from BIOS calls to modern operating system kernels.
In this post, I’m going to walk through the IVT from the ground up, covering what it is, how it’s structured in memory, how x86, x86-64, and ARM handle it differently, and how you can actually see it working with real Assembly code.
What Exactly Is the Interrupt Vector Table?
At its core, the Interrupt Vector Table is a lookup table stored in memory that maps interrupt numbers to the memory addresses of the routines that should handle them. Think of it like a phone directory: when interrupt number 21 rings, the CPU doesn’t guess what to do — it looks up entry 21 in the table, finds an address, and jumps straight there.
An interrupt is simply a signal that tells the processor “stop what you’re doing right now and handle something more urgent.” Interrupts come from three broad sources:
- Hardware interrupts — keyboard presses, timer ticks, disk I/O completion
- Software interrupts — deliberately triggered by instructions like
INT 0x21in DOS orSVCin ARM - Exceptions/faults — divide-by-zero, page faults, invalid opcodes
Without a table like the IVT, the CPU would have no systematic way to decide where to jump when any of these events occur.
Why the IVT Exists: The Architectural Reasoning
Early x86 processors running in real mode (like the original 8086) placed the IVT at a fixed location: memory address 0x0000:0x0000 through 0x0000:0x03FF. That’s the first 1KB of RAM, containing 256 entries, each 4 bytes long (a 2-byte offset and a 2-byte segment).
| Property | Value |
|---|---|
| Location | 0000:0000 to 0000:03FF |
| Total size | 1024 bytes |
| Number of entries | 256 |
| Entry size | 4 bytes (segment:offset far pointer) |
| Entry 0 | Divide-by-zero handler |
| Entry 1 | Single-step debug interrupt |
| Entry 0x21 | DOS service interrupt (common in old DOS programs) |
Here’s a simple memory diagram of how the real-mode IVT is laid out:
Address Content
0x0000 Offset for INT 0 (Divide by zero)
0x0002 Segment for INT 0
0x0004 Offset for INT 1 (Debug)
0x0006 Segment for INT 1
0x0008 Offset for INT 2 (NMI)
0x000A Segment for INT 2
... ...
0x03FC Offset for INT 255
0x03FE Segment for INT 255
This fixed placement made real-mode interrupt handling extremely fast — no searching, no indirection beyond one table lookup.
The Modern Equivalent: IDT in Protected/Long Mode
Once x86 processors moved into protected mode (and later long mode for 64-bit systems), the simple IVT was replaced by the Interrupt Descriptor Table (IDT). This is a more powerful structure that not only stores addresses but also privilege levels, gate types, and segment selectors.
Unlike the IVT, the IDT isn’t at a fixed address — its location is loaded into a special register called IDTR using the LIDT instruction, and each entry is 8 bytes in 32-bit mode or 16 bytes in 64-bit mode.
; x86 assembly: loading the IDT register
lidt [idt_descriptor]
idt_descriptor:
dw idt_end - idt_start - 1 ; limit (size of IDT - 1)
dd idt_start ; base address of IDT
Each IDT entry (called a “gate”) looks roughly like this in 32-bit protected mode:
| Bits | Field |
|---|---|
| 0–15 | Offset (low 16 bits of handler address) |
| 16–31 | Segment selector |
| 32–39 | Reserved / zero |
| 40–47 | Type and attributes (gate type, privilege level, present bit) |
| 48–63 | Offset (high 16 bits of handler address) |
How the CPU Processes an Interrupt: Step by Step
I find it helps to visualize the entire flow as a sequence, because the IVT/IDT is only one piece of a bigger dance between hardware and software.
sequenceDiagram
participant HW as Hardware Device
participant CPU as CPU Core
participant IVT as Interrupt Vector Table / IDT
participant ISR as Interrupt Service Routine
HW->>CPU: Raise interrupt signal (IRQ)
CPU->>CPU: Finish current instruction, save state (flags, CS:IP)
CPU->>IVT: Look up entry using interrupt number
IVT-->>CPU: Return handler address
CPU->>ISR: Jump to handler address
ISR->>ISR: Execute interrupt handling code
ISR->>CPU: Execute IRET / IRETQ
CPU->>CPU: Restore saved state
CPU->>HW: Resume normal execution
The critical steps, in plain terms:
- The CPU finishes the current instruction (it never jumps mid-instruction).
- It pushes the flags register, code segment, and instruction pointer onto the stack.
- It uses the interrupt number as an index into the IVT (or IDT) to find the handler’s address.
- It jumps to that handler, executes the interrupt service routine (ISR).
- The ISR ends with
IRET(orIRETQin 64-bit mode), which pops the saved state back and resumes exactly where it left off.
Assembly Examples: Triggering and Handling Interrupts
x86 Real Mode Example (DOS-style)
; Print a character using BIOS interrupt 0x10
mov ah, 0x0E ; teletype output function
mov al, 'A' ; character to print
int 0x10 ; call BIOS video interrupt
Here, INT 0x10 causes the CPU to look up entry 0x10 in the IVT, which points to the BIOS’s video service routine.
Setting a Custom Interrupt Handler (Real Mode)
; Redirect interrupt 0x1C (timer tick) to our own handler
cli ; disable interrupts while modifying table
mov ax, 0
mov es, ax
mov word [es:0x1C*4], offset my_handler
mov word [es:0x1C*4+2], cs
sti ; re-enable interrupts
my_handler:
; custom code here
iret
x86-64 Long Mode: Setting Up an IDT Entry (Conceptual)
; Simplified IDT entry setup in NASM-style pseudo-code
set_idt_entry:
mov rax, handler_addr
mov [idt_entry], ax ; offset bits 0-15
shr rax, 16
mov [idt_entry+6], ax ; offset bits 16-31
shr rax, 16
mov [idt_entry+8], eax ; offset bits 32-63
mov word [idt_entry+2], 0x08 ; code segment selector
mov byte [idt_entry+5], 0x8E ; present, ring 0, interrupt gate
ret
ARM: Vector Table Example
ARM processors (particularly Cortex-M and classic ARM) use a vector table conceptually similar to the x86 IVT, but structured as a simple array of addresses at the start of flash memory.
; ARM Cortex-M vector table (simplified, in assembly)
.section .isr_vector
.word _stack_top ; initial stack pointer
.word Reset_Handler ; entry 1: reset
.word NMI_Handler ; entry 2: non-maskable interrupt
.word HardFault_Handler ; entry 3: hard fault
.word MemManage_Handler ; entry 4: memory management fault
On ARM, this table typically resides at address 0x00000000 (or wherever the vector table offset register, VTOR, points), and each entry is a direct address rather than a segment:offset pair, since ARM doesn’t use x86-style segmentation.
Comparing IVT (x86 Real Mode) vs IDT (Protected/Long Mode) vs ARM Vector Table
| Feature | x86 Real Mode IVT | x86 Protected/Long Mode IDT | ARM Vector Table |
|---|---|---|---|
| Fixed location | Yes (0x0000) | No (set via LIDT) | Configurable via VTOR |
| Entry size | 4 bytes | 8 bytes (32-bit) / 16 bytes (64-bit) | 4 bytes (address only) |
| Privilege levels | None | Yes (ring 0–3) | Yes (via exception levels) |
| Entry content | Segment:offset | Offset + selector + gate type + privilege | Direct address |
| Number of entries | 256 | Up to 256 | Depends on core (usually 16 system + N external IRQs) |
Practical Use Cases and OS Interaction
The IVT/IDT is the backbone of how operating systems handle:
- Hardware interrupts — keyboard input, mouse movement, network packets arriving
- System calls — Linux traditionally used
INT 0x80to jump into kernel mode (modern systems favor the fasterSYSCALL/SYSENTERinstructions, but the underlying philosophy is the same table-driven dispatch) - CPU exceptions — page faults trigger the memory manager to load data from disk, divide errors terminate misbehaving processes
- Timer-driven multitasking — the timer interrupt is what allows an OS scheduler to periodically regain control and switch between processes
Without this table-based dispatch mechanism, an operating system would have no reliable way to preempt a running program or respond to asynchronous hardware events.
Debugging and Performance Considerations
When I’ve debugged low-level boot code or kernel modules, a few things about the IVT/IDT consistently come up:
- Triple faults: If the IDT itself is malformed or unreachable when a fault occurs, the CPU can’t even deliver the fault handler, which causes a “triple fault” — usually resulting in an immediate reboot. This is one of the trickiest bugs to diagnose in OS development.
- Interrupt latency: Because interrupt handling always requires a table lookup plus a context save/restore, performance-sensitive systems (like real-time OSes) carefully minimize the work done inside ISRs, deferring heavier processing to bottom halves or deferred procedure calls.
- Security implications: Since the IDT determines where control transfers on privilege-crossing events, protecting it (e.g., marking it read-only, using SMEP/SMAP on x86) is critical to prevent privilege escalation exploits.
- Debugging tools: Tools like GDB, WinDbg, or QEMU’s monitor can dump the IDT contents (
info idtin QEMU), which is invaluable when chasing down mysterious crashes tied to a specific interrupt vector.
Common Mistakes When Working With the IVT/IDT
- Forgetting to disable interrupts (
CLI) while modifying the table — a hardware interrupt firing mid-update can jump to a half-written address and crash the system. - Incorrect segment:offset ordering — easy to swap by mistake in real mode, causing jumps to garbage memory.
- Not restoring the original handler — if your custom ISR chains to another handler, forgetting to preserve and call the original address can silently break OS or BIOS behavior.
- Missing
IRET/IRETQ— using a regularRETinstead ofIRETfails to restore flags and can leave the interrupt-enable flag in a wrong state. - Ignoring privilege/gate types in the IDT — using the wrong gate type (interrupt gate vs. trap gate) changes whether interrupts are automatically disabled during handling, leading to subtle re-entrancy bugs.
Best Practices
- Always save and restore all registers you touch inside an ISR.
- Keep interrupt handlers as short as possible — defer heavy work to a later, non-interrupt context.
- Use
CLI/STI(or their equivalents) sparingly and only around the minimal critical section. - Validate the IDT limit and base address carefully before executing
LIDT. - On ARM, ensure the Vector Table Offset Register (VTOR) is set correctly before relocating the vector table.
Nested Interrupts and Interrupt Priority
One detail I glossed over initially is what happens when an interrupt fires while another interrupt is already being handled. This is where the Programmable Interrupt Controller (PIC) on classic x86 systems, or the more modern Advanced Programmable Interrupt Controller (APIC), comes into play. These chips sit between hardware devices and the CPU, prioritizing and queuing interrupt requests (IRQs) before they ever reach the IVT/IDT lookup stage.
| Component | Role |
|---|---|
| PIC (8259) | Legacy chip managing up to 15 IRQ lines (via cascading two 8-bit controllers), assigns priority, and signals the CPU |
| APIC (Local + I/O) | Modern replacement supporting more interrupt lines, multi-core routing, and inter-processor interrupts (IPIs) |
| NMI (Non-Maskable Interrupt) | A special interrupt line that cannot be disabled by CLI, reserved for critical hardware failures like memory parity errors |
By default, most interrupt gates in the IDT are configured to automatically disable further interrupts while an ISR runs (this is what an “interrupt gate” does, as opposed to a “trap gate,” which leaves interrupts enabled). This prevents a flood of same-priority interrupts from recursively overwhelming the handler, though the OS can deliberately re-enable interrupts partway through a handler if it needs to support nested/preemptible interrupt handling for latency-sensitive systems.
A Bit of History: Why 256 Entries?
It’s worth appreciating why the IVT/IDT has exactly 256 entries. The 8086’s designers reserved a single byte to encode the interrupt number in the INT instruction’s opcode (INT imm8), and a single byte naturally covers values 0–255. Intel further reserved the first 32 entries (0–31) for CPU-generated exceptions (divide error, invalid opcode, page fault, and so on), leaving 224 entries free for hardware IRQs and software-triggered interrupts — a convention that persists in every x86 and x86-64 processor manufactured since.
Frequently Asked Questions
Is the IVT the same as the IDT? Not exactly. The IVT is the simpler, fixed-location table used in x86 real mode. The IDT is the more flexible, feature-rich structure used in protected and long mode, supporting privilege levels and different gate types.
Can user-level programs modify the IVT/IDT? In real mode, yes, any code can modify it since there’s no privilege separation. In protected/long mode, modifying the IDT typically requires ring 0 (kernel) privileges, since LIDT is a privileged instruction.
What happens if an interrupt number has no valid handler? On x86, an unhandled or malformed interrupt vector can trigger a general protection fault or, in the worst case, a triple fault that reboots the machine.
Does ARM use segment:offset addressing like x86? No. ARM doesn’t use segmentation the way x86 real mode does, so its vector table simply stores direct addresses.
Why do modern systems use SYSCALL instead of INT 0x80? SYSCALL/SYSENTER avoid the overhead of a full interrupt-table lookup and privilege-level switch mechanics, making system calls noticeably faster — though conceptually they still transfer control from user mode to kernel mode in a controlled way.
What’s the difference between an interrupt gate and a trap gate in the IDT? An interrupt gate automatically clears the interrupt-enable flag when entering the handler, blocking further maskable interrupts until the handler explicitly re-enables them or returns. A trap gate leaves interrupts enabled throughout, which is typically used for exceptions where immediate re-entrancy isn’t a concern, such as debug traps.
Can a single interrupt number point to multiple handlers? Not directly at the hardware level — each vector has exactly one entry. However, operating systems commonly implement “chaining,” where a handler for a shared IRQ line checks a device status register and then calls into a list of registered software handlers, effectively multiplexing one hardware vector across several drivers.
Summary and Key Takeaways
The Interrupt Vector Table is one of the oldest and most fundamental ideas in computer architecture: a simple table that lets the CPU respond predictably to unpredictable events. In real mode, it’s a fixed 1KB table of segment:offset pairs. In protected and long mode, it evolves into the richer Interrupt Descriptor Table, complete with privilege checks and gate types. ARM achieves the same goal with its own vector table concept, tailored to its exception-level model.
Understanding this structure gives you real insight into how operating systems achieve multitasking, how hardware devices get serviced, and how a single stray write to the wrong memory address can bring an entire system down. It’s a small table with an outsized responsibility.
References
- Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide — Chapter on Interrupt and Exception Handling
- AMD64 Architecture Programmer’s Manual, Volume 2: System Programming — Interrupt and Exception Handling
- ARM Architecture Reference Manual (ARMv7-M and ARMv8-M) — Exception Model and Vector Table sections
- GNU Assembler (GAS) Manual — Directives relevant to sections and vector tables