Some BIOS routines disassemble cleanly; others are dispatch tables in disguise

5 min read
z80cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

Detail for Part 5 — The BIOS That Half-Exists.

With the Z-80 disassembler online, the natural next step was annotating each entry of the 2.23 BIOS jump table. The jump table itself disassembles correctly — 15 JP $xxxx instructions pointing into the BIOS range. But the bytes at the jump targets are inconsistent.

CONOUT at $FB4D is real Z-80 code:

$FB4D: 3D        DEC A
$FB4E: 20 0B     JR NZ,FB5B
$FB50: CD 83 FD  CALL FD83
$FB53: 21 00 C8  LD HL,C800     ; expansion ROM shared window — not Videx-
                                ; specific; any expansion-ROM card lives here
$FB56: CD 45 FB  CALL FB45
...

Same for the LIST routine at $FB70 and several others. These have plausible 35-byte bodies that decode into sensible-looking Z-80 sequences.

But CONST at $FB10 and CONIN at $FB1A decode as garbage — $FB10 starts with seven NOP bytes, $FB1A starts with a single RST $38 followed by eight NOP bytes. Neither of those is a credible BIOS routine prologue.

Looking at the raw bytes around those addresses reveals a structured data table:

$FB0A: 00 00 00 00 00 00 00 E4 FE 73 FA AC FF 64 FF 00   entry 1
$FB1A: 00 00 00 00 00 00 00 E4 FE 73 FA B8 FF 76 FF 00   entry 2
$FB2A: 00 00 00 00 00 00 00 E4 FE 73 FA C4 FF 88 FF 00   entry 3
$FB3A: 00 00 00 00 00 00 00 E4 FE 73 FA D0 FF 9A FF      entry 4

Four entries, 16 bytes each. Same shape: 7 zero bytes (probably per-device state buffer or padding), then 8 bytes that decode as four 16-bit values. Two of those addresses are constant across entries ($FEE4 and $FA73 — probably common dispatch targets); two vary in regular $0C-byte and $12-byte strides ($FFAC$FFB8$FFC4$FFD0, and $FF64$FF76$FF88$FF9A).

This is the per-device-code dispatch table — what the slot scanner builds at $03B9-$03BF consumes. Each entry holds (input-routine-pointer, output-routine-pointer) for one device class, and the spacing of the targets says each input routine is 12 bytes and each output routine is 18 bytes. Four entries fits the device-code range (1-4 in 2.20; 1-4 plus the new code 6 in 2.23 — so the 2.23 table needs to be larger than 4 entries somewhere, unless code 6 is dispatched separately).

The implication: the BIOS jump table’s CONST and CONIN entries don’t point at real CONST/CONIN routines at the file offset where I extracted the BIOS. The actual routines are elsewhere in the loaded image — probably copied in from the disk system tracks during the CP/M loader’s LOAD_CPM step (the loader stages 6 KB into LC RAM before the SoftCard switch).

What this BIOS extract IS reliable for: the routines that are contiguous at this offset — CONOUT, LIST, and probably HOME/SELDSK/SETTRK which appear to live in the upper part of the extract. Those are enough to confirm the BIOS targets the expansion-ROM area (the LD HL,$C800 in CONOUT — though that’s the Apple ][ shared expansion-ROM window, not Videx-specific by itself). What it ISN’T reliable for: CONST/CONIN/BOOT/WBOOT and the per-device dispatch routines themselves.

To get the complete BIOS, two paths:

  1. Reverse-engineer the loader’s LOAD_CPM to figure out exactly which sectors get read into which Z-80 addresses, and reconstruct the BIOS from those sources.
  2. Boot the system in a Z-80 emulator and dump memory after CP/M reaches the prompt — gives us the in-memory view directly.

Path 2 needs a Z-80 emulator (which nibbler doesn’t have yet — disassembler only). Path 1 is mostly a 6502 disassembly task; the LOAD_CPM routine is at Apple $0C00 and is partly understood.

Status: 2.23 BIOS jump table verified; CONOUT confirmed as real code with the $C800 Videx VRAM evidence. CONST/CONIN/BOOT extraction blocked by the loader’s non-contiguous BIOS layout. Two paths forward identified; Path 1 (reverse-engineer LOAD_CPM) is the next concrete step that doesn’t require new tooling.


Updates (2026-04-27):

Three corrections from later findings:

  1. The dispatch-table base address shown in the bytes block above is off by one entry’s worth of padding. The table actually starts at $FAEB (not $FB0A), with 4 entries at $FAEB, $FAFB, $FB0B, $FB1B — each 16 bytes (8-byte zero padding then 8-byte data). What was labeled “entry 1” at $FB0A was actually the last byte of entry 2 plus entry 3. CONST target $FB10 is in entry 3’s padding area (where the cold-boot generator writes runtime code), not in entry 1’s data.

  2. The “Videx VRAM” framing was over-stated. $C800-$CFFF is the shared expansion-ROM window, not Videx-specific. See the shared-address-spaces correction.

  3. The “BIOS extraction is blocked” framing was partly resolved. About half the BIOS turned out to be runtime-generated (not on disk at all) and half to be cleanly extractable. The LOAD_CPM mechanism (Path 1) was completed and the cold-boot generator was located. See LOAD_CPM cracked, BIOS runtime-generated, and cold-boot generator found.