Read

Real mode, protected mode, and the 16-bit past that never went away

Why every x86 processor still boots as an 8086, what the A20 gate was for, and the exact assembly that climbs out of 1978 into a flat 4 GB address space.

A processor manufactured in 2026 begins executing in a mode designed for a chip released in June 1978. It can address one megabyte. It has no memory protection, no virtual memory and no notion of a privileged instruction. Several hundred million transistors sit idle while it pretends to be an 8086, and it stays that way until software tells it to stop. That is not an oversight — it is a compatibility promise nobody has been willing to break, and it is why the first job of every x86 kernel is to escape the machine it was handed.

The processor wakes up as an 8086

Out of reset, CS is 0xF000 and IP is 0xFFF0. On an actual 8086 that resolves to physical address 0xFFFF0, sixteen bytes below the top of the megabyte — which is why the first thing at that address on every PC ever built is a jump instruction. A 386 and everything since quietly cheats: the hidden base behind CS starts at 0xFFFF0000, so the first fetch really happens at 0xFFFFFFF0, where the firmware ROM is mapped. The illusion holds until the first far jump reloads CS.

Segment, offset, and the megabyte

The 8086 had 16-bit registers and 20 address pins. Twenty bits is a megabyte; sixteen bits is 64 KB. Intel bridged the gap by making every memory reference a pair — a segment register and an offset, combined as (segment << 4) + offset. Segment 0x07C0 offset 0x0000 and segment 0x0000 offset 0x7C00 name the same byte, which is exactly the ambiguity that catches people writing their first boot sector.

The arithmetic has an awkward consequence. The largest address you can name is 0xFFFF:0xFFFF, which is 0xFFFF0 + 0xFFFF = 0x10FFEF — 65,520 bytes past the end of the megabyte, or 64 KB minus 16. The 8086 had no twenty-first address pin, so those addresses silently wrapped to the bottom of memory. Writing to 0x100000 wrote to 0x00000.

The A20 gate

Programmers used the wraparound. Tim Paterson relied on it in QDOS around 1980 for CP/M CALL 5 compatibility; Microsoft Pascal used it deliberately in 1981; the widely-used EXEPACK compressor spread the dependency through a great deal of shipped software.

Then came the IBM PC/AT in 1984, with an 80286 and 24 address lines on which those addresses no longer wrapped, and all of it broke. IBM's fix is one of the genuinely absurd artefacts of computing history: they routed address line 20 through a spare output pin on the Intel 8042 keyboard controller and left it forced low by default. To use more than a megabyte you had to ask the keyboard for permission — a command to port 0x64, data to port 0x60, and a wait for a chip designed to debounce key switches.

The PS/2 added a faster route in 1987: set bit 1 of port 0x92 and the chipset does it directly. The 486 pulled the logic into the CPU via the A20M# pin. And the 65,520 bytes that A20 unlocked got a name — the High Memory Area, which HIMEM.SYS managed and where DOS=HIGH put the resident part of DOS.

The 286, and a door that only opened one way

The 80286 of 1982 introduced protected mode. Segment registers stopped being shift-and-add arithmetic and became selectors: indexes into a table of descriptors, each carrying a base address, a size limit and a set of permissions. A segment could now be marked read-only, or executable-but-not-readable, or off-limits to unprivileged code, and the hardware enforced it.

It also had a famous flaw: Intel provided no documented way back. Since DOS lived in real mode, an operating system that wanted both had to reset the processor to return — by triple-faulting it on purpose, or by pulsing the CPU reset line through, once again, the keyboard controller. The 80386 in 1985 fixed that, widened everything to 32 bits, and added paging and virtual 8086 mode.

The GDT, descriptors and rings

The Global Descriptor Table is an array of 8-byte entries in ordinary memory. Entry zero must be null, so that an uninitialised segment register faults loudly instead of quietly addressing memory. Each real entry packs a 32-bit base, a 20-bit limit, an access byte and a flags nibble into a layout that made sense on a 286 and has been extended awkwardly since.

The limit counts bytes or, with the granularity flag set, 4 KB pages — which is how a 20-bit field describes 4 GB. A selector is 16 bits: bits 3 to 15 index the table, bit 2 chooses the global or a local table, bits 0 to 1 are the requested privilege level. That is why the first usable descriptor is selector 0x08 and the second 0x10.

Privilege runs from ring 0 to ring 3, and almost nobody uses the middle two: Unix-shaped systems put the kernel in ring 0 and everything else in ring 3, because that model ports to architectures with two privilege levels. The exceptions are instructive — OS/2 used ring 2 for I/O-privileged code, and 32-bit Xen ran paravirtualised guest kernels in ring 1 so the hypervisor could keep ring 0.

The switch, in full

  1. Disable interrupts with cli. Between setting the PE bit and reloading the segment registers the CPU is inconsistent, and the real-mode interrupt vector table is about to become meaningless. Serious code also masks the NMI via bit 7 of port 0x70.
  2. Enable A20, or half your address space stays folded on top of itself.
  3. Load the GDT with lgdt, which takes six bytes: a 16-bit limit that is the table size minus one, then the 32-bit linear base address of the table.
  4. Set bit 0 of CR0. This single instruction is the mode switch. The processor is now in protected mode with a stale, real-mode CS.
  5. Far jump. CS cannot be loaded by mov; only a far jump, call or return changes it. The jump loads a proper 32-bit code descriptor, whose D bit makes everything after it decode as 32-bit, and it serialises execution — traditionally described as flushing the prefetch queue of instructions fetched under 16-bit assumptions.
  6. Reload DS, ES, FS, GS and SS with a data selector, and set ESP. Until you do, they hold real-mode segment values the descriptor tables will not recognise.
