Engineering Notes
FrameMask: Stored, Never Applied on the Origin 200
The last wall before IRIX booted was a processor register the model stored and never applied.
IRIX boots on this machine now. The last wall between it and running was a control register our processor model had implemented in every respect except the one that mattered. It had storage, a read path, and a write path. A guest could set it and read back exactly what it set. What it didn’t have was any effect, and the kernel programmed it, believed it, and built its address space on the assumption that it worked.
Early in its own bring-up the kernel maps a small per-processor data page and writes to it. On this machine that write took a bus error, and the boot dropped into the firmware’s diagnostic mode. What made it interesting was that the faulting address decoded cleanly: physical 0xc00065c000. On this architecture the top byte of a physical address is the node number, so that names node 0xc0, node 192. The machine has one node, numbered zero. Nothing answers at node 192, so the store bus-errored, correctly.
The obvious reading is that the kernel computed a wrong address. It hadn’t. Read live under a debugger, the page it was mapping is physically on node zero, at 0x65c000. The kernel’s arithmetic was right. The 0xc0 was our model’s contribution, added somewhere between the page-table entry the kernel wrote and the address the processor issued.
Those two extra bits turned out to be flags the kernel parks inside its page-table entry on purpose. One marks a supervisor page and the other marks it dirty, and they sit in the part of the entry that also holds the physical frame number, so on the face of it the kernel is writing its own bookkeeping into the top of every address it maps. It can do that safely because the R10000 has a register whose whole job is to stop those bits reaching the address. FrameMask names bits of a page-table entry that must be forced to zero when the entry is written into the translation buffer, before the frame number is taken out of it. It resets to mask nothing, and the operating system programs it during setup. IRIX sets it to clear exactly those two bits. So on real silicon the kernel’s flags live in the entry, stay visible to the kernel, and are stripped on the way into the hardware. They never become part of an address.
Our model stored the register and never applied it. The flags passed straight through the translation write and into the frame number, where two bits of software bookkeeping became the top two bits of a physical address and named a node that doesn’t exist. That’s why the failure is so specific. It isn’t a corrupt address or an off-by-something. It’s the kernel’s own flags, arriving verbatim in the field they were supposed to be masked out of, and being read as a node number.
Most of the time I spent on it went into eliminating good candidates. The wrong address held a plausible-looking byte, and there are several places on this machine where a byte like that could legitimately come from: the firmware’s configuration structures, the global data area, the boot descriptors. I checked every one, and every one holds node zero. A kernel global that looked promising turned out to be an AND mask whose high bits are discarded before use, which is the kind of coincidence that costs an afternoon.
One instrument was lying by omission the whole time. The breakpoints that finally caught it had been set several times before and had never fired. The debugger silently truncates a breakpoint address to its low 32 bits unless address masking is explicitly turned off, so every earlier breakpoint had been planted at an address the kernel never executes, and reported nothing, which reads exactly like the code never being reached. Turning that off is a one-line setting, and it’s written down now in the tool that drives these sessions.
The fix applies FrameMask on a translation write, clearing the named bits before it takes the frame number, and keeps the register programmable so a machine that masks nothing still behaves as it did. The test that guards it is a property, not a boot: with the mask set the way IRIX sets it, an entry carrying both flags has to produce an address with both of those bits clear. That fails on the unfixed model, passes on the fixed one, runs in the register suite, and never boots an operating system. The behaviour comes from the public R10000 manual, read by someone who doesn’t write the processor model, which is how anything here that touches vendor documentation stays apart from the code that implements it.