RSS Amplifier

Wise Owl · Jun 16, 2026

The Whole Distribution, Rebuilt From Source

0
Sign in to vote or save

Brent Rector · Wise Owl

Everything Microsoft shipped on the original SoftCard CP/M floppies is now recovered as annotated assembly, and that assembly reassembles into the original disk images byte for byte. Every OS component on both processors, every bundled .COM program, across the whole distribution, with a one-command rebuild per release that exits nonzero if a single byte comes out wrong.

The five earlier parts built the tools behind that result. Part 1 gave us two round-tripping disassemblers, one for the 6502 and one for the Z-80, that refuse to emit a byte they can’t reassemble. Part 2 ran a .COM under emulation to learn code from data before disassembling it. Part 3 walked the CP/M filesystem and took an entire floppy apart. Part 4 fixed the rule that the recovered source matches the floppy as it shipped, not a running machine’s self-modified snapshot. Part 5 built one OS component from a single source at two different load addresses, and diffed versions.

Three releases

The distribution is three releases:

  • CP/M 2.23, 44K layout - the Videoterm-aware release. CCP and BDOS live in the Transient Program Area; the BIOS jump table sits at Z-80 $FA00.
  • CP/M 2.23, 60K layout - the same 2.23, with the CCP and BDOS lifted out of the TPA and into the Apple II Language Card so the TPA grows from about 39K to about 55K. You don’t get this disk from a fresh install. You get it by running CPM60.COM, the in-place updater that ships on the 44K disk, against a 44K boot disk.
  • CP/M 2.20 - the earlier, pre-Videoterm release. Its BIOS jump table sits at Z-80 $DA00, $2000 lower, and the whole BIOS lives up in Language Card RAM rather than the TPA.

The 60K system gets a separate release folder because its boot pipeline is different code, not a variant of 2.23. The 44K boot loader stages CCP and BDOS in the TPA and hands off. The 60K loader adds a Language Card bank relocator and four COPY_PAGES moves that lift CCP to $D300 and BDOS to $DC00 inside the card, then patches the in-card copies and plants the reset vector. Same operating system, different staging, so it has its own folder, boot doc, and byte-identical proof.

The two BIOS bases are $FA00 for 2.23 and $DA00 for 2.20. The SoftCard’s address translation is what puts them at different addresses: the Z-80 sees the BIOS at one address, the Apple sees it at another, through the four-window translation that the Emulator series lays out in Part 2. 2.23’s BIOS at Z-80 $FA00 lands in plain Apple RAM; 2.20’s at Z-80 $DA00 lands up in the Language Card. That $2000 gap is why diffing os/CPM_BIOS.asm across the two releases means rebasing one of them first.

What’s in each release folder

Each release is a self-contained folder under softcard/ in the apple-ii-orchard repo: the original disk image, an os/ tree, a utilities/ tree (for the two releases that have a filesystem of programs), a boot-and-patching doc, and a README with the one-command rebuild.

The os/ tree is the operating system as source. For 2.23 (44K):

FileCPULoadWhat it is
CPM_BootLoader.s6502$0800Stage-2 loader, install-copy logic, the LOAD_CPM staging read
CPM_RWTS.s6502$0A00Read/Write Track-Sector engine, the GCR 6-and-2 codec
CPM_InstallFragments.s6502$0200Fragments the loader copies into place
CPM_DiskCallbacks.asmZ-80imageThunks bridging BDOS/BIOS disk requests to the 6502 RWTS
CPM_SystemImage.asmZ-80$8000The staged CCP + BDOS image LOAD_CPM reads
CPM_BIOS.asmZ-80$FA00The as-shipped on-disk BIOS, jump table plus console/disk/IOBYTE primitives

The 6502 regions are ca65 .s plus a .cfg linker config; the Z-80 regions are sjasmplus .asm. The standalone CCP is the single shared source from Part 5 that builds both the 44K ($9300 TPA) and 60K ($D300, in-card) layouts byte-identical. 2.20’s os/ is the same shape minus the CPM_DiskCallbacks region it doesn’t have, with the BIOS at $DA00.

The utilities/ tree is one annotated .asm per .COM program, with bin/ holding the reassembled output. 2.23 carries 19 programs: the Digital Research tools (ASM, DDT, ED, PIP, STAT, LOAD, SUBMIT, XSUB) plus the Microsoft and SoftCard additions (MBASIC, GBASIC, COPY, CAT, APDOS, DOWNLOAD, DUMP, MFT, PATCH, AUTORUN, BOOT). 2.20 carries 11, including the 2.20-specific tooling: CPM56.COM (the 56K loader, 2.20’s analogue of CPM60), FORMAT.COM, and RW13.COM. CONFIGIO.BAS and DUMP.ASM are BASIC and assembler text on the disk, not machine code, so they ride along as filesystem data rather than getting decompiled.

The 60K folder has no utilities/ of its own. It doesn’t ship a filesystem of programs; it ships the updater that produces the 60K disk from the 44K one. The byte-identical buildable artifact for 60K is CPM60.COM itself, 11,264 bytes, a mixed-CPU .COM with a Z-80 installer driver wrapped around an embedded system image (the 6502 relocation code plus the relocated Z-80 CCP/BDOS/BIOS). It assembles from one master, CPM60.asm, which places each piece at its .COM offset, INCBINs the 6502 pieces, and assembles the relocating Z-80 modules as real code at their run addresses via DISP … ENT. Running CPM60.COM rewrites only the system tracks (0-2); the entire filesystem on tracks 3 and up stays byte-identical to the 44K disk it converted. The 60K release’s filesystem proof is therefore the 44K release’s filesystem proof.

