Login
Phase 6: Interrupts, drivers and the first Linux boot
Login

xv6 §6, "Interrupts and device drivers". "A driver is the code in an operating system that manages a particular device: it configures the device hardware, tells the device to perform operations, handles the resulting interrupts, and interacts with processes using the device." The chapter's "Real world" section adds: "Programmed I/O is simple, but too slow to be used at high data rates."

Phase 6 is where the plan's Concept first comes together. Glint takes real interrupts, drives its own devices and delivers virtual interrupts to a guest. That is enough for an unmodified Linux kernel to boot. The phase also turns the Zephyr seam that Track Z built for the console into the plan's answer to the "Real world" sentence above: the console driver becomes interrupt-driven (6.3), and the first Zephyr storage provider follows (6.10). Firmware drivers poll; Zephyr's are interrupt- and DMA-driven. Three of its slices extend Track Z (6.3, 6.8 and 6.9), as Track Z's What Track Z leaves sets out. The phase starts from Phase 5's emulated devices and Phase 4's run loop, on boots that are native by default since 2.10. It leaves a single-vCPU Linux guest; Phase 8 (8.6) gives it more vCPUs.

Where the two architectures differ substantially, a slice is split into an aarch64 check-in (a) followed by an x86_64 one (b), and both land within the phase.

Refined on Nov 10, 2026 (rule 9). Phase 6 starts with Phases 1–5 done on both architectures, Track S's S.1 and Track U's U.1 merged. The decisions taken in refining it:

Slices

6.1a The Arm interrupt controller. Glint drives the GIC (Arm's Generic Interrupt Controller, version 3 or 2): its distributor, which routes device interrupts, and its per-CPU interface, which delivers them. Physical interrupts then reach handlers through Phase 4's dispatcher (4.1). It opens the phase because every later slice here either takes an interrupt or injects one. Routing every device through the one dispatcher, never through ad hoc paths, is Parsimony in Tambassi's sense, as in 4.1. Its passage is MOS4/5 §5.1.5 ("Interrupts Revisited").

6.1b The x86 interrupt controllers. This is the same job for x86: each CPU's local APIC and the IOAPIC that routes device interrupts to CPUs, wired into the same dispatcher. It follows 6.1a because the two controllers share nothing but the dispatcher they feed. Keeping them as separate check-ins keeps each small enough to read in one sitting, which is Parsimony in the plan's sense.

6.2 Timer interrupts. The Arm generic timer and the LAPIC timer each deliver a periodic tick through 6.1a/6.1b, and Glint keeps time from it. The two timers differ only in a few registers, so this is one check-in. It comes right after 6.1a and 6.1b because a tick needs a controller to arrive through, and Phase 8's preemption (8.3) depends on it. Its passage is xv6 §6.4 ("Timer interrupts"). A tick that specs and a boot test can count serves Demonstrability: Glint's sense of time becomes something that can be shown.

6.3 An interrupt-driven console. The console since 2.6 is Zephyr's own UART driver (PL011 or NS16550), compiled in by Track Z and run in poll mode. Here it stops polling: it is rebuilt with CONFIG_UART_INTERRUPT_DRIVEN, and reads and writes when the UART raises an interrupt through 6.1a or 6.1b. That needs the first interrupt pieces of the Zephyr seam: the devicetree generator learns interrupts cells (and pc-com1.dts gains COM1's interrupt, which Z.3 left out), the shim implements IRQ_CONNECT and irq_enable on 4.1's dispatcher (on x86-64 through arch_irq_connect_dynamic; on arm64 the drivers' .intList entries become a generated table), and Z.9's console provider gains a receive ring, since the driver's callback runs in interrupt context and keeps no buffer (survey section 2). The two architectures register interrupts differently, so this slice may split into 6.3a and 6.3b when refined. It comes after 6.1–6.2 because it is the smallest real use of them, the phase's whole theme (from polling to interrupts) on one device. That keeps the first interrupt-driven driver small enough to check by eye, which is Parsimony. Its passages are xv6 §6.1–6.2 (console input and output).

6.4a Virtual interrupts on Arm. Glint injects interrupts into a guest through the vGIC's list registers (slots the hypervisor fills with pending virtual interrupts), and gives the guest a virtual timer. It is built on 6.1a's controller: a physical interrupt arrives there, and Glint decides which guest it belongs to. Emulated devices from Phase 5 (5.3–5.6) can now raise interrupts instead of being polled. It comes after 6.3 because Glint must handle its own interrupts before it delivers them to anyone else. The guest sees the same interrupt controller it would see on hardware, which serves Loose Coupling: the guest never learns it is talking to Glint.

6.4b Virtual interrupts on x86. On AMD x86, Glint injects interrupts by writing an event into the VMCB (the per-guest control block SVM reads on each entry), built on 6.1b's APICs. It follows 6.4a for the same reasons, and is split from it because the two mechanisms share no code. Keeping VMCB injection behind the same injection call as 6.4a is Loose Coupling.

