| name | osdev-cpu-tables |
| description | GDT, IDT, and TSS setup for x86/x86-64. Descriptor encoding, segment selectors, gate types, ring transitions, and interrupt routing. Use when creating descriptor tables, handling privilege changes, or setting up interrupt gates. |
| origin | MCC |
CPU Descriptor Tables (GDT, IDT, TSS)
Complete reference for x86/x86-64 descriptor table setup: the Global Descriptor Table for memory segmentation, the Interrupt Descriptor Table for interrupt routing, and the Task State Segment for privilege-level stack switching.
When to Use
- Setting up a GDT for protected mode or long mode entry
- Encoding segment descriptors (kernel code/data, user code/data, TSS)
- Building an IDT to handle exceptions and hardware interrupts
- Configuring a TSS for ring 3 to ring 0 stack switching
- Debugging triple faults, GPFs, or invalid TSS exceptions
- Transitioning from 32-bit protected mode to 64-bit long mode
GDT (Global Descriptor Table)
The GDT exists because x86 protected mode uses segmentation as its primary protection mechanism. Even if you use a flat memory model (which you should -- paging handles real memory protection), the CPU still requires valid segment descriptors to function. The GDT tells the CPU the privilege level, type, and address range of each memory segment.
Flat Model Setup
Modern OSes use a flat model: every segment covers the entire 4 GiB address space (base=0, limit=0xFFFFF with page granularity). This effectively disables segmentation while satisfying the CPU's requirement for valid descriptors.
32-bit flat GDT:
| Offset | Segment | Access | Flags |
|---|
| 0x00 | Null descriptor | 0x00 | 0x0 |
| 0x08 | Kernel code | 0x9A | 0xC |
| 0x10 | Kernel data | 0x92 | 0xC |
| 0x18 | User code | 0xFA | 0xC |
| 0x20 | User data | 0xF2 | 0xC |
| 0x28 | TSS | 0x89 | 0x0 |
64-bit flat GDT:
| Offset | Segment | Access | Flags |
|---|
| 0x00 | Null descriptor | 0x00 | 0x0 |
| 0x08 | Kernel code | 0x9A | 0xA |
| 0x10 | Kernel data | 0x92 | 0xC |
| 0x18 | User data | 0xF2 | 0xC |
| 0x20 | User code | 0xFA | 0xA |
| 0x28 | TSS (16 bytes) | 0x89 | 0x0 |
The 64-bit kernel code segment uses flags 0xA (L=1, DB=0) to enable long mode. In 64-bit mode, base and limit are ignored for code/data segments -- the CPU treats every segment as covering the full address space regardless.
Note the 64-bit ordering: user data appears before user code. This layout matters for SYSRET, which expects the user code segment selector to be the data selector + 16.
Loading the GDT
The LGDT instruction takes a pointer to a descriptor structure:
struct gdt_ptr {
uint16_t limit;
uintptr_t base;
} __attribute__((packed));
After LGDT, reload all segment registers. CS requires a far jump:
lgdt [gdt_ptr]
; Reload CS via far jump
jmp 0x08:.reload_cs ; 0x08 = kernel code selector
.reload_cs:
mov ax, 0x10 ; 0x10 = kernel data selector
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
See references/gdt-encoding.md for the full bit layout, encoding functions, and segment selector format.
IDT (Interrupt Descriptor Table)
The IDT maps each of the 256 interrupt vectors to a handler function. Without it, the CPU has no way to dispatch exceptions (divide by zero, page fault) or hardware interrupts (keyboard, timer). Vectors 0-31 are reserved by the CPU for exceptions; vectors 32-255 are available for hardware interrupts and software use.
Gate Types
The gate type controls what happens to the interrupt flag (IF) when the handler is invoked:
| Type | Value | IF Behavior | Use For |
|---|
| Interrupt gate | 0xE | Clears IF (interrupts disabled) | Hardware IRQs, most exceptions |
| Trap gate | 0xF | Leaves IF unchanged | Breakpoints, syscalls, debug exceptions |
Interrupt gates are the safer default because they prevent nested interrupts from corrupting handler state. Trap gates are appropriate when the handler explicitly needs to allow interrupts (breakpoint handlers, page fault handlers that may need I/O).
DPL Field
The DPL on an IDT gate controls which privilege levels can trigger that interrupt via the INT instruction. Hardware interrupts bypass this check entirely. Set DPL=0 for most entries (only kernel can INT). Set DPL=3 for the syscall vector so user-space can invoke it with INT 0x80.
Loading the IDT
Same structure as GDTR. After filling the table, load with LIDT:
struct idt_ptr {
uint16_t limit;
uintptr_t base;
} __attribute__((packed));
ISR Stub Pattern
Exception handlers need assembly stubs because some exceptions push an error code and some do not. The stub normalizes the stack:
; For exceptions that DON'T push an error code
isr_stub_no_err:
push 0 ; push dummy error code
push VECTOR_NUM ; push interrupt number
jmp isr_common
; For exceptions that DO push an error code (8, 10-14, 17, 21, 29, 30)
isr_stub_err:
push VECTOR_NUM ; error code already on stack
jmp isr_common
isr_common:
pusha ; save registers (pushad in 32-bit)
mov ax, 0x10
mov ds, ax ; load kernel data segment
push esp ; pointer to stack frame
call exception_handler
pop esp
popa
add esp, 8 ; pop error code and vector number
iret
See references/idt-gates.md for the full gate format, exception vector table, and 64-bit differences.
TSS (Task State Segment)
The TSS exists to solve a critical problem: when an interrupt arrives while user code is running (ring 3), the CPU needs to switch to a kernel stack (ring 0) before it can safely handle the interrupt. Without the TSS, the CPU has no way to find the kernel stack pointer.
Despite its name suggesting hardware task switching, modern OSes use the TSS only for this stack-switching purpose. Hardware task switching is slow, not portable, and was removed entirely in 64-bit mode. You need exactly one TSS per CPU.
What to Set
32-bit (Protected Mode): The TSS is 104 bytes. Only three fields matter for software multitasking:
SS0 -- kernel stack segment selector (e.g., 0x10)
ESP0 -- kernel stack pointer (update on every task switch)
IOPB -- set to sizeof(TSS) (104) if you don't use the I/O bitmap
64-bit (Long Mode): The TSS is 108 bytes with a different layout:
RSP0 -- kernel stack pointer for ring 0 transitions
RSP1, RSP2 -- stack pointers for ring 1 and ring 2 (rarely used)
IST1-IST7 -- Interrupt Stack Table entries
IOPB -- I/O permission bitmap offset
Interrupt Stack Table (64-bit)
The IST is a 64-bit feature that provides dedicated stacks for specific interrupt vectors. Each IDT entry has a 3-bit IST field (1-7, or 0 for disabled). When an interrupt fires with a non-zero IST, the CPU loads the stack from that IST entry regardless of the current privilege level.
This is essential for exceptions that can occur while already on a corrupted stack:
- Double fault (#DF) -- if the exception handler itself faults, the kernel stack may be corrupted. A dedicated IST stack prevents a triple fault.
- NMI -- can arrive at any time, including during stack switching.
- Machine check (#MC) -- same reasoning as NMI.
Loading the TSS
The TSS needs a descriptor in the GDT (system segment, type=0x9 for available TSS). In 64-bit mode, this descriptor is 16 bytes wide (occupies two GDT slots) to hold a 64-bit base address.
After the GDT is loaded, load the TSS with LTR:
mov ax, 0x28 ; TSS descriptor offset in GDT
ltr ax
See references/tss-layout.md for complete structure layouts and C struct definitions.
Putting It All Together
Initialization order matters because each table depends on the previous:
1. Build GDT entries (including TSS descriptor)
2. LGDT -- load GDT, reload segment registers
3. Initialize TSS structure (set RSP0/ESP0, IST entries)
4. LTR -- load task register with TSS selector
5. Build IDT entries (gates reference GDT code segment selector)
6. LIDT -- load IDT
7. STI -- enable interrupts (only after PIC/APIC is configured)
The GDT comes first because everything else references it. The TSS descriptor lives inside the GDT, so the GDT must be loaded before LTR. The IDT gates contain a segment selector pointing to the GDT kernel code segment. Interrupts should be disabled (CLI) until the entire chain is ready.
Common Pitfalls
Triple fault on GDT load: The GDTR.limit is sizeof(gdt) - 1, not sizeof(gdt). Off-by-one here means the last entry is inaccessible, which causes a GPF when accessed, which causes a double fault, which causes a triple fault.
GPF when loading segment registers: After LGDT, you must reload every segment register. Stale selectors from the old GDT are invalid. CS requires a far jump; the others use MOV.
Wrong access byte for TSS: The TSS descriptor is a system segment (S=0), not a code/data segment. Access byte 0x89 means P=1, DPL=0, S=0, Type=0x9 (available TSS). Using 0xE9 (S=1) causes an Invalid TSS exception.
64-bit TSS descriptor is 16 bytes: In long mode, system segment descriptors (TSS, LDT) are 16 bytes wide, not 8. If you use an 8-byte descriptor, the upper half of the base address is garbage, and LTR will fault or silently point to the wrong memory.
IRET vs IRETQ: In 64-bit mode, most assemblers do not automatically translate IRET to the 64-bit form. Use IRETQ explicitly, or you'll get mysterious crashes on interrupt return.
Missing ISR error code normalization: Exceptions 8, 10, 11, 12, 13, 14, 17, 21, 29, and 30 push an error code. All others do not. If your common handler assumes a uniform stack layout without push/pop compensation, it will read garbage values.
Forgetting to update ESP0/RSP0 on task switch: Each time you switch tasks, the TSS.ESP0 (32-bit) or TSS.RSP0 (64-bit) must be updated to point to the new task's kernel stack top. Otherwise, the next interrupt in user mode will use the old task's kernel stack.
Double fault without IST in 64-bit mode: If a double fault fires on a corrupted stack and you haven't assigned an IST entry for vector 8, the CPU will try to use the broken stack, causing a triple fault and reboot.
Related Skills
osdev-toolchain -- cross-compiler, linker scripts, QEMU debugging