BIOS jump tables read: cold-boot is BOOT (offset 0), not LIST
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:
| Offset | Address | Vector | Routine | What I called it before |
|---|---|---|---|---|
| 0 | $FAB8 | JP $FED1 | BOOT | (not previously identified) |
| 3 | $FABB | JP $FAB8 | WBOOT | (not previously identified) |
| 6 | $FABE | JP $FB10 | CONST | populated routine |
| 9 | $FAC1 | JP $FB1A | CONIN | populated routine |
| 12 | $FAC4 | JP $FB4D | CONOUT | ”CONOUT” ✓ |
| 15 | $FAC7 | JP $FB70 | LIST | ”cold-boot generator” ✗ |
| 18 | $FACA | JP $FB7F | PUNCH | (no claim) |
| 21 | $FACD | JP $FB91 | READER | (no claim) |
| 24 | $FAD0 | JP $FE6C | HOME | (no claim) |
| 27 | $FAD3 | JP $FE8E | SELDSK | (no claim) |
| 30 | $FAD6 | JP $FE77 | SETTRK | (no claim) |
| 33 | $FAD9 | JP $FBF4 | SETSEC | (no claim) |
| 36 | $FADC | JP $FBF9 | SETDMA | (no claim) |
| 39 | $FADF | JP $FEBD | READ | (no claim) |
| 42 | $FAE2 | JP $FEC0 | WRITE | (no claim) |
| 45 | $FAE5: AF C9 00 | inline XOR A; RET; NOP | LISTST | inline ✓ |
| 48 | $FAE8: 60 69 C9 | inline LD H,B; LD L,C; RET | SECTRAN | inline ✓ |
2.20 BIOS jump table at $DACC:
| Offset | Address | Vector | Routine |
|---|---|---|---|
| 0 | $DACC | JP $DEA8 | BOOT |
| 3 | $DACF | JP $DACC | WBOOT |
| 6 | $DAD2 | JP $DB08 | CONST |
| 9 | $DAD5 | JP $DB12 | CONIN |
| 12 | $DAD8 | JP $DB43 | CONOUT |
| 15 | $DADB | JP $DB66 | LIST |
| 18 | $DADE | JP $DB75 | PUNCH |
| 21 | $DAE1 | JP $DB87 | READER |
| 24 | $DAE4 | JP $DD4B | HOME |
| 27 | $DAE7 | JP $DD6D | SELDSK |
| 30 | $DAEA | JP $DD56 | SETTRK |
| 33 | $DAED | JP $DD89 | SETSEC |
| 36 | $DAF0 | JP $DD8E | SETDMA |
| 39 | $DAF3 | JP $DD93 | READ |
| 42 | $DAF6 | JP $DDA3 | WRITE |
The standard offsets check out. So:
- 2.23 cold-boot vector is
$FED1, not$FB70.$FB70is the LIST entry per jump-table convention. - 2.20 cold-boot vector is
$DEA8, not$DB66.$DB66is the LIST entry per jump-table convention. - 2.20
$DD8Eis SETDMA, not an “inline init helper called from cold-boot.” - 2.23
$FB70does still contain cold-boot-style code (LD SP,$0080followed by Apple video state read,LD ($0005),A,LD HL,$9C06; LD ($0006),HLfor 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$FB70slot gets reused for cold-boot code, (b)$FB70is a helper called from the runtime-generated BOOT routine at$FED1, or (c) BOOT at$FED1gets generated toJP $FB70early 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 page | Range | Code bytes / 256 | Filler bytes / 256 |
|---|---|---|---|
| 0 | $FAB8-$FBB7 | 195 | 61 (mixed $00/$FF) |
| 1 | $FBB8-$FCB7 | 0 | 256 ($00/$FF/$F7 marker) |
| 2 | $FCB8-$FDB7 | 241 | 15 (mostly code) |
| 3 | $FDB8-$FEB7 | 2 | 254 (marker) |
| 4 | $FEB8-$FFB7 | 167 | 89 (mostly code) |
| 5 | $FFB8-$100B7 | 0 | 256 ($00/$FF) |
| 6 | $100B8-$101B7 | 245 | 11 (mostly code) |
| 7 | $101B8-$102B7 | 0 | 256 ($00/$FF) |
| 2.20 page | Range | Filler |
|---|---|---|
| 0 | $DACC-$DBCB | mostly code, with small embedded fillers |
| 1 | $DBCC-$DCCB | 256 bytes of $E5 |
| 2 | $DCCC-$DDCB | mostly code |
| 3 | $DDCC-$DECB | 256 bytes of $E5 |
| 4 | $DECC-$DFCB | mostly code |
| 5 | $DFCC-$E0CB | 256 bytes of $E5 |
| 6 | $E0CC-$E1CB | mostly code |
| 7 | $E1CC-$E2CB | 256 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 forPUSH HL). - 2.23 uses
$00/$FF/$F7—FF FF 00 00 / F7 F7 00 00patterns, which decode asRST $38/RST $30traps.
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.