6.5 Guest firmware. Glint gives the guest a small EFI environment: a system table, a subset of boot services, a memory map and runtime services, plus a description of the guest's machine. Linux's own EFI stub (the small EFI program at the front of every Linux kernel image) boots from this, as What Linux sees in the plan describes. On Arm the description is a devicetree, written at run time by Glint's own allocation-free FDT writer, since Cleave::Fdt cannot run inside the image (Build constraints); a spec checks the writer's output by reading it back with Cleave::Fdt, as 2.4a's reader is checked. On x86 it is a set of ACPI tables, the minimum a Linux guest needs for the machine What Linux sees describes: an RSDP pointing to an XSDT; a FADT with its DSDT and FACS; a MADT listing the guest's local APIC and IOAPIC; and an MCFG giving the configuration space of the PCI bus that carries virtio-pci. The two describe different things and share only the writer's pattern, so the slice may split into 6.5a (devicetree) and 6.5b (ACPI) when refined. The environment is built from the same protocol definitions Phase 1 wrote (1.3, 1.5). Each definition exists once and serves both Glint and its guests, which is Parsimony (Tambassi's rule [39] that each thing should appear only once). It comes before 6.7a and 6.7b because Linux cannot start without it.

6.6 A virtio-blk device for guests. Guests get a virtio block device, built on 5.4's virtio-mmio transport, and on 5.5's virtio-pci on x86. It is backed by whichever BLOCK_IO provider Glint has. In native mode the firmware's provider is gone, so the first backing is a disk image held in memory, preloaded from the ESP (2.9). Swapping that backing later (6.10, Phase 10) needs no change to the device, which is Loose Coupling. It comes before 6.7a and 6.7b because Linux needs a disk to find its initial files on. Its passage is MOS4/5 §7.7 on guest disks as files or regions.

6.7a Linux boots on Arm. An unmodified arm64 kernel Image, with an initramfs (a small in-memory file system holding BusyBox, a single-binary set of basic Unix tools), boots through its EFI stub to a shell on one vCPU. It needs 6.4a's interrupts, 6.5's firmware and 6.6's disk, which is why it comes after all three. dmesg (Linux's kernel log) shows Glint's SMCCC hypervisor UID, the identifier Glint answers with when Linux asks which hypervisor it is running on. This is the phase's central Demonstrability claim, and MOS4/5 §7.2's fidelity requirement is its acceptance test: Linux behaves as it does on bare hardware.

6.7b Linux boots on x86_64. The same demonstration on QEMU's q35 machine with SVM, booting a bzImage (the x86 kernel image format) through 6.4b, with 6.5's firmware and 6.6's disk, as in 6.7a. It comes after 6.7a because the plan brings aarch64 up first in every phase. It is a slice of its own because a demonstration that only one architecture passes would not show what the plan claims. Showing the same guest ABI working on both architectures is the Demonstrability behind the plan's central claim, that to Linux on either architecture Glint looks like a hypervisor.

6.8 Zephyr's build data, extended. Track Z already generates Zephyr's three kinds of build input in Crystal: devicetree_generated.h from the devicetree and Zephyr's YAML bindings (Z.3–Z.5), autoconf.h from a checked symbol table (Z.6), and the syscall stub headers (Z.7), limited to what two poll-mode UARTs reach. This slice extends them to what the first block provider needs: ranges, PCI and chosen in the devicetree generator, beyond the interrupt cells 6.3 added (aliases and node labels only if the driver reaches them), and the provider's Kconfig symbols in the table. Pinctrl, clocks and resets are not among them; What Track Z leaves says where they stand. The provider's driver then compiles from generated headers, and Z.2's hand-written fixtures for it are retired. It comes before 6.9 and 6.10 because the provider cannot compile without them. The acceptance test stays Track Z's: Zephyr's own headers accept the output and the driver compiles with -Werror, which serves Demonstrability.

6.9 The Zephyr shim, extended. Track Z's shim (Z.8) has a device table, an init runner and no kernel object of its own (the NS16550's spinlock is Zephyr's inline one). This slice adds the kernel objects a block driver uses: k_sem, k_mutex, k_work and k_timer on 6.2's timer, with logging routed to Glint's console through CONFIG_LOG_CUSTOM_HEADER. C headers declare them and Crystal implements them, through the C seam proved in 1.4. Its lock objects start as stand-ins that simply mask interrupts on the one CPU running drivers, and a blocking k_sem_take spins with interrupts enabled until Phase 8 brings threads (survey risk 4). 7.5 maps the locks onto Glint's real ones once those exist. It follows 6.8 because the shim's headers include 6.8's generated ones. Keeping Zephyr behind this single seam is Loose Coupling: Glint can replace any Zephyr driver without touching the rest.

6.10 The first Zephyr provider. An upstream Zephyr block driver is built unmodified with clang against 6.8's generated headers and 6.9's shim. It is then wrapped as a BLOCK_IO provider behind 6.6's device. Which driver is the plan's open question 1. The benchmark that plan principle 8 requires compares the firmware's BLOCK_IO in hosted mode (a boot asked to stay hosted, 2.10) with this provider in native mode, reading the same QEMU disk. It comes last in the phase because it needs 6.8, 6.9 and a running guest to serve. Measuring the "Real world" sentence's claim, rather than assuming it, is Demonstrability.

Phase demo: an unmodified Linux kernel boots to a BusyBox shell on Glint (one vCPU, both architectures), and the benchmark shows the Zephyr provider against firmware.

References