BIOS jump tables read: cold-boot is BOOT (offset 0), not LIST

5 min read
z80cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

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

Working through 2.20’s $DD8E callee disassembly turned up a discrepancy that forced me back to fundamentals: I’d been calling $DD8E the “inline init helper called from cold-boot,” but disassembly at that address didn’t look like an init routine — it looked like SETDMA. Which it is. The BIOS jump table at $DACC puts SETDMA at offset 36, which is $DACC+36 = $DAF0: JP $DD8E.

So I went back and read both BIOS jump tables byte-by-byte against the standard CP/M 2.x layout (BOOT, WBOOT, CONST, CONIN, CONOUT, LIST, PUNCH, READER, HOME, SELDSK, SETTRK, SETSEC, SETDMA, READ, WRITE, LISTST inline, SECTRAN inline).

2.23 BIOS jump table at $FAB8:

OffsetAddressVectorRoutineWhat I called it before
0$FAB8JP $FED1BOOT(not previously identified)
3$FABBJP $FAB8WBOOT(not previously identified)
6$FABEJP $FB10CONSTpopulated routine
9$FAC1JP $FB1ACONINpopulated routine
12$FAC4JP $FB4DCONOUT”CONOUT” ✓
15$FAC7JP $FB70LIST”cold-boot generator”
18$FACAJP $FB7FPUNCH(no claim)
21$FACDJP $FB91READER(no claim)
24$FAD0JP $FE6CHOME(no claim)
27$FAD3JP $FE8ESELDSK(no claim)
30$FAD6JP $FE77SETTRK(no claim)
33$FAD9JP $FBF4SETSEC(no claim)
36$FADCJP $FBF9SETDMA(no claim)
39$FADFJP $FEBDREAD(no claim)
42$FAE2JP $FEC0WRITE(no claim)
45$FAE5: AF C9 00inline XOR A; RET; NOPLISTSTinline ✓
48$FAE8: 60 69 C9inline LD H,B; LD L,C; RETSECTRANinline ✓

2.20 BIOS jump table at $DACC:

OffsetAddressVectorRoutine
0$DACCJP $DEA8BOOT
3$DACFJP $DACCWBOOT
6$DAD2JP $DB08CONST
9$DAD5JP $DB12CONIN
12$DAD8JP $DB43CONOUT
15$DADBJP $DB66LIST
18$DADEJP $DB75PUNCH
21$DAE1JP $DB87READER
24$DAE4JP $DD4BHOME
27$DAE7JP $DD6DSELDSK
30$DAEAJP $DD56SETTRK
33$DAEDJP $DD89SETSEC
36$DAF0JP $DD8ESETDMA
39$DAF3JP $DD93READ
42$DAF6JP $DDA3WRITE

The standard offsets check out. So:

  • 2.23 cold-boot vector is $FED1, not $FB70. $FB70 is the LIST entry per jump-table convention.
  • 2.20 cold-boot vector is $DEA8, not $DB66. $DB66 is the LIST entry per jump-table convention.
  • 2.20 $DD8E is SETDMA, not an “inline init helper called from cold-boot.”
  • 2.23 $FB70 does still contain cold-boot-style code (LD SP,$0080 followed by Apple video state read, LD ($0005),A, LD HL,$9C06; LD ($0006),HL for BDOS vector, etc.). That’s a real observation. The interpretation needs re-thought: either (a) the LIST jump-table entry gets rewritten by the runtime generator to repoint somewhere harmless and the original $FB70 slot gets reused for cold-boot code, (b) $FB70 is a helper called from the runtime-generated BOOT routine at $FED1, or (c) BOOT at $FED1 gets generated to JP $FB70 early in cold-boot.

I haven’t disambiguated yet, but the structural observation about cold-boot code at $FB70 survives — only the label moves.

The bigger correction: I’d told the BIOS-layout story as “first 1 KB populated, second 1 KB all-zero on disk, runtime-generated.” That’s not right either. A page-by-page byte histogram tells the actual story:

2.23 pageRangeCode bytes / 256Filler bytes / 256
0$FAB8-$FBB719561 (mixed $00/$FF)
1$FBB8-$FCB70256 ($00/$FF/$F7 marker)
2$FCB8-$FDB724115 (mostly code)
3$FDB8-$FEB72254 (marker)
4$FEB8-$FFB716789 (mostly code)
5$FFB8-$100B70256 ($00/$FF)
6$100B8-$101B724511 (mostly code)
7$101B8-$102B70256 ($00/$FF)
2.20 pageRangeFiller
0$DACC-$DBCBmostly code, with small embedded fillers
1$DBCC-$DCCB256 bytes of $E5
2$DCCC-$DDCBmostly code
3$DDCC-$DECB256 bytes of $E5
4$DECC-$DFCBmostly code
5$DFCC-$E0CB256 bytes of $E5
6$E0CC-$E1CBmostly code
7$E1CC-$E2CB256 bytes of $E5

Both BIOSes use the same 256-byte interleave: alternating code pages and runtime-generated pages. Not “first half / second half.” That’s the BIOS factory’s actual on-disk shape.

The runtime-generated pages differ in fill byte:

  • 2.20 uses $E5 (CP/M’s “deleted file” filesystem marker, also the Z-80 opcode for PUSH HL).
  • 2.23 uses $00/$FF/$F7FF FF 00 00 / F7 F7 00 00 patterns, which decode as RST $38/RST $30 traps.

2.23’s choice is a clear safety upgrade: any premature execution lands in a defined trap rather than PUSH HL running to wherever. 2.20 uses the same byte CP/M would have used to mark unused directory entries — convenient on-disk, but a footgun if anything jumps there before generation runs.

Both BOOT vectors ($DEA8 for 2.20, $FED1 for 2.23) land in runtime-generated regions. So the “who populates the BOOT entry before the Z-80 first executes it” question applies to both versions, not just 2.23. This eliminates an earlier hypothesis that 2.20 was simpler because it had inline init — both versions need a generator.

Status: BIOS jump tables fully re-decoded for both versions; corrections noted on the affected devlogs. Next step is to disassemble the actual BOOT entry-point regions ($DEA8-$DECB in 2.20, $FED1-$FF0D in 2.23) and trace where execution ends up after the runtime-generated prologue runs — which will tell us whether the apparent overlap with $FB70/$DB? is by design or is a jump-table-rewrite by the generator.