Skip to content

Port I/O versus Memory-Mapped I/O

x86 hardware exposes two entirely separate address spaces for talking to devices, and nearly every driver article on this wiki uses one or the other without pausing to explain the choice: port I/O, a dedicated 64 KB space reached only through the IN and OUT instructions, and memory-mapped I/O (MMIO), where a device’s registers simply occupy a range of ordinary physical addresses and are touched with the same load and store instructions used for RAM. Both exist on x86; deciding which one a given piece of hardware uses is a property of that hardware’s design, not something software gets to choose, though a single controller sometimes offers both for the same functionality.

The IN and OUT instructions address a 16-bit port space, entirely separate from the normal memory address space and not reachable through ordinary loads and stores at all: in al, 0x60 reads a byte from port 0x60 (the PS/2 keyboard’s data port), and no pointer, page table entry, or memory address is involved in that operation. PCI configuration space, before the memory-mapped MCFG mechanism PCI Express introduces, is a canonical example: software writes a packed address to CONFIG_ADDRESS (I/O port 0xCF8) and reads or writes the actual data through CONFIG_DATA (0xCFC), two fixed ports standing in for what MMIO would otherwise expose as a large range of addressable registers.

mov dx, 0x60
in al, dx ; read one byte from port 0x60

Ring 3 code executing IN/OUT directly is not automatically forbidden the way a ring 3 write to kernel memory is; instead, it’s gated by IOPL (the I/O Privilege Level field in EFLAGS) together with the TSS’s I/O permission bitmap, one bit per port, letting a kernel grant a specific unprivileged process direct access to specific ports (a userspace driver for one particular device, for instance) without granting it blanket access to the entire port space or requiring every access to trap into the kernel first.

MMIO instead places a device’s registers at fixed physical addresses, mapped into the same address space RAM occupies, and accessed with the exact same instructions (MOV, and anything built on it) that touch ordinary memory. AHCI is a concrete, already-documented example: once a controller’s ABAR (its PCI Base Address Register) is mapped, every register described there, port command lists, the FIS receive area, interrupt status, is read and written as an ordinary struct member through a pointer, with no special instruction involved at all.

volatile uint32_t *ghc = (volatile uint32_t *)(abar_base + 0x04);
*ghc |= (1 << 31); // set AHCI Enable

The volatile qualifier above is not decorative: a compiler that doesn’t know an address maps to hardware, rather than ordinary RAM, is free to reorder, cache in a register, or entirely eliminate what looks like a redundant load or store, all of which are correct optimizations for memory and silently wrong for a device register that can change on its own or where the write itself, not just its eventual value, is what triggers a side effect.

Port I/O is architecturally specific to x86 (and a handful of close relatives); an MMIO-based device design carries over largely unchanged to any architecture with a flat physical address space, which by itself accounts for a large share of the industry’s drift toward MMIO for new hardware designs. MMIO registers are also directly reachable by any instruction that can touch memory at all, including ones a compiler already generates routinely, where port I/O is reachable only through the two dedicated instructions and whatever wrapper functions a kernel builds around them; PCI Express formalized this shift by adding a fully memory-mapped configuration mechanism (MCFG) alongside the legacy I/O ports, which most controllers still answer for their first 256 bytes purely for backward compatibility. Port I/O has not disappeared even from modern systems: it survives specifically where staying compatible with decades of existing software (the legacy PCI configuration ports, PS/2, the classic serial UART) outweighs whatever benefit switching to MMIO would bring for a peripheral that isn’t being redesigned anyway.

A physical address range backing MMIO registers must be marked uncacheable in the page tables mapping it, typically via the PCD bit or a PAT entry selecting an uncacheable memory type; leaving it cacheable lets the CPU silently serve a read from a stale cached copy instead of the device’s current register value, or delay a write in the cache indefinitely instead of actually reaching the device when the driver expects it to. Port I/O has no equivalent caching concern, since IN/OUT are defined to always reach the device directly, which is one of the few respects in which the older mechanism is simpler to reason about correctly than its replacement.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Chapter 3.1: I/O address space and the IN/OUT instruction family.
  2. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 1, Chapter 11: memory type range registers and the PAT, which govern how MMIO regions are cached.
  • PCI: configuration space accessed through legacy I/O ports, with a memory-mapped alternative in newer systems.
  • AHCI: a fully memory-mapped register interface, once its BAR is located through PCI.
  • Paging & Virtual Memory: where an MMIO region’s uncacheable attribute is actually set.
  • The Task State Segment: the I/O permission bitmap that governs ring 3 access to the port space.