The IDT
The Interrupt Descriptor Table (IDT) is the structure an x86 CPU consults every time an interrupt or exception occurs, mapping each of 256 possible vector numbers to the address of the code that should handle it. Where real mode uses a simpler Interrupt Vector Table at a fixed memory location, protected mode and long mode both use the IDT instead, gaining per-vector privilege enforcement and, under long mode, an additional mechanism for guaranteeing a clean stack on the most critical faults.
Loading the table
Section titled “Loading the table”The LIDT instruction loads the CPU’s internal IDTR register from a 10-byte structure in memory: a 16-bit limit (the table’s size in bytes, minus one) followed by a 64-bit base address under long mode. Until LIDT has executed with a valid table, any interrupt or exception the CPU receives has nowhere defined to dispatch to, which is why setting up a minimal IDT is one of the earliest steps in most kernels’ initialization, generally performed before interrupts are unmasked at all.
struct idtr { uint16_t limit; uint64_t base;} __attribute__((packed));
void load_idt(void *table, uint16_t entry_count) { struct idtr idtr = { .limit = entry_count * sizeof(struct idt_entry) - 1, .base = (uint64_t)table, }; asm volatile("lidt %0" : : "m"(idtr));}Gate descriptor format
Section titled “Gate descriptor format”Each of the table’s 256 entries is a 16-byte gate descriptor under long mode (8 bytes under protected mode, without the upper-address extension):
struct idt_entry { uint16_t offset_low; uint16_t selector; uint8_t ist; // bits 0-2: Interrupt Stack Table index; rest reserved uint8_t type_attr; // gate type, DPL, present bit uint16_t offset_mid; uint32_t offset_high; uint32_t reserved;} __attribute__((packed));The handler’s address is split across three separate fields (offset_low, offset_mid, and offset_high) rather than stored contiguously, an artifact of the descriptor format’s protected-mode ancestry being extended rather than redesigned when long mode added the upper 32 address bits. selector identifies which code segment in the GDT the handler runs under, and type_attr packs together the gate type, the Descriptor Privilege Level (the minimum privilege required to invoke this vector directly via software, as opposed to hardware), and a present bit that must be set or any interrupt landing on that vector immediately faults instead of being handled.
Gate types
Section titled “Gate types”The type field distinguishes an interrupt gate from a trap gate, differing in exactly one respect: an interrupt gate automatically clears the interrupt flag on entry, disabling further maskable interrupts until the handler explicitly re-enables them or returns, while a trap gate leaves the interrupt flag unchanged. Hardware interrupt handlers are conventionally installed as interrupt gates specifically to avoid re-entrant delivery of a second interrupt in the middle of handling the first; exception handlers are more often installed as trap gates, since an exception such as a page fault frequently needs interrupts to remain enabled while it runs. A third type, the task gate, exists for hardware-assisted task switching but sees essentially no use under long mode, which removed hardware task-switching support entirely.
The Interrupt Stack Table
Section titled “The Interrupt Stack Table”Long mode adds a mechanism absent from protected mode: each gate descriptor’s ist field can select one of seven alternate stacks, defined in the current Task State Segment, that the CPU switches to unconditionally on entry to that vector, regardless of what stack was in use beforehand. This exists specifically for faults that can occur when the current stack itself is not trustworthy (a double fault caused by a corrupted or exhausted stack, or a non-maskable interrupt arriving during a stack-sensitive window), where continuing to use whatever stack happened to be active could turn a recoverable fault into an unrecoverable one. A kernel typically reserves the IST mechanism for a small handful of vectors (commonly double fault and NMI) and leaves the ist field zero, meaning “no stack switch,” for the rest.
Reserved exception vectors
Section titled “Reserved exception vectors”Vectors 0 through 31 are reserved by the architecture for CPU-generated exceptions rather than available for general software or hardware interrupt use, which is the specific reason a PIC or APIC configuration must remap hardware IRQs to vectors 32 and above. Leaving hardware interrupts at their unremapped default of vectors 0 through 15 would collide directly with this reserved range. A handful of the reserved vectors come up constantly during kernel development:
| Vector | Name | Notes |
|---|---|---|
| 0 | Divide error | Integer division by zero, or a quotient that overflows the destination |
| 1 | Debug | Single-step trap or a debug-register breakpoint/watchpoint match |
| 6 | Invalid opcode | Executing a byte sequence the CPU does not recognize as a valid instruction |
| 7 | Device not available | CR0.TS set and a floating-point/SIMD instruction ran; see FXSAVE/XSAVE |
| 8 | Double fault | An exception occurred while the CPU was trying to invoke a handler for a prior exception |
| 13 | General protection fault | The broadest category: segment violations, privilege violations, and many other conditions all report through this one vector |
| 14 | Page fault | See Paging & Virtual Memory |
Error codes
Section titled “Error codes”Some, but not all, of the reserved exceptions push an additional 32-bit error code onto the stack below the standard interrupt frame, and a handler must know in advance which vectors do this: there is no way to detect its presence at runtime, since a handler for a vector that never pushes one and a handler for one that always does are simply written differently, popping (or not) a fixed number of bytes matching what that specific vector is documented to push. Page fault and general protection fault both push an error code; divide error and invalid opcode do not.
Implementation notes
Section titled “Implementation notes”A handler entered through an interrupt gate must exit with IRET, not an ordinary RET: the interrupt frame the CPU pushed on entry (instruction pointer, code segment, flags, and, if a privilege change occurred, stack pointer and stack segment) has a different shape than a normal call’s return address, and IRET is the only instruction that unwinds it correctly, including restoring the flags register. Selector values in the GDT loaded into selector must also be exactly right; an incorrect or stale selector, particularly one left over from before the GDT was reloaded during earlier boot stages, is a common cause of a general protection fault firing the instant the very first interrupt arrives, which itself then fails to dispatch correctly if the IDT is not yet in a state to handle a GPF either.
References
Section titled “References”- ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 6: defines the IDT format, gate types, and the full list of reserved exception vectors.
- ^ AMD, AMD64 Architecture Programmer’s Manual, Volume 2, Chapter 8: the corresponding long-mode reference, including the Interrupt Stack Table.
See also
Section titled “See also”- PIC & APIC: where hardware interrupts are routed before reaching a vector this table dispatches.
- System Calls: a software-triggered path into the kernel that can, depending on mechanism, also route through the IDT.
- The Task State Segment: the structure the Interrupt Stack Table pointers above are defined in.
- FXSAVE/XSAVE and FPU/SIMD State: the lazy-switching mechanism that relies on the #NM vector above.
- Debug Registers and Hardware Breakpoints: what actually configures the #DB vector above and reads back which condition fired.
- Kernel Panics and Stack Backtraces: what a handler for one of these vectors commonly does when the exception has no sane recovery.