x86 Execution Modes, Descriptor Tables & Privilege

Real mode, protected mode, and IA-32e mode are coordinated contracts among control registers, descriptors, paging, stacks, and event delivery—not interchangeable names for operand width.

An x86 execution mode is not just an instruction-width switch. It is a mutually constrained state spanning CR0, CR4, IA32_EFER, CS attributes, descriptor tables, paging structures, interrupt gates, and stacks. A transition succeeds only when the state required by the destination mode already exists and the instructions completing the transition remain executable under both sides of the boundary.

real-address mode reset state CR0.PE = 0 16-bit segmented execution 1 protected mode CR0.PE = 1 descriptor checks active 16- or 32-bit code via CS legacy paging optional GDT · IDT · TSS establishable 2 IA-32e active EFER.LMA = 1 · 64-bit paging compatibility CS.L = 0 16/32-bit decoding 64-bit submode CS.L = 1 · CS.D = 0 far transfer loads CS 1 · Protected entry: LGDT → CR0.PE=1 → immediate far branch. 2 · IA-32e activation: CR4.PAE → CR3 root → EFER.LME → CR0.PG. Mode bits establish the architecture; a loaded CS descriptor selects the current decoding submode.
Protected mode supplies descriptor-based execution; IA-32e activation additionally requires PAE paging and a long-mode-capable code descriptor. Compatibility and 64-bit execution are then selected per code segment.

Name the modes precisely #

The Intel 64 and IA-32 Software Developer's Manual defines three distinctions that are often compressed into the word “mode”:

Architectural state How it is selected Central semantics
Real-address mode reset state; CR0.PE = 0 16-bit segmented addressing, no descriptor-based protection
Protected mode CR0.PE = 1, IA-32e inactive descriptors, privilege checks, 16/32-bit code, optional legacy paging
IA-32e mode IA32_EFER.LMA = 1 64-bit paging architecture plus compatibility and 64-bit submodes
Compatibility submode IA-32e active, CS.L = 0 legacy 16/32-bit code semantics under IA-32e paging/event machinery
64-bit submode IA-32e active, CS.L = 1, CS.D = 0 64-bit general-purpose execution and canonical linear addresses

“Long mode” is common shorthand for IA-32e mode, but it can hide the compatibility/64-bit split. LMA=1 does not by itself make the current instruction stream 64-bit. The current CS descriptor makes that final selection.

Likewise, mode and privilege are different axes. Ring 3 code can be 32-bit compatibility code or 64-bit code; ring 0 code can be either too. Operand size, address size, paging, and privilege must be inspected independently.

Why tables come before the switch #

Protected execution turns several ordinary-looking registers into selectors. A selector does not contain a segment base; it names a descriptor and requests a privilege level:

text
15                         3  2  1 0
+---------------------------+---+---+
|          index            |TI |RPL|
+---------------------------+---+---+
  • index chooses an 8-byte descriptor.
  • TI=0 selects the Global Descriptor Table (GDT); TI=1 selects the current LDT.
  • RPL is the selector's Requested Privilege Level.

GDTR and IDTR are not selectors. Each holds a table base and byte limit. LDTR and TR are selector-backed system registers with hidden cached state.

When software loads a segment register, the processor validates the selector and descriptor, then caches the descriptor's base, limit, type, privilege, and other attributes in the hidden part of that segment register. Editing the GDT entry afterward does not update an already loaded cache. Reload the selector—or perform the architecturally defined control transfer—when the changed descriptor must take effect.

This is why a mode switch cannot safely “set the bit and build the GDT later.” Immediately after CR0.PE becomes 1, segment references and control transfers are interpreted through protected-mode rules. At minimum, valid code/data descriptors and a loadable GDTR must already exist.

Entering protected mode #

The robust sequence is an invariant-preserving transition, not a recipe with freely movable lines:

  1. Place a minimal GDT and the post-switch code in reachable RAM.
  2. Prevent maskable interrupts with CLI; arrange that no exception or NMI needs an unavailable handler during the transition.
  3. Execute LGDT with the GDT base and limit.
  4. Set CR0.PE = 1 with MOV CR0.
  5. Immediately execute a far JMP or far CALL to a valid protected-mode code selector. This serializes the transition and replaces the old CS cache.
  6. Reload SS, DS, ES, FS, and GS with valid selectors or allowed null selectors; establish a known stack.
  7. Load TR if privilege stack switching will be used and IDTR with a protected-mode IDT.
  8. Enable interrupts only after their gates, handler code, controller routing, and stacks are ready.

A schematic NASM-like skeleton is:

asm
cli
lgdt [gdtr]

