Skip to content

Protected Mode

Protected mode is the CPU mode that introduces the machinery essentially every modern operating system depends on: memory protection enforced by hardware rather than convention, and multiple privilege levels the CPU actively checks rather than merely documents. Where real mode trusts every piece of running code equally, protected mode gives the CPU a place (descriptor tables) to record what each region of memory is for and who is allowed to touch it, and enforces those records on every access.

Real mode computes an address as segment × 16 + offset, with the segment value used directly with no lookup involved. Protected mode reinterprets the segment register’s contents entirely: rather than a shiftable base address, it becomes a selector (an index into a descriptor table, plus a few low bits recording the requested privilege level), and the CPU looks up the actual segment base, limit, and access rights from the table the selector points into. This indirection is what makes protection possible at all: software can freely load any selector value, but the CPU only permits an access if the corresponding descriptor’s rights allow it, and only within the range the descriptor’s limit specifies.

A segment selector’s index field selecting a GDT entry, whose descriptor supplies the segment’s base, limit, and access rightsSegment selectorGDTDescriptor (8 bytes)Index15:3TI2RPL1:001234567Base [31:0]Limit [19:0]Access (P, DPL, type)Flags, Limit [19:16]

The GDT holds the descriptors selectors index into, loaded with the LGDT instruction from a small structure giving the table’s size and address, the same limit/base shape LIDT uses for the interrupt table. Each 8-byte descriptor packs a 32-bit base address, a 20-bit limit (interpreted as either bytes or 4 KB pages depending on a granularity bit), and access rights including the descriptor’s own privilege level and whether it describes code or data.

struct gdt_entry {
uint16_t limit_low;
uint16_t base_low;
uint8_t base_mid;
uint8_t access;
uint8_t granularity; // high nibble: flags; low nibble: limit bits 19-16
uint8_t base_high;
} __attribute__((packed));
The gdt_entry struct’s six fields laid out across its 8 bytes, with the access byte’s four sub-fields (P, DPL, S, Type) broken out belowlimit_lowbytes 0–1base_lowbytes 2–3base_midbyte 4accessbyte 5granularitybyte 6base_highbyte 7access byte, expandedP7DPL6:5S4Type3:0

A GDT conventionally begins with a null descriptor at index 0: loading a null selector is a legitimate way to mark a segment register unused, and the CPU faults if code actually attempts to use it for an access, which is exactly the behavior a deliberately-invalid placeholder should have. Beyond the null entry, a minimal kernel GDT needs at least one code and one data descriptor for ring 0, and typically a matching pair for ring 3 if user-mode execution is supported at all.

Every segment selector carries a two-bit Requested Privilege Level in its low bits, and every descriptor carries its own Descriptor Privilege Level. The CPU compares these (along with the Current Privilege Level, tracked as part of the currently loaded code segment) on relevant operations, refusing an access where the requesting privilege is numerically higher (less privileged) than what the target descriptor permits. This three-way check (CPL, DPL, RPL) is what makes it meaningless for ring 3 code to simply load a ring-0 data selector and expect ring-0 access: the selector’s own privilege bits, combined with the descriptor’s, still block it.

CPL and RPL combining into a requesting privilege level, compared against the target descriptor’s DPL to decide whether the access is permitted or faultsCPLcurrent code segmentRPLselector bits 1:0max(CPL, RPL)DPLtarget descriptorrequesting priv. ≤ DPL?access permitted#GP faultyesno

A further structure, the Task State Segment (TSS), holds the stack pointer the CPU switches to automatically when a privilege-raising interrupt or exception occurs. Without it, a ring 3 → ring 0 transition has no defined kernel stack to land on, which makes a valid TSS a practical requirement for any kernel supporting user-mode code, even though the TSS’s original purpose, hardware task switching, sees little use today.

The transition itself is a short, rigid sequence: load a valid GDT with LGDT, set bit 0 (PE) of CR0, and perform a far jump into a code descriptor’s selector. That far jump does real work: it flushes the CPU’s pipeline of any real-mode-decoded instructions still queued and forces a fresh fetch under the new mode’s decoding rules.

lgdt [gdt_descriptor]
mov eax, cr0
or eax, 1
mov cr0, eax
jmp CODE_SEG:protected_mode_entry

Segment registers other than the code segment (DS, ES, SS, and so on) must also be reloaded with valid protected-mode selectors immediately after the jump: their real-mode values are not automatically reinterpreted, and using them unreloaded reads whatever stale selector value happened to be there, generally faulting the first time it is used for an access.

Protected mode does not, on its own, remove the 1 MB addressing ceiling associated with real mode’s 20-bit address computation: that limit is a side effect of the A20 line, a legacy compatibility feature that masks the 21st address bit unless explicitly enabled, and needs to be enabled separately (through one of several historical mechanisms, most commonly the keyboard controller or a dedicated fast-A20 port) before code can reliably address memory above 1 MB, regardless of which CPU mode is active. Forgetting this step produces a particularly confusing class of bug: memory accesses appear to work, but silently alias to an address 1 MB lower than intended, since the top bit is simply discarded rather than causing any fault.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapters 3 and 5: defines segment descriptors, the GDT, and privilege-level enforcement.
  • x86 Architecture: the broader mode overview this article expands on.
  • Long Mode: the further transition from protected mode into 64-bit execution.
  • The Task State Segment: the full structure this article’s brief TSS mention expands into.
  • The A20 Line: the addressing quirk behind the 1 MB ceiling noted above.
  • Unreal Mode: a trick for reaching memory above that ceiling without performing this article’s full transition.