RSSAmplifier

Wise Owl · Jun 16, 2026

Why a Replica, Not a Reading

0
Sign in to vote or save

Brent Rector · Wise Owl

When this bug first came to me I had two disk images and a symptom, and nothing else. Microsoft SoftCard CP/M 2.20 hangs at boot with a Videx Videoterm, and 2.23 boots fine with the same card. The obvious way in is to disassemble both disks and read the code until the difference between the two explains the hang. That’s how I started. See the ‘Disassembler’ series for background on how I created it. But disassembly alone wasn’t sufficient, for two reasons. First, static analysis can’t always tell code from data, so the disassembly never comes out complete: stretches of the disk stay as blobs of bytes you can’t classify by reading them. You’re always left wondering, is the bug in the blob I can’t yet read? Second, sometimes the failure doesn’t isn’t obvious from the static code. It lives in the timing of how the code executes. Disassembly is the wrong instrument for that. You need to watch the run run. I couldn’t do that with the actual hardware so I built the entire machine in software.

I built a complete Apple ][+ sytem with 6502 processor, included a Microsoft SoftCard with its Z-80 processor, added the problematic Videx Videoterm, plus a Disk II controller. A system faithful enough to the active hardware it could boot an unmodified CP/M disk to its A> prompt. I’ll call that emulated machine the replica. Reading couldn’t find the bug, and a replica I couldn’t trust would be no better than reading, so I set a hard bar for it before I believed anything it showed me. Part 5 is where the replica clears that bar.

The overview series tells the same story without the byte-level detail, starting at The Card That Wouldn’t Boot CP/M. These articles go down to the bytes it skips.

What the disassembler can see

Static analysis on this system was strong, and that strength is what fooled me. It found the difference between the versions. Version 2.20 checks the Videoterm for Pascal 1.0 I/O support, finds it, and files the card as device code 4. Version 2.23 checks the same card for both Pascal 1.0 and 1.1 support, finds both, and picks 1.1, filing it as device code 6. That difference is real, and it survives every later correction. Immediately, I thought, Aha! I bet that’s the issue. Some how version 2.20 using the Videoterm as a Pascal 1.0 device is the problem. Using it was a Pascal 1.1 device clearly works. Great hyphothesis. Sadly, it took many days to discover it was totally wrong.

I got as far as this instruction, which is part of the code performing an inter-CPU call. The line doesn’t execute as it was compiled. Other runtime code patches the destination address of the JSR instruction to various destinations. That was a key insight.

$03CF: 20 58 FF  JSR $FF58      ; <-- operand at $03D0/$03D1 is rewritten at runtime

The full annotated CPU-handoff loop, with the bus flip and the register restore around this call, is in Part 2. Disassemble this line and JSR $FF58 reads as a call to a monitor routine, but that target is wrong. The Z-80 overwrites the two operand bytes at $03D0/$03D1 before every call, with the address of whichever 6502 service routine this round needs. The disassembler shows you FF 58 because that’s what is on disk. The real target only exists after the Z-80 has written it, microseconds before the JSR executes. So the single most important branch in the cooperative protocol is, on disk, a placeholder pointing at the wrong place. Understanding self-modifying code generally requires watching the changes as the system runs. The harder problem is the address translation underneath it.

The map that is not on the disk

The SoftCard runs a Z-80 that has no memory of its own. Every fetch it makes is satisfied by the Apple’s RAM, through a fixed hardware translation that lives on the card, in no disk byte and no memory dump. The full four-window map is the subject of Part 2; the two windows that drive the CPU-handoff loop are these:

Z-80 addressApple addressNote
$E000-$EFFF$C000-$CFFFthe I/O page and slot space
$F000-$FFFF$0000-$0FFF-$F000, Apple zero page, stack, text, screen holes

Take those rows with the CPU-handoff loop in mind. When the Z-80 flips the bus to the 6502, it executes LD ($E700),A. Through the $E000-$EFFF window that’s a write to Apple $C700, the same STA $C700 the 6502 side does at $03C6. One physical access, two addresses, depending on which CPU you are standing on. When the Z-80 patches that JSR operand, it writes to Z-80 $F3D0, which through the -$F000 window is Apple $03D0. The two processors write to one set of cells through two different coordinate systems.

This table sits underneath every address-level conclusion you draw from the Z-80 disassembly. Get the table wrong and the code still disassembles cleanly into plausible instructions, it just describes a machine that doesn’t exist. The first pass at this investigation did exactly that. It used a wrong bit-12-XOR map under which the device-4 console path appeared to dispatch into a region of $E5 filler bytes and run them as instructions, a wrong answer told in the overview series (in The Answer That Agreed With Itself) and traced at the byte level in Part 5. A disassembly is only as true as the address map you read it through, and the address map is not in the bytes. It comes from hardware. The most reliable way to get it from hardware is to build the hardware and see whether the system boots.

The fact no dump contains

Say the map is right and you’ve resolved every self-modified operand by hand. One fact about this bug is still missing, and no static artifact holds it, because it is not a value in memory. It is which CPU owns a window at one instant.

The $C800-$CFFF expansion-ROM window is shared by every card in the machine. Only one card may drive it at a time, and ownership is a flip-flop on the cards, claimed and released by bus accesses. The Videx Videoterm claims the window when its $C3xx page is touched and releases it on $CFFF. On the A2FPGA re-creation it also releases on any access to a different slot’s $Cnxx page, which means the SoftCard’s own bus flip qualifies: STA $C700 is an access to slot 7’s page.

The device-4 path on 2.20 walks straight into that. Its Z-80-side RPC setup does the textbook ownership sequence before the call: LD A,($EFFF) to deselect all expansion ROMs (Apple $CFFF), then a read through $E300 to select the Videx (Apple $C300). Correct, by the book. Then the dispatcher flips the bus at $E700. That flip is an other-slot access, so it releases the claim the Z-80 just made. The 6502 wakes and calls the Pascal 1.0 fixed firmware entries that live inside the window: $C800 (INIT), $C84D (READ/CONIN), $C9AA (WRITE/CONOUT). It calls them into a window that no card owns. On real hardware that’s a floating-bus fetch, and the system is dead before the banner.

The bug is which card owns the window at the instant of the fetch at $C800. Read the 2.20 bytes and the 2.23 bytes side by side and both do a $CFFF-deselect and a $C3xx-select. Both look correct. 2.20 does it on the Z-80 side, before the flip that tears it down. 2.23 does it on the 6502 side, after the flip, at $0E30 ($CFFF deselect) and $0E33 ($C330 claim), so the claim survives to the fetch. That timing relationship between two correct routines and a flip-flop appears in no disk byte, no symbol table, and no memory dump. It exists only while the machine runs, as the order in which three accesses land.

(One note carried from the overview series: the kill is demonstrated for the A2FPGA’s arbitration rule, where the $C700 flip releases the window. The physical Videoterm’s PAL suggests $CFFF-only release, yet a real PAL board still hangs on 2.20. The symptom is confirmed on hardware; the exact electrical trigger on the physical card is still open. The protocol violation, 2.20 entering $C8xx space with no live claim, holds either way.)

The bar I held the replica to

If the bug is an ordering of runtime accesses, an emulator is the only instrument that can hold it. A careless emulator is worse than nothing. It runs my assumptions and hands them back to me as “behavior.” So the bar I set was not “can it reproduce the hang.” Almost any wrong execution ends in a crash, and two different bugs can share a dead screen, so a crash proves nothing. The bar I set instead was that the replica has to boot an unmodified disk from the Disk II PROM’s first sector read, run a real 6502 and a real Z-80 against one shared Apple memory, execute the real Videx firmware ROM for every character of output, and reach an interactive A> where DIR lists the actual directory of the actual image. A wrong assumption, a wrong map byte, or a wrong ownership rule would break one of those steps before I ever asked about the bug. Clear all of them and the replica is worth asking.

Part 2 builds the spine of the replica: one shared memory, two CPU cores, and the bus switch that is the $C700 access at the center of everything above.

Read the original on wiseowl.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.