mov eax, cr0
or  eax, 1                  ; CR0.PE
mov cr0, eax
jmp CODE32_SEL:protected_entry

[bits 32]
protected_entry:
    mov ax, DATA32_SEL
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, protected_stack_top
    lidt [idtr]

It is deliberately incomplete: descriptor addresses, A20, NMI control, PIC/APIC policy, TSS, and paging depend on the enclosing system. Its important property is the adjacency of MOV CR0 and the far branch. Intel warns that unrelated instructions between them can fail unpredictably in some circumstances.

If paging is enabled in the same transition, the instruction that writes CR0 and the following far transfer must execute from identity-mapped memory: the pre-switch linear address and post-switch physical translation must name the same bytes until control reaches code mapped under the new design.

Entering IA-32e mode safely #

Long-mode capability must first be discovered through CPUID. The transition begins in protected mode, with CR0.PE=1, and requires paging to be off while IA32_EFER.LME is changed. The essential sequence is:

  1. Disable or otherwise contain events; install transition-safe exception handling where possible.
  2. Ensure CR0.PG = 0.
  3. Set CR4.PAE = 1.
  4. Load CR3 with the physical base of a valid PML4, or PML5 when five-level paging is deliberately enabled.
  5. Set IA32_EFER.LME = 1 with RDMSR/WRMSR (IA32_EFER is MSR 0xC0000080). Enable NXE separately if no-execute mappings are part of the design.
  6. Set CR0.PG = 1. With PAE and LME already enabled, the processor activates IA-32e mode and reports IA32_EFER.LMA = 1.
  7. Far-transfer to a present code descriptor with L=1 and D=0 to enter the 64-bit submode.
  8. Load 64-bit-appropriate GDTR/IDTR/TR state before permitting events.

The transition page tables must map the instruction sequence, its data, current stack, target code, target stack, and table descriptors at the addresses actually used. When a 32-bit protected-mode stub writes CR3 before IA-32e activation, the paging root must be below 4 GiB because only the low 32 bits can be supplied by that instruction. It may be relocated later.

CR0.PG, CR4.PAE, and IA32_EFER.LME are consistency-checked as a set. Examples that generate #GP include enabling IA-32e paging without PAE, changing LME while paging is active, disabling PAE while IA-32e mode remains active, or activating IA-32e while the current CS already has its L bit set.

The far transfer matters here for the same reason it mattered when enabling protected mode: architectural mode bits establish what is possible, while the loaded CS cache establishes how the current instruction stream is decoded.

The GDT still matters in 64-bit mode #

64-bit mode largely flattens ordinary segmentation: bases for CS, DS, ES, and SS are treated as zero, and most legacy limits are not used for linear-address formation. That does not make the GDT obsolete.

The GDT still supplies:

  • the code descriptor whose L, D, present, executable, and DPL fields validate 64-bit execution;
  • data/stack selector semantics needed by transitions and compatibility code;
  • the 16-byte 64-bit TSS descriptor loaded through LTR;
  • optional LDT and call-gate system descriptors;
  • privilege metadata used during control transfers.

FS and GS are deliberate exceptions to the flat-base simplification: their bases can come from dedicated model-specific registers and are widely used for thread-local and per-CPU data.

The first GDT entry is conventionally null because selector zero has special meaning. A minimal 64-bit kernel GDT commonly contains null, kernel code, kernel data, user code, user data, and a two-slot TSS descriptor. “Flat” means bases/limits no longer partition ordinary 64-bit addresses; it does not mean selectors, descriptor validation, or privilege have disappeared.

CPL, DPL, and RPL answer different questions #

x86 defines four privilege levels numbered 0 through 3; lower numbers are more privileged. Most general-purpose operating systems use ring 0 for the kernel and ring 3 for applications, leaving rings 1 and 2 unused.

Three values participate in descriptor checks:

  • CPL (Current Privilege Level) describes the currently executing code and is represented by the low bits of CS/SS.
  • DPL (Descriptor Privilege Level) is a property of a segment or gate.
  • RPL (Requested Privilege Level) is carried in a selector and can intentionally reduce the authority of a reference.

For ordinary data-segment access, the effective request is commonly summarized as max(CPL, RPL), which must be no more privileged than the descriptor permits. Code segments, conforming segments, call gates, interrupt gates, stack segments, and returns have distinct rules; the summary is not a universal comparison formula.

Paging forms a second protection layer. In the mainstream flat 64-bit model, page-table permissions carry most memory isolation:

  • U/S separates supervisor-only from user-accessible pages;
  • R/W, together with CR0.WP, controls writes, including whether supervisor code honors read-only mappings;
  • NX, when enabled through IA32_EFER.NXE, prevents instruction fetches;
  • SMEP and SMAP can further constrain supervisor execution from and access to user pages.

