A source byte is a byte that shipped on the disk. Not a byte I read out of a running machine. The decompiled CP/M distribution is the bytes Microsoft pressed onto the floppy in 1980 and 1982, recovered and given names and labels and structure, and nothing else. Let a runtime value into the source and the source no longer matches the disk. It matches one run of one machine.
The first three parts of this series built the tools that produce those source bytes. Two disassemblers that argue about which bytes are code (Part 1). Running one program inside an emulator to watch the data fall out of the control flow (Part 2). Doing that across every file on the disk (Part 3). The disk-byte rule is what makes their output worth anything.
The rule is simple to state and was not simple in practice. Getting it wrong cost me a whole false model of the BIOS that I had to dismantle and throw away, and the line between a shipped byte and a runtime byte takes real forensic work to draw for any given byte.
The BIOS comes off the disk whole
Take the runtime BIOS for 2.23, the resident I/O layer the operating system calls for every character and every sector. When the Z-80 is running, it lives at Z-80 $FA00-$FFFF, which under the SoftCard’s real address translation is Apple $0A00-$0FFF (the -$F000 window; Part 2 of the emulator series lays out all four windows). The jump table sits at the very top of that range. Dump Apple $0A00 after boot and you get the 17-entry vector list:
$0A00: C3 D1 FE JP $FED1 ; BOOT
$0A03: C3 B8 FA JP $FAB8 ; WBOOT
$0A06: C3 10 FB JP $FB10 ; CONST
...
Where did those bytes come from? For a long time I believed the cold-boot routine built them. My snapshots said so. But I took those snapshots after the Z-80 had already run, and a snapshot taken then cannot separate the bytes the loader put down from the bytes the running system changed.
To find the real answer I took a page-provenance snapshot at the very first Z-80 instruction and then exact-match searched every page against the raw disk image. The entire 2.23 runtime BIOS comes verbatim off track 2. File-sectors 13 down to 8, descending, one 256-byte page each, landing at Z-80 $FA00 through $FF00. 2.20’s BIOS comes off track 2 sectors 1 through 5, ascending, into its own jump-table base at $DA00 (Apple $FA00, through the +$2000 window). Both BIOSes are on the disk, whole, as pages. The 6502 loader copies them into place. It does not patch them and it does not generate them; it copies them.
The BIOS is a disk artifact, and the disk artifact is the source.
Cold boot fixes up a few operands, it does not build the BIOS
The Z-80 does change a few of those disk bytes after they load, and that is what fooled me. After the BIOS is in place, the cold-boot routine runs and modifies a small number of them. Page by page, for 2.23, the cold boot changes 4, 4, 2, 0, 45, and 130 bytes across the six BIOS pages. For 2.20: 3, 3, 2, 3, 82, 86. The changes concentrate in the last two pages, where the per-device console tail gets fixed up, and they are sparse everywhere else. A page is 256 bytes wide; two or four rewritten bytes is a linker applying relocation fixups to a BIOS that is already there on the disk.
The fixed-up bytes are all vector operands. The $E700 bus-flip operand. Per-device dispatch operands. The CPU-handoff loop’s JSR target at $03D0/$03D1 that the Z-80 patches with the live 6502 service-routine address. None of these can be known until install time and run time, because they depend on which slot the SoftCard is in and which console firmware the machine has.
If you snapshot memory after cold boot and call that snapshot the source, you bake all of those runtime-specific fixups into the source as if they were shipped constants, which they are not. The shipped byte at the CPU-handoff loop operand is a placeholder. In 2.23’s install image it is literally STA $FFFF (operand $FFFF), and the old InstallFragments annotation reported that $FFFF correctly. I read the annotation and took the $FFFF for a real address instead of the placeholder it is. In 2.20 the placeholder is STA $C400, slot 4, which the stage-2 slot scanner patches to the SoftCard’s actual slot (STY $03C8 / STA $03C7 around $1086) once it identifies where the card lives.
So the rule forces a separation. The shipped placeholder goes in the source. The runtime fixup goes in a boot/load/patching document, described as a fixup, with its trigger and its computed value, deliberately not frozen into the assembly. The source assembles to the bytes on the disk. The document records what the cold-boot routine writes over those bytes after it loads them. Two artifacts, and you can diff each against ground truth independently.
Where the “185 patches” count came from
Before the disk-byte rule I worked from a count: 185 BIOS patches, a “BIOS that half-exists” until the cold-boot routine finished writing it. That number was an extraction artifact.
The 185 conflated two completely different things. About 38 of them were genuine Z-80 cold-boot self-modifications, the real fixup pass I just described. The other roughly 147 were post-boot dead-RAM reuse caught in a snapshot. After booting, the system reuses regions that had held setup code as ordinary scratch. Snapshot that and you see “changed” bytes all over the BIOS pages, and if your extraction window is also straddling the boundary between real BIOS sectors and the adjacent padding sectors on track 2, you see alternating “code” and “filler” pages that look like a half-built thing. None of it was the BIOS being constructed. It was a running system reusing its RAM, and the snapshot counted that reuse as a patch.
Two derived-BIOS code paths got built on top of that wrong model, routines that “reconstructed” the BIOS the way the snapshot implied it was assembled. Both were removed outright, because there is nothing to reconstruct: the BIOS is on the disk and you copy it.
The “2.23 upgraded its $E5 filler to RST traps for safety” claim fell to the same forensics, for the same reason. The $E5 bytes (2.20) and the FF/F7 trap-fill (2.23) are not inside the BIOS at all. They are in the unused track-2 sectors adjacent to the BIOS block: 2.20’s fill in file-sectors 7 through 15 right after its BIOS, 2.23’s in sectors 1 through 6 and 15 around its BIOS. Two releases padded their spare sectors of an otherwise-empty filesystem with different constants. There was no safety upgrade.
924 bytes that prove the line exists
“As-shipped” and “snapshotted” are different states of the same bytes, and one forensic result puts a number on the difference. I ran the real CPM60.COM installer inside the emulator and compared its output disk against a real Apple’s 60K disk (Part 3 of the emulator series has the full chase).
Tracks 0 and 1, the loader and the relocator, came out byte-for-byte identical. Track 2 differed in 924 bytes. And every single one of those 924 differences was the same shape: my disk had a zero where the real disk had data. One-directional, never the reverse.
Three experiments settled what those bytes were:
-
Trace each byte to its source. Instrument every write, capture which RAM buffer each of the 51 sector writes copies from, then walk all 924 differing positions. Unanimous result: all 924 were copied from the staging buffer at
$3D00-$43FF, from byte addressesCPM60never wrote. The installer assembles the new system image in that buffer, fills in the parts that matter, and writes the whole buffer to disk, gaps included. -
Prove they are a photograph of RAM. Fill the staging region with
0xAAat theA>prompt, run the installer again. All 924 of those exact disk positions came out0xAA. They track whatever is in the buffer, which means they are not values the installer computes. -
Prove they are don’t-care. Three disks now exist with three different things in the track-2 gaps: the real Apple’s leftover bytes, my zeros, and the wall of
0xAA. All three boot to60K Ver. 2.23. All three run.
The real Apple’s bytes there are mostly fragments of the 44K system that had just been running in that RAM, which is why so many of them match the original stock disk. They are residue of a previous program, no more part of the source than the contents of the heap before malloc zeroes it. Snapshot those 924 bytes into the source and the source only reassembles correctly on a machine that happened to have a 44K system warm in memory first. The as-shipped source writes the placeholder gap and leaves a note that the installer never initializes it.
The rule is what lets the single-source rebuilds in Part 5 diff two versions and trust the deltas, and what lets the full-distribution rebuild in Part 6 claim byte-identical reproduction and mean it. Across all three releases (2.23 at 44K, 2.23 at 60K, and 2.20 at 56K) every source byte is a disk byte, the emulator only traces structure and never supplies a resident value, and the runtime behavior, the cold-boot fixups and the device tail the Z-80 builds on top of the on-disk image, lives in its own document where it can be checked but cannot contaminate the source.
The emulator was indispensable for telling code from data and for watching the boot sequence resolve dispatch targets. It was the wrong place to read provenance from. By the time it has anything to show you, the Z-80 has already rewritten part of the loaded image. The disk holds the bytes from before any of that runs, so the disk is where the source comes from.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.