Tracing CONOUT into the BIOS hits the extraction wall
Detail for Part 5 — The BIOS That Half-Exists.
Continuing from the Z-80 disassembler devlog: traced CALL $FB45 from CONOUT (the helper that runs after LD HL,$C800) to see if it does Videx-pattern hardware writes.
$FB45 itself is 8 bytes, then falls through into CONOUT’s body:
$FB45: 07 RLCA ; rotate A left
$FB46: CD 81 FE CALL FE81 ; ?? — referenced
$FB49: 36 03 LD (HL),03 ; write 03 at (HL)
$FB4B: 36 15 LD (HL),15 ; write 15 at (HL)
$FB4D: ; falls through to CONOUT entry
It looks like a small init routine — but tracing CALL $FE81 immediately hits a wall. $FE81 in our BIOS extract is FF 00 00 FF FF 00 00 ... — that’s RST $38 followed by NOPs followed by more RST $38s, which is data, not a routine. The pattern continues through $FEA8 where it changes to F7 F7 00 00 and then to all zeros at $FEB8+. Some kind of structured table, but the routines it would call into aren’t here.
Same for the dispatch-table common targets: $FEE4 (which both common entries in the per-device dispatch table reference) is all zeros in our extract. So the dispatch table points at addresses that, at extraction time, contain no code.
Real Z-80 code does appear at $FF0E:
$FF0E: LD B,A
$FF0F: LD HL,FECD ; load HL from a BIOS-internal data area
$FF12: LD A,(HL)
$FF13: LD E,A
$FF14: OR A
$FF15: JR NZ,FF29
$FF17: LD A,(F397) ; check Apple ][ TPA address $F397 for state
$FF1A: OR A
$FF1B: JR Z,FF23
...
This routine reads from $F397 — that’s in the Apple ][ TPA area (which the Z-80 sees at the same address since $2000+ doesn’t get XORed). Some kind of system-state check.
And $FA73 (the OTHER constant address from the dispatch table entries) is before our BIOS extract starts ($FAB8 is offset 0). So that part isn’t here either.
The boundary of what’s recoverable is now clear:
- In our extract: the jump table, CONOUT, LIST, the
$FB45init helper, the$FF0Edispatch routine, and probably a few other routines around$FE6C-$FE93(HOME/SETTRK/SETSEC area) and$FF0E-$FFnn. - NOT in our extract:
$FA00-$FAB7(the BIOS cold-boot area below the jump table),$FE81callee,$FEE4callee, all four per-device input handlers at$FF64-$FF9F, all four per-device output handlers at$FFAC-$FFDF, BOOT at$FED1, and chunks elsewhere.
Two paths to fill the gaps:
- Reverse-engineer LOAD_CPM at Apple
$0C00— figure out exactly which sectors get read into which Z-80 addresses during the boot loader’s CP/M-image staging. Tedious 6502 disassembly + careful mapping. No new tooling needed. - Z-80 emulator + memory dump — boot CP/M, let it reach the
A>prompt, dump the entire$2000-$FFFFrange. Gives the in-memory view directly. Butnibblerdoesn’t have a Z-80 emulator yet (only a Z-80 disassembler), so this needs new tooling.
Path 1 is mostly disassembly work that the existing toolchain handles. Path 2 produces cleaner results faster (no inference about what gets loaded where) but requires building a Z-80 emulator first, which is a significant project on its own.
Status: trace from CONOUT into per-device routines is blocked at the BIOS extraction boundary. About half of 2.23’s BIOS is reachable in the partial extract; the other half (specifically the per-device input/output handlers and BOOT) requires either Path 1 or Path 2. Both are real next steps; choice depends on whether the goal is “understand 2.23 specifically right now” (Path 2) or “build infrastructure that also unlocks 2.20 BIOS extraction and the diff” (Path 1, since LOAD_CPM is the same routine in both versions).
Update (2026-04-27): Path 1 was chosen and completed. The “missing” BIOS routines (BOOT, HOME, SELDSK, the per-device handlers, the cold-boot area) turned out to be runtime-generated, not loaded from disk — see BIOS runtime-generated. The cold-boot generator that produces them was located at $FB70 (in the populated half) — see the cold-boot finding. So the “extraction wall” was partly a categorization mistake — the bytes that “weren’t extractable” were never on disk to extract; they’re constructed at runtime by code in the populated half.