Segmentation may authorize an address while paging rejects it. The processor performs the relevant checks in sequence, so a correct diagnosis identifies which layer generated the exception rather than describing all protection failures as “a ring problem.”

The IDT is a checked dispatch table #

IDTR gives the base and byte limit of the Interrupt Descriptor Table. An interrupt vector indexes that table; in 64-bit mode each interrupt/trap gate is 16 bytes, so vector n selects IDTR.base + 16n. There are at most 256 vector slots.

A 64-bit interrupt or trap gate contains a target code selector, a canonical 64-bit handler offset, DPL, present bit, gate type, and an Interrupt Stack Table selector. The target must be a 64-bit code segment. The processor validates the gate and target descriptor before transferring control.

The DPL of an interrupt/trap gate is checked for software-generated INT n, INT3, and INTO paths, preventing ring 3 from invoking a ring-0-only gate. Hardware interrupts and processor-detected exceptions ignore that gate-DPL admission check; otherwise a user-mode page fault could be blocked from reaching the kernel that must handle it.

An interrupt gate clears IF after saving the old flags, preventing ordinary maskable interrupts from nesting until software reenables them. A trap gate leaves IF unchanged. Both clear tracing-related state defined by the architecture and both return through IRET/IRETQ, not an ordinary RET.

event vector n exception · IRQ or software INT IDT gate n present · type · DPL selector · RIP · IST software INT checks gate DPL GDT code descriptor present · executable · privilege 64-bit target: L=1 · D=0 validate destination canonical target RIP allowed CPL transition interrupt gate clears IF current TSS RSP0..2 · IST1..7 loaded through TR select stack IST if nonzero else RSPn on privilege change push frame SS · RSP RFLAGS CS · RIP optional error 64-bit handler software saves ABI state return with IRETQ If FRED transitions are enabled, this classic IDT-gate path is replaced by the FRED event-delivery contract.
The IDT chooses the gate, the gate and GDT choose executable code, and the TSS/IST may replace the stack before the hardware frame is written.

The TSS is a stack-switch structure, not a scheduler #

In 64-bit mode, hardware task switching is not supported. The Task-State Segment remains essential because it supplies:

  • RSP0, RSP1, and RSP2 for interrupt-driven transfers to a numerically lower privilege level;
  • seven Interrupt Stack Table pointers, IST1 through IST7;
  • the I/O permission bitmap boundary when that facility is used.

The kernel creates a present 64-bit TSS descriptor in the GDT and loads its selector with LTR. On event delivery:

  1. A nonzero IST field selects the corresponding known stack regardless of whether CPL changes.
  2. Otherwise, a privilege transition uses the destination privilege's RSPn from the current TSS.
  3. Otherwise, delivery continues on the current stack.

IST is most valuable when the ordinary kernel stack may itself be the reason delivery is failing. A double-fault handler on a dedicated IST stack can still run after stack overflow or a bad stack mapping that would prevent an ordinary handler frame from being written. NMI and machine-check handlers are other common candidates, depending on the kernel's nesting and failure policy.

What hardware writes on 64-bit event entry #

In 64-bit mode, interrupt-frame items are 8-byte pushes. The processor saves SS:RSP unconditionally, not only when privilege changes, and aligns the handler stack according to the architectural event-entry rules. Conceptually, it saves:

text
old SS
old RSP
RFLAGS
old CS
old RIP
[exception error code, for vectors that define one]

Because the stack grows downward, the optional error code is at the current top after entry. Some exceptions push an error code and others do not. Assembly stubs usually normalize this difference before calling common code—for example by supplying a synthetic zero for vectors without a hardware code—then remove exactly the normalized frame they created. IRETQ does not remove an exception error code for the handler.

The entry stub must also save every general register it promises to preserve, clear the direction flag before calling ordinary compiled code if that ABI requires it, manage SIMD state according to kernel policy, and keep the stack aligned for the compiler's calling convention. Hardware constructs an architectural return frame; it does not construct a C ABI frame.

Worked trace: a ring-3 page fault #

