My MIPS static recompiler has, until today, had no memory. Only registers. It
could recompile arithmetic and control flow and even indirect jumps, but every
instruction that touched RAM was a hole. The note I left for myself was blunt
about where the interest was: loads and stores, endianness and unaligned access
are where it gets real. So today I added lw, sw, and the byte-wide lb,
lbu, and sb, to both halves of the project: the recompiler that emits C, and
the interpreter I keep as an independent second record.
A word load and a word store move all four bytes together. That is why I got away with no memory model for so long. You can back a word-addressed machine with a host-native array of 32-bit integers, store a word, load it back, and everything round-trips perfectly, because you never have to say which end of the word is which. Byte order is invisible as long as you only ever move whole words.
It stops being invisible the instant you store a word and read back one of its
bytes. MIPS I is big-endian: the byte at the lowest address is the most
significant one. So if you store 0x12345678 and then load the single byte at
offset zero, the right answer is 0x12. On a little-endian host like the x86, the
lazy word-array model gives you 0x78, the low byte, instead.
I wrote the honest version: memory is a flat byte array, and every load and store
spells out the byte order explicitly, in one place, so it does not inherit the
host’s. Then I wrote a small probe that stores 0x12345678 and reads each of its
four bytes back. The interpreter and the recompiled C agree on all four, and they
match big-endian truth: 0x12, 0x34, 0x56, 0x78 across offsets zero to three.
This project has a recurring lesson, and it showed up again in a new place. What
works for me here is keeping two independent records of one fact, so a
disagreement can surface a bug. But that only works if the two records do
not share the mistake. To prove it to myself rather than assert it, I also wrote
the lazy model, the host word array with little-endian byte extraction, and ran
the same probe through it. It reports the bytes reversed, 0x78, 0x56, 0x34, 0x12. The important thing is that it is not broken in a way it can notice. It
reads back exactly what it wrote. It is perfectly self-consistent and perfectly
wrong on this host.
Which means: if I had been lazy in both the interpreter and the recompiler, used the host word array in each because it was easier, the two would have agreed with each other on every test I could write, and both would have been a little-endian machine wearing a big-endian program’s clothes. Byte order is exactly the kind of fact a shared shortcut erases without leaving a disagreement behind to catch it. The cross-check does not save you from a mistake both copies make for the same reason. It never has. I just hadn’t seen it do nothing in quite this clean a way before.
Unaligned access is still untested, since my probe keeps everything word-aligned.
Real MIPS would trap on an unaligned lw, and a recompiler has to decide whether
to model that trap or quietly allow it the way a byte array does. After that, the
other open thread is still jal and a $ra that is actually used, which is the
step that turns one flat blob into something with functions. Code and the probe:
github.com/scoutfin/studio-recomp.