As mentioned in Act 4: I've got a bootable 32MB-capable system - except when I don't. I've found two published hard drive images that just won't boot at all: the Total Replay image, and Nox Archaist. Both absolutely worth the time to figure out why not.
The symptom is clear. Both images completely hang when you try to boot them, very unsatisfyingly like this -

Without a distinct crash, this is harder than just disassembling what's happening. This means we need to take a look at the boot block for Total Replay and walk the whole boot cycle through the ROM. Given that these images are 32MB, it's possible that they're trying to load something from the end of the disk and failing - given my geometry hack and the $FF00 magic block rewrite override. Maybe THAT'S my bug!
We can start with the debug output of the Teensy ProFile emulator, which dumps this more than a dozen times a second:
rd blk=$000000 block 0 rd blk=$00D848 "next directory block" = bytes +2/+3 of block 0 = 48 D8 (the loader's own TXA/PHA) rd blk=$000002 block $D848's bytes +2/+3 are 02 00, so back to block 2 rd blk=$000003 rd blk=$000004 rd blk=$000005 null next pointer -> read block 0 "as a directory" -> repeat
So that hypothesis is dead; that's not my bug. It's looping, not getting anywhere near the end of the disk, in a very sane boot pattern that just never completes. We're going to have to go deeper - down in the Total Replay boot block disassembly:
$0867 LDA #$1E $0869 STA $45 ; driver buffer hi = $1E00 $086B STA $3F ; scan pointer hi = $1E $086D JSR READBLOCK ; $08C3: ReadBlock via the card's ProDOS driver $0870 LDA #$04 $0872 STA $3E ; scan pointer = $1E04 (first dirent) $0874 LDY #$00 $0876 LDA ($3E),Y ; entry's storage/length byte <-- walks the pointer $0878 AND #$0F ; name length $087A TAX $087B INX $087C CMP $09A4,Y ; compare against "LAUNCHER.SYSTEM" $087F BEQ $0898 $0881 CLC $0882 LDA $3E ; next entry: pointer += $27 (39 bytes) $0884 ADC #$27 $0886 STA $3E $0888 BCC $088C $088A INC $3F $088C CMP #$FF ; end of this block's entries? $088E BNE $0874 $0890 LDX $1E02 ; next directory block, ABSOLUTE from the buffer $0893 LDY $1E03 ; (this is why the hops work but the walk doesn't) $0896 BCS $0867 ; read it, resetting only the high byte $0898 INY $0899 LDA ($3E),Y ; match the name, character by character $089B DEX $089C BNE $087C $089E STX $FF $08A0 LDY #$11 $08A2 LDA ($3E),Y ; key pointer (index block) lo $08A4 TAX $08A5 INY $08A6 LDA ($3E),Y ; key pointer hi $08A8 TAY $08A9 JSR READBLOCK ; read the file's index block READBLOCK: $08C3 STX $46 ; block number lo (caller passes the block in X/Y) $08C5 STY $47 ; block number hi $08C7 LDA #$01 $08C9 STA $42 ; command 1 = READ $08CB LDA $45 ; save the buffer high byte... $08CD PHA $08CE JSR $D1D1 ; PLACEHOLDER: rewritten at boot to JSR $CnEA $08D1 PLA $08D2 STA $45 ; ...and restore it: the card's read loop advances $45 by 2 $08D4 RTS
What I see there is a ton of zero-page magic variable addresses, self-modifying code, and cleverness.
The 6502 has distinct opcodes that make it faster, easier, and the code smaller when using the first $100 bytes of memory for variables ("zero-page addresses"). This memory space is, then, incredibly valuable: if you have a program running, which is using an operating system, which talks with drivers, all of them will need some set of it and - as with most hardware of this era - there's no real way to stake your claim that your special zero-page address is or is not already claimed. Handy charts like this existed in the 80s to help programmers figure out where to stash their variables, and what to avoid.
Lo Nibble of Address
Hi
Nib 0 1 2 3 4 5 6 7 8 9 A B C D E F
--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---
0 | A~ A~ A A A A - - - - A A A AI A A
1 | A A A A A A A A A A A A A A - M*
2 | M M M M M3 M MA3 MA3 M3 M3 M3 M3 MA3 MA3 M3 MA3
3 | MA M MA MA3 M M3 M3B M3B M3B M3B M~ M~ MA3~MA3~MA3~MA3~
4 | M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ M3~ I3~ I3~ I3~ I3~ M~ M
5 | MA MA MA MA MA MAI AI AI AI AI AI AI AI AI AI AI
6 | AI AI AI AI AI AI AI AI3 AI3 AI3 AI3 AI AI AI AI AI3
7 | AI3 AI AI AI3 AI3 AI AI3 AI AI AI AI AI AI AI AI AI
8 | AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI
9 | AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI
A | AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI3
B | AI3 AI AI AI AI AI AI AI AI AI AI AI AI AI AI AI
C | AI AI AI AI AI AI AI AI AI AI AI3 AI3 AI3 AI3 I I
D | AI AI AI AI AI AI AI3 I AI3 AI3 AI AI AI AI AI3 AI
E | A A A A A A A A A A A - - - - -
F | A A A A A A A A A A - - - - I AI
M = Used by Monitor; * used in early Apple IIe ROMs-- now free
A = Used by Applesoft BASIC
I = Used by Integer BASIC
3 = Used by DOS 3.3
~ = Used by ProDOS ($40-$4E is saved before and restored after use)
B = Used by ProDOS BASIC.SYSTEM (also uses all Applesoft locations)
- = Free; not used
--Bryan Dunphy, Michael J. Mahon, Rubywand
Yep, you read that correctly - taking in to account all of DOS, BASIC, ProDOS, and the Monitor there are only 14 bytes of zero-page RAM available that are conflict-free. Who knows what is or isn't used by individual device drivers, ROMs, or shim utilities. If your program has any substantial footprint, you're almost certainly going to have to accept that you conflict in some way - hopefully in a way that you don't care about. Like "I'm using ProDOS with AppleSoft BASIC, so the DOS 3.3 and Integer BASIC locations aren't important." Or, just as likely, "I'll save these variables when we start, and then restore them when we exit, so the operating system still works correctly."
All of those boot block zero-page accesses are important here - it could be that any one of them is in conflict with the ProFile ROM. Is THAT my bug? There's no reason why the author of the Total Replay or Nox Archaist boot blocks would ever know about this - these titles are eating 32MB of space, while the ProFile ROM only ever drove 10MB of disk. Without our geometry decoupling and block access hack there's no way this card would ever have been able to run such an image, just due to space requirements.
The author here was aware that this was the case: you can see it in READBLOCK, where $45 is being saved and restored across calls to the hardware. I suspect that's for a SmartPort device - probably something that cropped up during testing of the initial boot block.
And in Nox Archaist, we see the same general pattern, just looking for a different filename:
$082C LDA #$1E $082E STA $45 $0830 STA $3F $0832 JSR $0888 ; ReadBlock $0835 LDA #$04 $0837 STA $3E $0839 LDY #$00 $083B LDA ($3E),Y ... (identical walk: ADC #$27, INC $3F, CMP #$FF, LDX $1E02 / LDY $1E03) $085E LDA ($3E),Y $0867 LDA ($3E),Y $086B LDA ($3E),Y $086E JSR $0888
Let's see what we have going on in Apple's 1984 ROM for the ProFile card:
; the driver bracket and dispatcher $C81C PHP $C81D SEI $C81E JSR $C829 $C821 BCC $C826 $C823 PLP $C824 SEC $C825 RTS $C826 PLP $C827 CLC $C828 RTS $C829 STA $3F ; <-- $3F = $C5 $C82B LDA #$00 $C82D STA $3E ; <-- $3E = $00 ($3E/$3F now point at $C500) $C82F LDA $C082,X ...
Welp, that looks like a smoking gun to me. The ProFile is storing its slot information in 3E and 3F; the boot block is using the same addresses as a scanner pointer. Neither is saving/restoring those memory locations, and both of them are listed as used by various parts of the ecosystem, but at boot time, it's not clear that any of those are important. The boot block sets up its pointer, calls out to read a block, and the readblock driver in the ProFile ROM happily overwrites the pointer. Whose bug is that, exactly? Nobody's. Everyone's. But today it's mine to untangle.
Either (a) we ask for these to be patched upstream, so that Total Replay and Nox Archaist both change their code; or (b) we patch the ROM so that it saves/restores what it's doing in zero page itself, protecting whatever calls it.
(a) involves waiting, so let's look at (b) first. In theory all it would take is a couple instructions to save $3F/$3E at the head of the driver, and restore at the end. Unfortunately the ROM is, as most are, completely full and we'd need a couple dozen bytes to do what I'm proposing. But if we rewind back to the full ROM disassembly, it begins like this:
$C800 STR '(C) APPLE COMPUTER INC. 1984' $C81C DRIVER PHP $C81D SEI $C81E JSR DRV_DISPATCH ...
We don't need that string. The only thing it could be doing is helping to identify what peripheral is plugged in to this slot and, since the ROM does its own work alongside boot blocks in this area, it's really not important. Let's see how much I can cram in there:
$C800 LDA $3E ; ZPSAVE: save the caller's pointer $C802 PHA $C803 LDA $3F $C805 PHA $C806 JSR $C81C ; the ORIGINAL bracket, byte-identical $C809 STA $31 ; park A ($31 is stock-ROM scratch already) $C80B PLA $C80C STA $3F ; restore $C80E PLA $C80F STA $3E $C811 LDA $31 ; restore A; PLA/STA/LDA never touch carry $C813 RTS $C814 STR '3E3FSAVE' ; marker, in the space the copyright used to occupy $C81C PHP ; ...the stock ROM continues here, unchanged
Then we just need to patch where the driver is called:
stock zpsave
$CFF6 STA $07F8 $CFF6 STA $07F8
$CFF9 JMP $C81C $CFF9 JMP $C800 <-- the only other change
$CFFC HEX 00 00 47 EA $CFFC HEX 00 00 47 EA
Now when ProDOS tries to dispatch to our READ, it actually calls the ZPSAVE wrapper which then calls the stock read dispatcher. Nice, clean, and ... mostly worthless. I haven't even tested it; one of the failures of my card was in the rom slot itself, and I have no interest in making that worse. For the same reason I really wouldn't suggest that anyone else do so. Which means we have an academic, theoretical fix that's not terribly useful and we don't even know if it definitively fixes our boot error. Not terribly satisfying (although, I have to say that cramming those bytes over a string really felt good.)
No, what we need to do is patch one of the images and see if it boots while *not* using $3E / $3F. First, a scan of the boot block tells me that $EB / $EC seem free. These are in the range of "nothing known claims them," so they're probably safe for the bootloader itself. What if we find and replace all of the former instances with the latter?
| Total Replay Memory Location | old | new | instruction |
|---|---|---|---|
$086C | 3F | EC | STA $3F |
$0873 | 3E | EB | STA $3E |
$0877 | 3E | EB | LDA ($3E),Y |
$0883 | 3E | EB | LDA $3E |
$0887 | 3E | EB | STA $3E |
$088B | 3F | EC | INC $3F |
$089A | 3E | EB | LDA ($3E),Y |
$08A3 | 3E | EB | LDA ($3E),Y |
$08A7 | 3E | EB | LDA ($3E),Y |
(And, for completeness, the same 9 operands for Nox Archaist at $0831, $0838, $083C, $0848, $084C, $0850, $085F, $0868, $086C.)
What does that get us (he asked, expecting another crash further down the boot process)?
Gold. YES, THAT'S OUR BUG. Reported upstream, already patched in both; the next Total Replay past v5.2 should include it; and if you're a Nox owner that needs an updated image, you should join the 6502 Workshop Discord server, introduce yourself in #the-scuttlebutt-pub channel, and then ask for the update in the #nox-tech-help channel.
Jorj Bauer
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.