The cold-boot generator hides at the LIST jump-table entry
Detail for Part 6 — The BIOS Factory.
Update (2026-04-28): The cold-boot vector is actually
$FED1, not$FB70. The CP/M BIOS jump table at$FAB8puts BOOT at offset 0 (JP $FED1) and LIST at offset 15 (JP $FB70). I conflated the two. The structural observation here — that real cold-boot init code lives at$FB70(LD SP,$0080, BDOS vector planting, etc.) — still holds; what’s wrong is the claim that this code is the BOOT-vector target. It’s reached indirectly, probably from runtime-generated code at the actual BOOT vector. See the BIOS jump-table correction.
Working through the populated 1 KB of the 2.23 BIOS to find the cold-boot generator (per the BIOS-runtime-generated finding). The natural target was anything that looked like initialization code with stack setup — which turned up immediately at the address listed as LIST in the jump table.
$FB70, supposedly the LIST entry per the jump table at $FAC7: JP $FB70:
$FB70: 31 80 00 LD SP,0080 ; init Z-80 stack
$FB73: 3A 51 E0 LD A,(E051) ; read Apple ][ video state
$FB76: 21 00 0E LD HL,0E00 ; HL = $0E00
$FB79: CD 45 FB CALL FB45 ; helper
$FB7C: CD 82 FA CALL FA82 ; CALL into the runtime-generated area!
$FB7F: 3A 08 9C LD A,(9C08) ; (this is also the PUNCH jump-table target)
$FB82: FE 9C CP 9C
$FB84: 28 11 JR Z,FB97 ; first-boot detect / branch
$FB86: 21 59 FF LD HL,FF59
$FB89: 22 D0 F3 LD (F3D0),HL
$FB8C: 2A DE F3 LD HL,(F3DE)
$FB8F: 3E 77 LD A,77
$FB91: 32 0B 00 LD (000B),A ; (this is also the READER target)
$FB94: C3 0B 00 JP 000B
$FB97: AF XOR A
$FB98: 32 07 93 LD (9307),A
$FB9B: AF XOR A
$FB9C: 32 DD FE LD (FEDD),A ; clear BIOS state byte
$FB9F: 32 D8 FE LD (FED8),A ; clear BIOS state byte
$FBA2: 3E C3 LD A,C3 ; A = $C3 (Z-80 JP opcode)
$FBA4: 32 00 00 LD (0000),A ; plant byte at Z-80 $0000 = Apple $1000
$FBA7: 21 03 FA LD HL,FA03 ; new Z-80 reset target = $FA03
$FBAA: 22 01 00 LD (0001),HL ; plant address at Z-80 $0001-$0002
; → Z-80 reset vector now JP $FA03
; (was JP $FA00 from the 6502 loader)
$FBAD: 32 05 00 LD (0005),A ; plant $C3 at Z-80 $0005
$FBB0: 21 06 9C LD HL,9C06 ; HL = $9C06 (CCP/BDOS entry)
$FBB3: 22 06 00 LD (0006),HL ; plant address at $0006-$0007
; → CP/M BDOS call vector at Z-80 $0005
; = JP $9C06 (the standard CP/M
; "CALL $0005" interface for user
; programs to invoke BDOS)
$FBB6: 01 80 FF LD BC,FF80
$FBB9: ; ... continues into runtime-generated area
Several reframings fall out of this:
The Z-80 reset vector gets rewritten. The 6502 loader plants JP $FA00 at Apple $1000-$1002 (= Z-80 $0000-$0002) before the SoftCard switch. The cold-boot routine at $FB70 overwrites that to JP $FA03 after init completes. So JP $FA00 is the first instruction the Z-80 ever executes; JP $FA03 is what subsequent warm-boots see. The $FA00 and $FA03 addresses are 3 bytes apart — exactly one Z-80 instruction. So the cold-boot path enters at $FA00, runs one instruction (the first cold-boot-init action), and then later warm-boots skip that first instruction and enter at $FA03.
The CP/M BDOS call vector is planted standardly. Z-80 $0005-$0007 = C3 06 9C = JP $9C06. That’s the standard CP/M convention: user programs CALL $0005 to invoke BDOS. So $9C06 is BDOS’s actual entry point in this build’s runtime memory. Working backward from $9C06: BDOS is in the staged sysimg area (the CCP+BDOS chunk at Apple $A300-$B9FF). $9C06 < $A300, which means… wait, that doesn’t fit. Unless the sysimg gets relocated from Apple $A300 to a different final address before the cold-boot runs.
That’s actually consistent with CP/M’s standard model: the staged image at $A300 would get moved to a final location calculated from the memory size. For this 60K build, BDOS final position would be ~$9C06. So before cold-boot runs, something must move the sysimg from $A300 to $9406 (CCP at $9406, BDOS at $9C06 — typical for 60K CP/M). The current investigation hasn’t located that move yet.
The LIST jump table entry isn’t really LIST. The bytes at $FB70 are unmistakably cold-boot init (stack setup, vector planting). The PUNCH entry at $FB7F and READER entry at $FB91 are also INSIDE this cold-boot routine — they’re alternate-entry midpoints into the same code. So the BIOS jump table is being abused: LIST/PUNCH/READER targets are positioned at points within the cold-boot code, not at separate LIST/PUNCH/READER routines. Either the SoftCard CP/M doesn’t really support a LIST/PUNCH/READER device, or those entries are reachable via a different path that does the right thing.
The CALL $FA82 at $FB7C goes into the runtime-generated area. $FA82 is in the $FA00-$FAB7 region that’s all-data on disk. Either (a) the cold-boot routine’s earlier steps populated $FA82 before this CALL, or (b) the CALL is expected to land in pre-populated code that something else generated. Most likely the former — the cold-boot routine writes some bytes into $FA00-$FAB7 before reaching the CALL. The exact sequence would need to be traced through the bytes BEFORE $FB70.
What this answers and what it opens
This identifies the cold-boot routine — a major prerequisite for understanding 2.23’s Pascal-1.1 driver code generation. The routine at $FB70 is the entry point. It:
- Sets up the Z-80 stack and BDOS call vector (standard CP/M init)
- Calls into runtime-generated code at
$FA82 - Branches based on whether
$9C08 == $9C(a sentinel check — possibly “first boot vs warm reboot”) - Clears specific bytes in the BIOS second-half state area (
$FED8,$FEDD)
What’s still open: the actual code-generator part of cold-boot. The clear/plant operations at $FBA2-$FBB6 aren’t producing per-device handler code yet — they’re setting up architectural fixed points. The Pascal-1.1 driver-code generation must happen elsewhere, possibly in the routine at $FA82 (which the cold-boot calls into) or in code that runs after the JP $9C06-equivalent (entering BDOS, which calls back into BIOS for first console output, triggering more init).
Diffing 2.20’s equivalent routine should pinpoint where 2.23 differs. 2.20’s BIOS jump table has LIST at $DB66 (= $DACC + $9A); should contain analogous bytes.
Status: cold-boot routine entry located at $FB70. The routine plants the standard CP/M reset vector and BDOS call vector, calls into runtime-generated code, and branches on a sentinel. The actual per-device code-generation is the next piece — likely involves the $FA82 call target or downstream BIOS routines triggered by first console I/O.