Reconstructing MBASIC 5.21, byte for byte

Microsoft's source for MBASIC 5.21 is gone. A reconstruction from a sibling version and a disassembly is only half the problem. The harder half is proving the reconstruction is right, and that proof needed a byte-identical assembler to be built first.

MBASIC 5.21 is the version of Microsoft BASIC that most CP/M machines actually shipped with. Its source code no longer exists. A set of sources labeled "MBASIC 5.2" survives and is clearly from the same family, but just as clearly is not 5.21 — assemble it and you get a different program.

AI-built. I directed this work; I did not write the code. How that works.

That gap is a recurring request on retro computing forums. People want to read how the interpreter works, or to add a driver for some piece of hardware, or to fix something. A disassembly answers the first question badly and the other two not at all: you cannot maintain 24 kilobytes of unlabeled disassembly.

So the goal is a set of sources that assemble to the 5.21 binary. Not to an equivalent binary — to the binary.

Why "equivalent" is not good enough

The obvious approach is to disassemble mbasic.com, tidy the output into something readable, and stop when the result behaves the same. This produces something useful-looking and almost impossible to trust.

The problem is that you have no way to distinguish a reconstruction error from a toolchain difference. Suppose your rebuild differs from the original by eleven bytes. Did you mis-transcribe an operand? Or does your assembler encode LD A,(HL) differently, or order its symbol table differently, or pad a segment where Microsoft's did not? With a "behaves the same" pass condition you cannot tell, and every subsequent change makes it worse, because you have no baseline that is known-clean.

Byte-identity removes the ambiguity entirely. If the output matches, every uncertainty in the chain — every operand, every macro expansion, every link decision — resolved the same way the original did. There is nothing left to argue about. It is a brutal pass condition, and that is exactly its value.

But it only works if you have an assembler and linker that reproduce the originals byte for byte. Which meant building those first.

There is a second reason byte-identity was the only workable standard here. I did not write this code — I directed AI to write it, and that includes the assembler it is built with. Nobody is going to read 18,584 lines of reconstructed Z80 assembly and certify it by eye, least of all me. The comparison against the original binary is the review, and it is a better one than a human reading would be: it checks every byte, it never gets tired at line 12,000, and it cannot talk itself into believing that a difference is probably harmless.

The toolchain comes first

MBASIC was built with Microsoft's MACRO-80 assembler and LINK-80 linker. To reproduce its output I needed Linux equivalents that agree with them not approximately but exactly: same .rel object format, same relocation handling, same segment layout, same link-order semantics.

That is um80_and_friendsum80 (assembler), ul80 (linker), ulib80 (librarian) and ud80 (disassembler), also AI-built. It exists because the reconstruction needed it. A tool built to make a verification possible is usually a sign the verification is worth something: if "close enough" had been acceptable, none of it would have been necessary.

ud80 then did the first pass on the problem, disassembling the real mbasic.com so its output could be compared against what the 5.2 sources produce.

Converging

From there it is a loop. Assemble the 5.2 sources. Compare against the disassembly of 5.21. Find a divergence. Work out what source-level change accounts for it. Apply the minimal change — minimal matters, because a reconstruction that rewrites freely is a rewrite, not a reconstruction. Repeat.

The distance turned out to be smaller than the version numbers suggest. Across 18,584 lines of source in fourteen modules, the reconstruction changes 1,286 lines. That is about seven percent, and it is not evenly spread:

ModuleChanged linesWhat it does
bintrp.mac598Interpreter core
f4.mac215Floating point
bimisc.mac65Miscellaneous
bio.mac60I/O
init.mac58Initialization
fiveo.mac58File I/O
dcpm.mac54CP/M interface
binlin.mac2Line input

Nearly half the work is in the interpreter core and the floating-point package. The line editor and line input are almost untouched — two changed lines in binlin.mac. The differences between 5.2 and 5.21 are concentrated exactly where you would expect a point release to concentrate them, which is itself a small piece of evidence that the reconstruction is tracking something real rather than being fitted to the target.

The proof

The build is fourteen assembles and one link:

for f in bintrp f4 biptrg biedit biprtu bio bimisc bistrs \
         binlin fiveo dskcom dcpm fivdsk init; do
    python3 -m um80.um80 mbasic_src/$f.mac -o out/$f.rel
done
python3 -m um80.ul80 -o out/mbasic_go.com -s out/*.rel

Which gives:

Linked -> out/mbasic_go.com
  Modules: 14
  Global symbols: 578

And the check that matters:

$ md5sum out/mbasic_go.com com/mbasic.com
5fc6b24ecb203287d96e9e642ee4fc3b  out/mbasic_go.com
5fc6b24ecb203287d96e9e642ee4fc3b  com/mbasic.com

24,320 bytes, 578 global symbols, identical. Not equivalent — identical.

What this is for

The output is a set of .mac files you can read, edit and rebuild. Change one instruction and you get a 24,320-byte binary that differs from Microsoft's in exactly the way you intended, and you can prove it, because the unmodified build is byte-identical. That is the property that makes the sources actually usable: every future change is measured against a known-clean baseline.

The same discipline shows up in the rest of my work for the same reason. A Z80 C compiler is judged against a published benchmark suite, not against my impression of its output. A Smalltalk-80 virtual machine is judged against the Xerox reference trace, not against whether the desktop looks right. Picking a pass condition someone else can check is most of the engineering; the rest is arithmetic.

The sources are at github.com/avwohl/mbasic2025, and the toolchain that builds them is at github.com/avwohl/um80_and_friends.