; ---------- still 16-bit real mode ----------
    cli

    in   al, 0x92               ; Fast A20 gate, PS/2 and later
    or   al, 2
    out  0x92, al

    lgdt [gdtr]                 ; where the descriptors live

    mov  eax, cr0
    or   eax, 1                 ; PE -- this instruction is the switch
    mov  cr0, eax

    jmp  0x08:pm32              ; load CS from GDT entry 1

; ---------- 32-bit protected mode ----------
bits 32
pm32:
    mov  ax, 0x10               ; GDT entry 2: flat 4 GB data
    mov  ds, ax
    mov  es, ax
    mov  fs, ax
    mov  gs, ax
    mov  ss, ax
    mov  esp, 0x90000           ; a stack, at last

    mov  dword [0xB8000], 0x2F502F4D   ; M and P, white on green
    jmp  $

; ---------- the table ----------
align 8
gdt:
    dq 0x0000000000000000       ; 0x00  null, required
    dq 0x00CF9A000000FFFF       ; 0x08  code: base 0, limit 4 GB, ring 0
    dq 0x00CF92000000FFFF       ; 0x10  data: base 0, limit 4 GB, ring 0
gdtr:
    dw gdtr - gdt - 1           ; limit = 24 bytes minus one = 23
    dd gdt                      ; base

The two magic quadwords decode as base 0x00000000, limit 0xFFFFF, 4 KB granularity (so 4 GB), 32-bit operands, present, ring 0; access byte 0x9A is executable and readable, 0x92 is data and writable. The screen write at the end is there because INT 10h no longer exists — the BIOS was real-mode code, and setting PE puts it out of reach. Bytes at 0xB8000 are the only output left.

Flat memory, and why segmentation lost

Having gone to all that trouble for descriptor-based segmentation, essentially everyone then configures it away: base 0, limit 4 GB on every segment, so a linear address equals an offset and the segment registers stop mattering. Linux does it, Windows does it, every teaching kernel does it.

The reasons are practical. C assumes a flat pointer, so segmented code needs near and far pointers and stops being portable. Segments are the wrong granularity for demand paging. And no other architecture worth porting to had segmentation. AMD settled it in x86-64: in 64-bit mode the base and limit of CS, DS, ES and SS are ignored outright. Only FS and GS keep a usable base, which operating systems use for thread-local storage.

Paging

What replaced it is paging, introduced on the 386 and switched on by setting bit 31 of CR0. It works on fixed 4 KB pages and is a two-level lookup. CR3 holds the physical address of a page directory: 1024 entries of 4 bytes, exactly one page. Each entry points at a page table of another 1024 entries, each naming a 4 KB frame. 1024 x 1024 x 4096 is exactly 4 GB.

A 32-bit virtual address splits 10 / 10 / 12: ten bits index the directory, ten the table, twelve the offset within the page. Each entry carries permission bits — present, writable, user-accessible, accessed, dirty — and it is those, not segment descriptors, that protect a modern kernel. Walking two tables per access would be ruinous, so the CPU caches translations in the Translation Lookaside Buffer, and changing a mapping means invalidating the stale entry: invlpg since the 486, or a write to CR3.

Long mode, and why paging comes first

64-bit mode arrived with AMD's Opteron in 2003, and you cannot reach it directly from real mode. From protected mode: turn paging off, set the PAE bit in CR4 so page table entries become 8 bytes wide, build a four-level hierarchy from the PML4 down, point CR3 at it, set the LME bit in the EFER MSR at 0xC0000080, and only then turn paging back on. Setting LME arms the transition; enabling CR0.PG performs it.

That ordering is not a convention, it is the architecture: long mode has no unpaged variant. The page tables must already be valid at the instant the mode changes, including a mapping covering the instruction pointer — otherwise the first fetch after the switch faults with no handler installed.

PropertyReal modeProtected modeLong mode
Introduced8086, 197880286 in 1982, 32-bit on the 80386 in 1985AMD Opteron, 2003
Address width20 bits32 bits48-bit virtual, 4-level paging
Addressable1 MB, plus 64 KB with A20 on4 GB256 TB of virtual address space
SegmentationMandatory, (seg << 4) + offDescriptor-based, usually flattened awayIgnored except FS/GS bases
PagingNoneOptionalMandatory
Privilege levelsNoneRings 0-3, enforcedRings 0-3, enforced
Runs in TempMVYesYes, with pagingNo

Where the emulator stops

v86, the emulator TempMV runs on, implements a 32-bit 686-class processor: real mode, protected mode, paging, virtual 8086 mode, and an instruction set reaching SSE3. Everything above except the long mode section works there unmodified, on an emulated CPU that faults properly if you get a descriptor wrong. What is missing is a short, specific list — task gates, far calls in protected mode, some 16-bit protected mode features, debug registers and the trap flag.

Long mode is not on that list because it was never started. That single fact sits behind everything the catalog does and does not contain: a 64-bit kernel gets as far as setting LME in EFER, finds it does not stick, and stops. It is not a performance limit or a missing driver — the mode does not exist in the emulated processor, which is why a 2026 Ubuntu ISO will never boot in a tab.

To watch a mode switch rather than read about one, build a floppy following write your own boot sector, paste the assembly above into it and mount it at /new. For the firmware side see BIOS and UEFI; for how the CPU is built out of WebAssembly see how x86 emulation works in the browser; the glossary defines the terms above.