Some BIOS routines disassemble cleanly; others are dispatch tables in disguise
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:
- 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.
- 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:
-
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$FB0Awas actually the last byte of entry 2 plus entry 3. CONST target$FB10is in entry 3’s padding area (where the cold-boot generator writes runtime code), not in entry 1’s data. -
The “Videx VRAM” framing was over-stated.
$C800-$CFFFis the shared expansion-ROM window, not Videx-specific. See the shared-address-spaces correction. -
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.