The as-shipped rule, applied to a whole BIOS

Part 4 set the rule that the source matches the floppy as shipped, not a self-modified runtime snapshot. The BIOS is the hardest case for it. os/CPM_BIOS.asm for each release is the pristine on-disk BIOS, the bytes that arrive verbatim from track 2: 2.23’s runtime BIOS comes off track 2’s file-sectors 13 down to 8, one page each into Z-80 $FA00-$FF00; 2.20’s off sectors 1 through 5 ascending into $DA00-$DE00, with a sixth page at $DF00 assembled during the 6502 boot phase, so the 2.20 BIOS spans six pages from $DA00 to $DF00. Cold boot then modifies a sparse handful of bytes per page (on the order of 4/4/2/0/45/130 for 2.23, concentrated in the last two pages) and builds a $FE00-$FF47 device and console tail in RAM, the Videoterm and Pascal-firmware path. Each folder’s BOOT_AND_PATCHING.md describes that runtime tail. It isn’t in CPM_BIOS.asm, because it isn’t on the floppy. There are no generator pages and no wholesale-generated BIOS. An earlier pass had the BIOS half exist on disk and get manufactured at boot. That came from extracting the wrong track-2 sectors under a wrong load address. The real BIOS is whole on disk, and cold boot is a fixup pass over it.

The console tail is also where 2.23 and 2.20 differ, and the recovered source shows the difference on disk. 2.23 routes console I/O through a 6502 service island in plain RAM at Apple $0DD0-$0E35 that does the expansion-ROM ownership handshake on the 6502 side, after the CPU switch: deselect at $0E30 (read $CFFF), claim at $0E33 (read $C330), then dispatch through the $Cn0D-$Cn10 slot vectors. 2.20 does the same handshake on the Z-80 side, before flipping the bus, so the $C700 switch access tears the claim down before the firmware call lands in the shared $C800-$CFFF expansion-ROM window. The overview series reaches that conclusion in Two Right Parts, One Dead Machine, demonstrated for the A2FPGA Videoterm (the precise electrical trigger on a physical Videoterm board is still open, though the symptom is confirmed on real hardware). This series adds the two code paths themselves: both sit in os/CPM_BIOS.asm of their respective folders, and diffing them (after rebasing 2.20 from $DA00 to 2.23’s $FA00) puts the deselect-before-switch versus deselect-after-switch difference on adjacent lines. The detection half is a plain diff in the boot loader’s slot scanner: an 11-byte check that reads $Cn0B for the Pascal 1.1 signature $01 and files the card as device 6 instead of 2.20’s device 4.

The proof

From the repo root, with the toolchain on PATH, each release rebuilds and verifies in one command:

source shared/toolchain/env.sh        # ca65 + ld65 + sjasmplus
python -m cpm_pipeline.reconstruct softcard/CPMV223-44K/CPMV223-44K.DSK rebuilt.dsk
python -m cpm_pipeline.reconstruct softcard/CPMV220/CPMV220-Disk1.po    rebuilt.po

reconstruct assembles the OS region from os/, lays in every .COM from utilities/bin/ (and CPM60.COM from the 60K master), carries the filesystem’s data files, prints a per-byte provenance summary, and exits 0 only when the result is byte-identical to the original image. Over 80% of each disk comes from re-assembled source; the rest is filesystem data, the directory, and free space, carried verbatim. The 60K installer has its own check: build_cpm60_com() against the genuine CPM60.COM extracted from the disk, cross-checked by a second independent layout path in build_cpm60_com_via_layout(). All of this runs in CI, through test_cpm223_full_disk_reconstruct_byte_identical, test_cpm220_reconstruct_byte_identical, and test_master_build_is_byte_identical, so a regression in a disassembler or an annotation pass that perturbs a byte fails the build rather than slipping into a release.

The byte-identical requirement is the part you can’t fudge. A disassembly that looks right but won’t reassemble is worth nothing. A disassembly that reassembles to the original 143,360 bytes is the floppy, restated as source you can read and modify. Regenerating one release’s bin/ while building it caught a flaw in the Z-80 disassembler: it emitted a single oversized data directive for a long message block. That output assembled by eye but failed to reassemble, and the round-trip was the only thing that caught it. Chunking long data runs fixed it.

Where to go next

That’s the last part of this series. The two round-tripping disassemblers, emulation-seeded code/data discovery, whole-disk filesystem walking, the as-shipped rule, and single-source multi-address builds all fed into one result: the complete Microsoft SoftCard CP/M distribution, all three releases, recovered as annotated source that rebuilds the original disks to the byte.

The Emulator series builds the software model these disks run on, a 6502 and a Z-80 sharing one bus. It boots these same disks to an A> prompt and reproduces the 2.20-with-Videoterm hang, which is what let me check the recovered source against actual behavior. For the story without the byte-level detail, why any of this got built, the wrong answer that came first, and the real cause, start at The Card That Wouldn’t Boot CP/M. Claude built most of the tooling here. The decision to keep decompiling past the first confident answer was mine, and the byte-for-byte check is what kept either of us honest.

Read the original on wiseowl.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.