Suppose 64-bit user code at CPL 3 accesses an unmapped page:

  1. Paging detects the violation and raises #PF vector 14. CR2 records the faulting linear address; the error code classifies present/protection, read/write, user/supervisor, instruction-fetch, and newer conditions.
  2. The processor indexes IDT entry 14. Because this is a processor-detected exception, the gate's DPL does not block delivery.
  3. The gate targets a ring-0 64-bit code descriptor. CPL changes, so the processor selects RSP0 from the TSS unless the gate requests an IST stack.
  4. It saves the old user SS:RSP, RFLAGS, CS:RIP, and the page-fault error code on the kernel stack, then begins the handler with interrupts masked if entry used an interrupt gate.
  5. The assembly entry stub saves remaining registers and calls the page-fault policy with the saved frame, error code, and CR2.
  6. The kernel installs a mapping and retries, grows a stack under policy, delivers a user signal/exception, or terminates the task. A valid return removes the software frame and error code, then uses IRETQ to restore user state.

Every line is a separate checkable boundary. A wrong page permission, missing IDT entry, non-present code descriptor, bad TSS RSP0, unmapped handler, and malformed return frame are different failures even if a virtual machine eventually displays the same reboot.

Fault cascades and the apparent “triple fault” #

A fault during delivery does not automatically become a double fault. Intel classifies exception combinations and delivers them serially when possible. Certain contributory/page-fault combinations while invoking a first handler generate #DF (vector 8), an abort whose saved execution state is not resumable.

If another contributory exception or page fault occurs while the processor is attempting to invoke the double-fault handler, the processor enters shutdown mode. Platform hardware or a virtual machine commonly responds by resetting, which is why the symptom is called a triple fault reset. Architecturally, the decisive event is failed double-fault delivery followed by shutdown; “three arbitrary faults occurred” is not the rule.

Debug the first invalid dependency, not the final reset:

  1. Was IDTR.base + gate_offset mapped and inside the limit?
  2. Was the gate present, the correct 64-bit type, and aimed at a canonical RIP?
  3. Did its selector name a present L=1, D=0 code segment?
  4. Was the TSS descriptor valid and TR loaded?
  5. Was the selected RSP0 or IST pointer canonical, mapped, writable, and sufficiently deep?
  6. Was the handler's own code and data mapped under the active CR3?
  7. Did the stub account for the hardware error code and build an exact IRETQ frame?

A dedicated double-fault IST stack plus an extremely small handler that records state without touching complex subsystems converts many silent resets into evidence.

IDT delivery and FRED are different mechanisms #

Current Intel architecture also defines Flexible Return and Event Delivery (FRED). When FRED transitions are enabled, events do not use IDT descriptors and much of the classic IDT/IRET path described above does not apply. FRED is an alternative architectural contract, not a new kind of IDT gate.

The correct design rule is explicit selection: either initialize and reason about classic IDT delivery, as this chapter does, or initialize FRED and follow its event-record and return rules. Do not combine invariants from both paths.

A compact bring-up proof #

Before enabling events in a new mode, capture or assert this state:

State Proof question
CR0 Are PE and PG set exactly for the intended stage?
CR4 Is PAE set before IA-32e paging, with optional features enabled only after their structures exist?
IA32_EFER Are LME, read-only LMA, and optional NXE mutually consistent?
CR3 Does it name an aligned, reachable root whose walk maps transition code, stacks, tables, and handlers?
CS Does the cached descriptor select 16-, 32-, compatibility-, or 64-bit execution as intended?
GDTR Is its base mapped and limit large enough for every referenced descriptor?
IDTR Is its base mapped and limit large enough for the highest enabled vector?
TR/TSS Is the 64-bit TSS descriptor present, and are required RSPn/IST stacks valid?
RSP Is it canonical, mapped, writable, aligned, and owned by this execution context?
interrupts Are controller routes, gates, handlers, and end-of-interrupt policy ready before STI?

Then provoke controlled events one layer at a time: a software breakpoint, a known hardware timer, a deliberate page fault with a valid handler, and finally a privilege transition. For each, record vector, error code, saved CS:RIP, saved SS:RSP, CR2 where applicable, and the active CR3. A successful handler print without a successful IRETQ proves entry only; it does not prove the return frame.

Visual source audit #

The current Intel manual's reset-state figure, IA-32/IA-32e system-register diagrams, GDT/LDT layout, protection-rings diagram, IDT dispatch and stack-frame figures, and mode-switch pages were rendered and inspected at readable scale. The all-in-one system diagrams are authoritative but too dense at article width, while the ring illustration hides the operational checks among CPL, DPL, RPL, paging, gates, and stacks. The retained SVGs redraw only the two load-bearing relationships needed here—mode-transition prerequisites and event-delivery dependencies—without copying vendor styling or omitting the conditions that cause real bring-up failures. Labels, direction, gate selection, stack selection, and the classic-IDT/FRED boundary were checked against the 2026 manual.

Sources

  1. docsIntel 64 and IA-32 Architectures Software Developer's Manualpdf

Searches titles, aliases, headings, summaries, and full text.↑↓ move ↵ open esc close