LOAD_CPM cracked: 29 sectors, $8000 staging, then Z-80 disk callbacks at $0A00

5 min read
apple-ii6502z80cpmsoftcardreverse-engineeringretrocomputingcpm-videx-series

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:

PositionTrackPhys SecApple Dest
0-40$0B-$0F$8000-$84FF
5-201$00-$0F$8500-$94FF
21-282$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_CPM call (possibly with $03E9 set to $FA instead 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.