CHAPTER 6 · THE CPU
ONE INSTRUCTION AT A TIME
Chapters 2 to 5 cleared away everything that was not a bug — the red herrings, a census read perfectly and misread completely, a dialog nobody could see. What remained were four places where the emulator’s idea of an Intel PC differed from silicon by one bit or one instruction. This chapter is those four mechanisms: an interrupt shadow flattened by batching, a timer parked for a caller that never comes, a status bit that never latched, and a four-megabyte page mistaken for a table of smaller ones.
Four defects, one desktop
When the invisible dialogs of chapter 5 were finally answered — CD in the drive, the product key entered properly — native Windows 2000 setup ran its full course inside mddosem for the first time. That it could was down to four corrections the acceptance record credits by name — three of them landed with fail-first regressions, the fourth under adversarial review — each a statement about what real hardware does rather than what Windows wants. None is Windows-specific; all four were in the interpreter-only binary that finally booted (the dynamic recompiler sat this campaign out). Three landed in one night, 15–16 July; the fourth at five past three in the morning of the 19th, twenty-two minutes before acceptance was recorded.
The four wear the panes of the flag throughout this site. Fixes 1, 2 and 4 are this chapter’s subject in depth. Fix 3 belongs to the interrupt wiring of chapter 3 and gets a summary here.
Fix 1 — the one-instruction shadow
An x86 processor checks for pending interrupts at every instruction boundary — that is
the contract. The IF flag gates it: CLI closes the door,
STI opens it. But STI has a peculiar clause, present since the
8086: interrupts become recognisable not after STI itself, but after the
instruction that follows it. That one-instruction “shadow” exists so software can
compose two instructions atomically. STI; HLT — enable interrupts and
halt, with no gap between them into which a wake-up could fall and be consumed, leaving
the machine asleep forever. STI; RET — re-enable and return before the
next interrupt piles a frame onto the same stack. The same one-instruction grace covers
MOV SS, POP SS and LSS, so a stack switch — segment
then pointer — cannot be split down the middle. And one instruction restores
IF with no shadow at all: POPF/POPFD, which
makes recognition legal at the very next boundary.
mddosem’s interpreter, for speed, runs the guest in grants of up to sixteen
instructions between visits to the scheduler — and the grant machinery had flattened the
shadow’s edges. The regression suite caught three failure shapes red-handed:
STI; DEC ECX; CLI retired three instructions when silicon would deliver the
pending interrupt after two; POPFD; CLI retired the entire sixteen-instruction
grant when the legal count is one; and STI applied its shadow twice,
protecting a second successor no manual describes. Each shape has the same lethal
consequence: a following CLI could close the only boundary at which a pending
interrupt was ever allowed to be recognised.
Why did Windows 2000, of all guests, die of this? Because NT’s ubiquitous
critical-section shape is PUSHFD; CLI; …; POPFD — save the flags, close
interrupts, do the work, restore. Kernel, HAL and drivers execute it constantly. If the
interpreter never opens a poll window between the IF-restoring
POPFD and the next CLI, completion interrupts die in the gap —
and a lost completion is a lost wake-up for exactly the machinery
chapter 4’s census stared at: the kernel-stack inswap
that would have brought setup’s threads back from their long sleep. A DOS game polls;
NT waits. Invent an interrupt-free window no silicon ever had, and NT is the
guest that will park in it.
The fix gives the boundary a name. The interpreter now returns an explicit result,
RunResult::InterruptPollBoundary, after STI, MOV SS,
POP SS, LSS, or any POPF/POPFD that
turns IF on — and the scheduler owns exactly one shadow-protected successor,
run under a budget of one, never two. Inner and outer scheduling share a single readiness
predicate, pending_interrupt_delivery_ready(), which also counts a byte
already sitting in the keyboard controller’s output buffer as deliverable — visible 8042
state matters even before IRQ1 materialises in the PIC. At landing, 2,511
mddosem-interp tests were green; by acceptance, 2,512.
RunResult::InterruptPollBoundary, one
protected successor, one shared pending_interrupt_delivery_ready()
predicate including visible 8042 output. Validation at landing: 2,511 mddosem-interp
tests. Ledger: MDD-BUG-KILN-00004.Fix 2 — the parked timer
The second defect was quieter. In its default configuration mddosem services BIOS
interrupts in Rust — high-level emulation, HLE — and one of its optimisations defers the
bookkeeping of the 8254 timer’s channel-0 counter, leaving the HLE timer path to commit
the deferred track when it next runs. Under --real-boot
(chapter 1), there is no “next runs”: hardware interrupts
vector through the guest’s own IDT, the HLE timer path never executes, and the deferred
commit never comes. The 8254’s counter simply froze between interrupts.
Windows 2000 noticed because it reads that counter. On the Standard PC HAL —
the one this machine’s ACPI-less firmware selects — QueryPerformanceCounter
is the 8254, frequency 1,193,182 Hz. The kernel’s timekeeping and the
scheduler’s wake path consult PIT state; a counter that only moves at interrupt time
starves everything paced between ticks. The fix is a one-condition answer:
can_defer_pit_channel0_track() returns no under native boot, so
channel 0 advances immediately, while the DOS and DPMI HLE paths keep their deferral
untouched. The fail-first regression caught the parked state in the act:
Nine focused channel-0 regressions pin the behaviour
(test_on_pit_channel0_event_real_boot_never_defers_hle_counter_track among
them). A real 8254 decrements on every input clock no matter who handles the interrupt;
there is no “defer until the BIOS looks at me” mode on any board ever soldered.
Fix 3 — the silent status bit
The third correction lives on the wires, and chapter 3
owns its mechanism; here is its shape. A real PIIX3 IDE controller latches its
per-channel BMISTA Interrupt status bit the moment the drive raises INTRQ — during DMA
or not. mddosem raised IRQ14 with BMISTA reading zero (and the secondary channel’s
status register hardwired to zero), so a PCI IDE interrupt handler could look down at
the controller, see “no channel interrupt”, and dismiss its own IRQ as not-mine. The fix
latches the bit at assertion time — independent review proved that latching at the end
of the scheduling quantum was still too late, because the guest can execute another I/O
instruction in the same batch and observe stale status; the latch must be visible before
the I/O instruction returns. Landed 16 July at 01:09 (e0e9fe9a5), with 5,544
mddosem-hw tests green.
Fix 4 — the four-megabyte page
The last fix needs three pieces of background, each brief. First, paging: a 386-class
processor translates addresses through two levels of tables. CR3
points at a page directory; the top ten bits of a linear address select a page-directory
entry (PDE), which points at a page table; the next ten bits select a page-table entry,
which names a 4 KiB page. Second, PSE: later processors added page-size extension —
set the PS bit in a PDE (with CR4.PSE enabled) and there is no second level at all; that
one entry is a 4 MiB page. Windows 2000 maps its kernel — and with it
the TSS — through exactly one such page. Third, the I/O-permission bitmap: when the HAL
needs the VGA BIOS (a real-mode citizen) it runs it in V86 mode, and every port
instruction executed in V86 mode is checked against a bitmap held in the TSS — one bit
per port, fetched by the CPU itself mid-instruction. The interpreter performs that fetch
with a hand-rolled supervisor page walk (iopm_walk_supervisor), because it
cannot take the ordinary fault path halfway through a permission check.
That hand-rolled walker checked the PDE’s Present bit — and ignored PS. Confronted
with W2K’s kernel mapping, a PDE whose live value was
0xE3 (Present, writable, PS=1), it treated the 4 MiB
page’s base address as a page-table pointer, fetched a “PTE” from whatever bytes
happened to live inside the mapped frame, read it as not-present, and folded to “port
denied”. The port in question, 0x402, was one the guest’s
bitmap explicitly allows (io_map_base 0x88, bitmap byte 0x00,
TSS limit 0x20AB). The result was a spurious #GP(0) on the
HAL’s own V86 OUT — and the Standard PC HAL’s V86 monitor handles
INT-reflection only; anything else it treats as impossible and asserts. Bluescreen:
STOP 0x1E KMODE_EXCEPTION_NOT_HANDLED. One ignored bit, in one implicit
walk, ends the boot.
The fix is a single masked OR, mirroring what the real translator had done all along:
if CR4.PSE and the PDE’s PS bit are set, physical = (pde & 0xFFC0_0000) |
(linear & 0x003F_FFFF) — walk over. The bug had sat latent for years because
DOS extenders map everything with 4 KiB pages; Windows 2000’s large kernel page
was the first workload in the emulator’s life to send the IOPM walker through a PSE
mapping.
translate_paging_checked). Landed 13 July (Claude, HUM lineage) and
independently re-derived 19 July 03:05 (Codex, fe28f3aa0) — 22 minutes before
acceptance.Necessary, not sufficient
The fixes did not land into applause; they landed into walls. Fix 1 went in at 23:04 on 15 July; capture 08b, running a release binary carrying it, hit the identical black terminal state — setup flipping from resident to fully out-swapped between one watcher sample and the next. Fix 2 went in at 23:30; capture 09, on a binary that identified itself as 26915729b, walled the same way — while proving, via 2,789 protected-mode stack transitions in a 150-second “zero-runnable” window, that the machine was awake the whole time (chapter 4). Fix 3 went in at 01:09; capture 11 walled again. Three fixes, each provably real, each apparently insufficient — a drumbeat that would demoralise any lab.
Then chapter 5 happened, and the drumbeat was recast: captures 03 through 19 had been running with no CD in the drive. Those retests were never going to finish setup, however correct the CPU became — Windows was standing at an invisible dialog, politely asking for media the harness had removed. So the “failed retests” decided nothing about sufficiency. What stands is this: the fixes carry fail-first red witnesses or review-proven latch timing, refusing to pass on the old code; and the clean end-to-end install — media in, key in, all four corrections in the binary — exercised every one of them. The acceptance record credits exactly these four, none sufficient alone. There was no single ribbon-cutting bug at the bottom of Windows 2000; there was a machine that had to be right in four unrelated places at once.
Which is, in the end, the argument of the whole project: emulate the hardware, not the guest. Not one of the four fixes mentions Windows. An interrupt shadow honoured to the instruction, a timer that counts whether or not anyone is watching, a status bit latched the moment the wire rises, a page walk that believes the PS bit — every one is a statement about silicon, checkable against datasheets and rival emulators, useful to every future guest. Windows 2000 was simply the first tenant demanding enough to notice. What happened when the last of the four landed — the final night, minute by minute — is chapter 7.
Sources for this chapter — the four corrections as credited by the acceptance record (399d34b3b, 26915729b, e0e9fe9a5, fe28f3aa0) and their commit messages; the goal journal (wrk_journals, 2026.07.15) for the red witnesses and capture verdicts; bug ledger MDD-BUG-KILN-00004. Full evidence table: how we know.