Skip to content
GDT, IDT & Mode Switching
step 1/5

Reading — step 1 of 5

Read

~4 min readBoot & Memory

GDT, IDT & Mode Switching

Two tables rule protected-mode x86: the GDT decides what memory segments exist and who may use them, the IDT decides where the CPU jumps when the world interrupts it. Neither is optional — the CPU literally consults them on every privilege check and every interrupt. Your kernel must build both, byte-exact, and in this lesson's exercise you'll encode real GDT descriptors down to the bit.

Why segmentation still exists

In real mode, an address is segment × 16 + offset — a 1978 hack to reach 1 MiB with 16-bit registers. Protected mode replaced raw segment numbers with selectors that index into a table of descriptors, each describing a region: where it starts (base), how big it is (limit), and what's allowed inside it (access rights).

Modern 64-bit kernels use the flat model: every segment has base 0 and maximum limit, so segmentation checks all trivially pass and paging does the real protection. So why still build a GDT? Because the CPU refuses to run without one:

  • The current privilege level lives in the CS register's low bits — ring 0 vs ring 3 is a segment selector property.
  • Interrupt handlers need a kernel code selector to run in ring 0.
  • The TSS (needed for stack switching on interrupts — chapter 3) is a GDT entry.

The GDT is vestigial the way a foundation is vestigial.

The descriptor layout (your exercise)

Each GDT entry is 8 bytes, and its layout is a museum of backwards compatibility — the base and limit are scattered because the 386 extended the 286's 6-byte entries without moving old fields:

bits  0–15   limit[15:0]
bits 16–39   base[23:0]
bits 40–47   access byte
bits 48–51   limit[19:16]
bits 52–55   flags (G, D/B, L, AVL)
bits 56–63   base[31:24]

The access byte, bit by bit:

  • P (bit 7) — present. 0 means "descriptor unused"; loading it faults.
  • DPL (bits 5–6) — descriptor privilege level, 0–3. The ring you must be in (or entering) to use this segment.
  • S (bit 4) — 1 for code/data segments, 0 for system entries (TSS, gates).
  • Type (bits 0–3) — for code: executable(1), conforming, readable, accessed. For data: executable(0), grow-direction, writable, accessed.

The flags nibble:

  • G — granularity. 0: limit counts bytes (max 1 MiB). 1: limit counts 4 KiB pages — how a 20-bit limit field describes 4 GiB (0xFFFFF pages ≈ 4 GiB).
  • D/B — 1 for 32-bit segments.
  • L — 1 marks a 64-bit code segment (and then D must be 0).

Entry 0 must be the null descriptor — all zeroes, never usable; loading selector 0 into a data register is legal, using it faults. It's the table's built-in trap for uninitialized selectors.

A classic flat 32-bit kernel code segment: base 0, limit 0xFFFFF, access 0x9A (present, DPL 0, code, exec+read), flags 0xC (G=1, D=1) → the canonical bytes FF FF 00 00 00 9A CF 00... except you'll compute that yourself, bit by bit, in the exercise — and after it, GDT dumps in a debugger will read like text.

The IDT: 256 doorways

The IDT has 256 entries — one per interrupt vector — each a gate holding: the handler's address, the code selector to run it with (your ring-0 kernel code segment — this is where the tables meet), and a DPL.

  • Vectors 0–31 are CPU exceptions, fixed by Intel: 0 divide error, 6 invalid opcode, 13 general protection, 14 page fault
  • 32–255 are yours. Hardware IRQs get remapped here (timer at 32 by convention — chapter 3 does this to the PIC).

Gate DPL matters for one thing: whether ring 3 may deliberately trigger the vector with int n. Handlers still run in ring 0 regardless — that's the syscall trick, and it's also why every gate's DPL must be chosen on purpose.

The switching sequence

The full real→protected ceremony, in order and with no steps skippable: enable the A20 line (another compatibility ghost — address bit 20 is gated off at boot so 1985 software wraps addresses like an 8086), lgdt your table, set CR0.PE, then immediately far jump — because the far jump reloads CS from your new GDT and flushes the prefetched real-mode instructions. Then reload the data segment registers. For long mode, add: page tables into CR3, set EFER.LME, enable CR0.PG, far jump into an L=1 code segment. Get one byte of a descriptor wrong and the far jump triple-faults the machine — which is exactly why you're learning to encode them exactly.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…