2.20 hang settled in the emulator: Case B (Z-80 stack overflow on $E5 spam)

5 min read
z80cpmsoftcardvidexapple-iiemulationreverse-engineeringretrocomputingcpm-videx-series

The v2.20 hang byte-trace devlog settled the dispatch path through $DFBE → CALL $DAC5 and identified two static-evidence-consistent failure modes:

  • Case A: Cold-boot generator populates $DAC5+ with code that calls $Cn07 on the Videx; 6502 enters Videx ROM without the V-flag preamble; hangs in BASOUT waiting for an uninitialized status bit; Z-80 polls forever.
  • Case B: Generator leaves $DAC5+ and $DFBE+ as $E5 filler; Z-80 executes PUSH HL repeatedly; stack overflows; system loses coherence.

Without runtime, both stayed plausible. The emulator settles it.

Setup

After the slot-scanner detects the Videx as device 4 (Pascal 1.0; 2.20 doesn’t check $Cn0B), I loaded bios_220.bin (2 KB, the disk-staged BIOS image) at Apple $DA00 (the address 2.20’s planted Z-80 reset vector points to with JP $DA00).

The bytes at the supposed dispatch handler $DFBE:

$DFBE: E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5

Pure $E5 fill — the runtime-generator zone marker for 2.20. Same for $DAFF onward (the dispatch table) and most of the supposed BIOS-handler region.

$DAFF: B2 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5
$DB0F: E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5 E5
... (fill continues)

So statically, on disk, $DFBE is not a real handler. The cold-boot generator is supposed to populate it at runtime with device-specific code. For a Videx (device 4 in 2.20), that population doesn’t happen — because 2.20’s generator doesn’t have a Pascal-1.1 path, and the device-4 generator path either targets a different region or generates incorrect code for this device.

Running it

I jumped the Z-80 directly to $DFBE (simulating what CONOUT dispatch via the device-4 path would do), with SP=$0080 (per the BIOS init convention) and HL=$1234 as a marker. Tracing the SP register over 500 instructions:

First 10 SPs:  $0080, $007E, $007C, $007A, $0078, $0076, $0074, $0072, $0070, $006E
Last 10 SPs:   $FEE4, $FEE2, $FEE0, $FEDE, $FEDC, $FEDA, $FED8, $FED6, $FED4, $FED2

Two PUSH HLs underflow the available stack ($0080-$0000 = 64 PUSHes). After that SP wraps to $FFFE and PUSH HL keeps going. The BIOS region ($FA00-$FFFF) gets overwritten:

$FFE0: 34 12 34 12 34 12 34 12 34 12 34 12 34 12 34 12
$FFF0: 34 12 34 12 34 12 34 12 34 12 34 12 34 12 34 12

Pure $1234 fill — the marker HL value, written 2 bytes per PUSH, 16 PUSHes per row.

What this means

Case B confirmed. The 2.20 hang isn’t the 6502 stuck inside the Videx ROM (Case A); it’s the Z-80 stack-overflowing through $E5-filled handler space. After ~32K iterations the entire 64K Z-80 address space gets corrupted with whatever HL contained on entry. The 6502, still alive on its own and running its warm-boot loop at $03C0, eventually has its state overwritten too once SP wraps low enough.

This happens because:

  1. The 2.20 slot scanner (without the Pascal-1.1 check) tags the Videx as device 4 (Pascal 1.0 generic).
  2. 2.20’s BIOS dispatch table at $DAFF maps device 4 to the handler at $DFBE.
  3. $DFBE is in the runtime-generator zone — $E5 fill on disk.
  4. The cold-boot generator that would populate $DFBE with real handler code doesn’t have a working device-4-with-Videx path. (For device 3 or no card, the generator either fills it correctly or leaves it deliberately $E5-filled because the BIOS isn’t expected to dispatch via that handler.)
  5. CP/M boots, displays the cold-boot banner, then calls CONOUT for the first character of the A> prompt. CONOUT goes through $DFBE. PUSH HL spam.

2.23 fixes this by detecting the Videx as device 6 (Pascal 1.1) and routing through $FDB0 — which the Stage-3 cold-boot generator devlog showed is dispatched correctly. ($FDB0 itself is a one-byte RET stub on disk; the real Pascal-1.1 driver gets installed via a separate runtime mechanism.)

What this closes

The investigation’s central question — why does CP/M 2.20 hang with a Videx in slot 3 — is now end-to-end settled in the emulator:

Path2.202.23
Slot scanner detects Videx✓ device 4✓ device 6
Cold-boot generator routes✗ device 4 → $DFBE✓ device 6 → $FDB0
Handler bytes at target$E5 fillOne-byte RET stub (placeholder for runtime install)
Behavior on first CONOUTPUSH HL spam, stack overflowRETs cleanly
Outcomehangsworks

The Case A vs Case B distinction is now unambiguously Case B in the emulator.

Status

The 2.20 hang failure mode is settled: Case B, Z-80 stack overflow on $E5 PUSH-HL spam at $DFBE. Reproduced byte-for-byte in the Stage-3 emulator. Closes the byte-trace devlog’s open Case A vs B question.