Login
Phase 5: Page faults and emulated devices
Login

xv6 §5, "Page faults". "The combination of page tables and page faults is a powerful tool."

Phase 5 turns a guest's accesses to memory it does not own into useful work. A fault on a device page becomes an emulated register access. A fault on unbacked RAM becomes lazy allocation. On x86, devices can also be reached through I/O ports rather than memory; those accesses arrive as port I/O exits, already decoded by 4.3b, and feed the same device dispatcher. The phase starts from Phase 3's guest address space (3.5) and Phase 4's run loop (4.3a/b), on boots that run native by default (2.10) and enter hypervisor mode when asked to, as Phase 4 describes (4.3a). It leaves Phase 6 an emulated UART and virtio devices that guests can use by polling; Phase 6 adds interrupts so Linux can use them. Slices are single check-ins for both architectures except where the plan's guest ABI differs: Arm guests get virtio over memory-mapped registers (virtio-mmio) and x86 guests get virtio over PCI (virtio-pci), so those two transports are separate slices.

Provisional slices. Each is refined, with its quoted sentence, when the phase starts.

Slices

5.1 Decoding second-stage memory faults. From the trap information (the ESR_EL2 and HPFAR_EL2 registers on Arm, which hold the fault's syndrome and guest address, and, on x86, the VMCB's exit fields, which hold the same facts for an NPT fault), Glint extracts the faulting guest address, the access size, whether it was a read or a write, and the register involved. It covers memory faults only; port I/O exits are already decoded by 4.3b. It opens the phase because every later slice acts on what it decodes. The decoder is ordinary Crystal checked against recorded fault records in specs, which is Demonstrability. Its passage is MOS4/5 §3.6.2 ("Page Fault Handling").

5.2 One device dispatcher. Glint maps address ranges to Crystal device models, and an access becomes a read or write call on the model. Ranges live in two address spaces: guest-physical memory (from 5.1) and, on x86, the I/O-port space (from 4.3b's port exits). It comes after 5.1 because it consumes 5.1's decoded accesses, and before any device so that every device plugs into it. One dispatcher keyed on ranges in both spaces, instead of one mechanism per device or per architecture, is Parsimony in Tambassi's sense [39]. Hailperin §7.5.2 makes the underlying point that a trapped device access becomes emulation.

5.3 An emulated UART. A PL011 model on Arm and a 16550 model on x86 (reached through I/O ports), both registered with 5.2's dispatcher. The test guest from 4.2 now prints through a device instead of a call to Glint. It is the simplest device a guest can use, so it comes first among the devices; starting with the smallest device that proves the dispatcher works is Parsimony. Both architectures land together.

5.4 The virtio-mmio transport (aarch64). Glint emulates the register block and the status handshake (the steps by which a guest discovers, configures and starts a virtio device) from the virtio specification's memory-mapped transport (§4.2), registered with 5.2's dispatcher, with no device behind it yet. It comes before any virtio device because every virtio device on Arm shares it. Implementing the transport once, apart from the devices, is Parsimony.

5.5 The virtio-pci transport (x86_64). x86 guests find virtio devices on a PCI bus, as What Linux sees in the plan specifies. Glint emulates a minimal PCI host bridge (just enough configuration space for a guest to enumerate devices) and the virtio-pci transport, through I/O ports only: configuration space at ports 0xcf8 and 0xcfc, and virtio's legacy interface in an I/O-port BAR, as QEMU offers it (hw/virtio/virtio-pci.c line 2155). x86 guests reach no device through memory (decision log, x86 devices through I/O ports only). It follows 5.4 because the architecture rule lands aarch64 first. Presenting the same virtio devices through either transport, so a device model never knows which bus it is on, is Loose Coupling.

5.6 A virtio console. The first virtio device: a console with one virtqueue (a shared ring of buffers) that the test guest polls, running on 5.4's transport on Arm and 5.5's on x86. It comes after both transports so it can be shown on both architectures in one check-in. MOS4/5 §7.7 explains why paravirtual devices beat emulating real hardware: a guest that says what it wants needs no guessing from register writes. A device that works unchanged over two transports is Loose Coupling made visible.

5.7 Lazy guest memory. Guest RAM pages are backed with a page from 3.1 only when 5.1's decoder reports a first touch, not at boot. It comes last because it reuses 5.1's decoder for a second purpose once the device path (5.2–5.6) is working. Memory is spent only where the guest actually uses it and the fault path is shared rather than duplicated, which is Parsimony. Its passage is xv6 §5.1 ("Lazy allocation"), with Hailperin §6.2.7 ("Efficient Zero Filling"). Both architectures land together.

Phase demo: booted with bin/glint-run aarch64 --mode hypervisor (and x86_64), with nothing typed, the test guest prints through the emulated UART and through the virtio console, and its memory use grows only as it touches pages.

References