LOAD_CPM cracked: 29 sectors, $8000 staging, then Z-80 disk callbacks at $0A00
Detail for Part 5 — The BIOS That Half-Exists.
Took the user-recommended Path A from the BIOS-extraction decision point: reverse-engineer LOAD_CPM at Apple $0C00 to figure out what the 6502 actually loads from disk. Path picked for story coherence — the next chapter naturally extends Part 2’s “the loader stages 6 KB into LC RAM” rather than detouring into emulator construction.
The actual entry point isn’t $0C00 — it’s $0BEB. The PREP_HANDOFF code at Apple $1119 copies the disk routines from $0A00-$0FFF up to $BA00-$BFFF first, then calls JSR $BBEB — which is $0BEB in the original page mapped to $BBEB after the copy. So LOAD_CPM is at $0BEB, called via the relocated copy at $BBEB.
What it does:
$0BEB: STA $03E9 ; $03E9 = $80 (destination high byte)
$0BEE: LDY #$00 / STY $03E8 / STY $03E0 ; track = 0, dest_lo = 0
$0BF6: INY / STY $03E4 / STY $03EB ; some flags = 1
$0BFD: LDA #$60 / STA $03E6 ; slot index = $60 (slot 6)
$0C02: LDA #$0B / STA $03E1 ; starting sector = $0B
$0C07: LDA #$1D / PHA ; sector counter = $1D (29)
$0C0A: PHP / SEI ; save flags, disable IRQ
$0C0C: JSR $BE11 ; call sector-read routine
$0C0F: BCC $0C19 ; success → continue
$0C11: JSR $FF2D / PLP / PLA / JMP $BBE9 ; error → retry
$0C19: PLP
$0C1A: INC $03E9 ; advance dest page
$0C1D: LDX $03E1 / INX ; sector++
$0C21: CPX #$10 ; sector wrap?
$0C23: BNE $0C2A ; no
$0C25: LDX #$00 / INC $03E0 ; yes → wrap sector, ++track
$0C2A: STX $03E1
$0C2D: PLA / SEC / SBC #$01 ; counter--
$0C31: BNE $0C09 ; loop while != 0
$0C33: LDA #$08 / STA $03E9 ; reset $03E9 = $08
$0C38: RTS
29 sectors read sequentially starting at track 0 sector $0B. Each goes to $03E9 << 8, then $03E9 increments. So sectors land at $8000, $8100, ..., $9CFF. The sector path:
| Position | Track | Phys Sec | Apple Dest |
|---|---|---|---|
| 0-4 | 0 | $0B-$0F | $8000-$84FF |
| 5-20 | 1 | $00-$0F | $8500-$94FF |
| 21-28 | 2 | $00-$07 | $9500-$9CFF |
After LOAD_CPM returns, PREP_HANDOFF continues with two more page copies (which I’d already mapped but not understood the source of):
$9700-$9CFF(last 6 pages) → Apple$0A00-$0FFF$8000-$96FF(first 23 pages) → Apple$A300-$B9FF
So the staged content gets split into two destinations. Wrote a reconstruction script that reads those exact sectors from CPMV233.DSK, applies the splits in software, and dumps the results.
The system image at $A300-$B9FF
5888 bytes. Real Z-80 code. Starts with:
$A300: JP $9631 ; jump to CCP entry (post-relocation address)
$A303: LD A,(DE) / OR A / RET Z / CP $20 / JR C,$A2DF / RET Z / CP $3D / RET Z ...
; classic CCP command-line parser: check for space, '=', '_', '.', ':', ';', '<', '>'
Tail (the last 64 bytes) decodes as ASCII text:
Softcard CP/M
60K Ver. 2.23
(c) 1980,1982 Microsoft
That’s the boot banner — the line of text that appears the instant CP/M reaches A>. So the 5.9 KB at $A300-$B9FF is the CCP + BDOS + banner string combined. The CCP starts at the top with the command parser; BDOS sits below; banner string at the very end.
The Z-80 disk callbacks at $0A00
The other half of the staged content ($9700-$9CFF, 1.5 KB) becomes the bytes at Apple $0A00-$0FFF. These bytes are Z-80 code, not 6502. After the SoftCard XOR, Apple $0A00 is Z-80 $1A00. The 6502 stages Z-80 code at low Apple memory for the Z-80 to find post-handoff.
Disassembled as Z-80, the first 80 bytes are dense JP/CALL/LD HL,() operations referencing the staged CCP/BDOS area at $A3xx-$B9xx and the Apple TPA at $9Fxx. This is the Z-80-side “disk callback” interface that the BIOS calls when it needs to do disk I/O. The cooperative-CPU pattern: Z-80 hits a callback, it toggles back to 6502, 6502 reads a sector using the original disk routines (which were preserved at $BA00-$BFFF by the first PREP_HANDOFF copy), then hands back to Z-80.
Searched the Z-80 callback code for any IN/OUT instructions or memory accesses to $C0xx (where the SoftCard CPU-switch soft switch would live). None found. The CPU-switch trigger isn’t a direct I/O write in this code either. So the SoftCard switch must happen via either: (a) a memory access pattern the SoftCard hardware monitors, (b) bytes inside an instruction encoding the disassembler doesn’t yet decode (CB/DD/ED/FD prefix tables), or (c) somewhere I haven’t extracted yet.
What’s still missing: the BIOS itself
The BIOS lives at Z-80 $FAB8 (or $DACC in 2.20). That’s high memory in language-card RAM. Nothing in this 29-sector load lands there. So the BIOS comes from another disk load — either:
- A second
LOAD_CPMcall (possibly with$03E9set to$FAinstead of$80), orchestrated by the Z-80 code post-handoff, OR - It’s part of the next 23-page block staging (the third PREP_HANDOFF copy at Apple
$1133:$8000-$96FF→$A300-$B9FF) — but that’s the one I already identified as CCP+BDOS, so the BIOS would need to be inside the same sector range, which it isn’t (5888 bytes total of CCP+BDOS+banner doesn’t leave 1300 bytes of room for BIOS).
Most likely: the warm-boot loop at Apple $03C0 (which calls $0E36 — now redirected to the new Z-80 disk-callback code post-PREP_HANDOFF) eventually triggers a second sector load that brings the BIOS in. Or the BIOS is staged in a piece of memory I haven’t extracted yet — possibly through a different path.
Status: LOAD_CPM fully reverse-engineered. CCP+BDOS extracted as real Z-80 code with banner string verifying it’s correct. Z-80 disk callbacks at $1A00 identified. BIOS staging path is the next gap; the warm-boot loop’s $0E36 callee is the trace target. SoftCard CPU-switch trigger still not located but bounded.