My great aunt used to say that she had a great Forgettery. I think I inherited it. My Remembery is faulty.
I can remember really arcane and worthless trivia, like "CALL -151" enters the machine monitor on an Apple ][ and that the HERO-1 robot had a built-in speech synthesizer with the phrase "I don't do windows". Useful everyday stuff, like where my wife said the various knives belong in the knife drawer? Not a chance.
This works out pretty well for this project, though! I still remember enough of how to get around the Apple //. The assembly language looks familiar. I can poke at executables in memory. I can boot various kinds of DOS and transfer files between them. With ProDOS and Virtual ][, I can copy files back out to my Mac and use all of the tools I've got there, too. But I need a bit of a refresher - I haven't looked at the internals of the Apple //e since 1989. I don't remember what $C010 is, but I do remember that $C000 is the beginning of ROM. But it's a fast study: I quick remember zero-page memory, Applesoft BASIC tricks with magic values therein, and find references for various ROM entry points. The TMI disk is a DOS 3.3 disk; I use disk Copy to copy it to a ProDOS disk, boot to ProDOS, and then use Virtual ][ to copy it out to my Mac. Lovely.
Do I know where in memory this binary even loads, so that I can poke at it in the virtualizer? Nope. But a little poking around memory, and I find it at $1000.
Do I have a disassembler for 6502? Well, no. But several exist. I download a few and find them all lacking.
The 6502, being an 8-bit processor, has at most 256 opcodes. It can't be that hard to write a disassembler. When faced with the same problem for the PIC, I wrote pic-disassemble, a (wait for it) disassembler for (ahem) the PIC. (Bet you didn't see that coming.)
So an hour later, and I've got a perl script that disassembles. Just like all the other disassemblers that I found and didn't like. But from here, I can augment it to do anything I want!
I want it to be able to label memory points. I want it to be able to mark regions of memory as data, so they're not disassembled. But mostly I want it to be able to draw call flow graphs. Like perhaps this one!
(The full-size copy is here, on my web server, if you really want to look at it.)
From the call flow graph - which initially just had nameless labels, no descriptive ones like "MAINLOOP" - I was able to guess where the highlights are. MAINLOOP was my first target. Then I named ROM entry points in the assembly listing, followed by a lot of reset-to-monitor; call subroutine manually; see what happens.
Eventually I got tired of pure exploration and went in for the kill.
What does this program *do*?
0x1000 ORG 0x1000 0x1000 JSR ASKINIT 0x1003 L1003 JSR F8ROM:INIT 0x1006 JSR F8ROM:HOME 0x1009 JSR PRNTMENU 0x100C MAINLOOP LDA #$FF 0x100E STA CURSCR 0x1011 JSR PRNTTIME 0x1014 JSR RUNTILL 0x1017 CMP #$84 0x1019 BNE L1022 0x101B LDA #$2 0x101D STA $5B28 0x1020 BNE MAINLOOPWell, there's the beginning of it. Simple enough. The ASKINIT function asks if you want to initialize the reactor core, and then returns. The F8ROM functions are part of the machine's rom - in this case, clearing the screen and returning the cursor to the top-left. The PRNTMENU function prints out the list of screens in the game. And so on. Boring.
I spent some time looking at code bits that read from the hardware KBD register, and that cleared the KBDSTROBE register (telling the machine that you're ready for the next keypress). Which lead me to this piece of the program:
0x543F GETKEY LDA KBD 0x5442 BMI L5446 0x5444 SEC 0x5445 RTS 0x5446 L5446 BIT KBDSTRB 0x5449 STA $18 0x544B CLC 0x544C RTSRead from the keyboard; if the high bit isn't set, then there's no input, and we'll set the Carry flag and return. If the high bit *is* set, then we'll clear it, store the result in zero-page location 0x18, clear the carry, and return. Clearly this is a utility function to get a keypress and return (via the Carry flag) whether or not something was read.
What calls GETKEY? I want to find the thing that responds when you press '7'. Should be easy enough.
Except that it isn't. The code is sprawling; it's clearly compiled (not hand-coded) because there are pieces of code that look like they're doing parts of the same thing, in very different parts of memory. I'd only found a couple places that call GETKEY; from there, I suspect everything is picking up the key code from $18? Hmm. Well, I find code snippits like this that are looking for specific keys:
0x12F4 L12F4 JSR GETKEY ; wait until we get a keypress; S/R go to 1303, others return 0x12F7 BCS L12F4 0x12F9 CMP #$D3 ; 'S' 0x12FB BEQ L1303 0x12FD CMP #$D2 ; 'R' 0x12FF BEQ L1303 0x1301 L1301 CLC 0x1302 RTS 0x1303 L1303 STA $0 ; keypress was 'S' or 'R' from L12F4... only no part of the game I know of uses S or R. But there are pieces like this:
; This is reached normally for every tick 0x18A5 L18A5 LDA KBD ; read from keyboard 0x18A8 BMI L18EF ; this may be "if key not pressed"? 0x18AA LDY $5B0D 0x18AD CPY #$7 0x18AF BCC L18B3 ; branch if screen < 7 0x18B1 LDY #$7 0x18B3 L18B3 LDA L5B04 ; wait out the rest of the cycle? 0x18B6 BEQ L18BB 0x18B8 JSR $FCA8 ; F8ROM:WAIT 0x18BB L18BB LDA $5B0D 0x18BE CMP #$0 ; on screen #0? 0x18C0 BEQ L18C6 ; yes: go to L18C6 0x18C2 CMP #$2 ; on screen #2? 0x18C4 BNE L18EA ; no: go to 18ea 0x18C6 L18C6 LDA #$12 ; this is screen 2 work... 0x18C8 JSR L5511 0x18CB TAX 0x18CC INX... which are identifiably "if you hit key 0-7, then we do something". I dug through a lot of code like this, which is repeated various places, that is so tangled up that I'd need a lot of tea to decipher its intent.
I spent a lot of time changing pieces of the program to BRK statements (0x00) so that I could tell whether or not that bit of code was being executed, and when. None of them were executed when I pressed '7'. Here's another one:
0x1039 L1039 CMP #$B0 ; 0? 0x103B BCC L1003 ; branch if A <'0' 0x103D CMP #$B8 ; 8? 0x103F BCS L1003 ; branch if A >= '8' 0x1041 PHA ; start of handling keys 0-7 0x1042 SEC 0x1043 SBC #$B0 0x1045 STA $5B0D ; 5b0d gets the current screen we're on 0x1048 PLA 0x1049 JSR L564F 0x104C BCS L10B5 0x104E BPL L1001 ; WTF? can't be right. 0x1050 DEC $10 0x1052 ??? 0x1053 ROL $11,X 0x1055 ??? 0x1056 SEI 0x1057 ORA ($B4),Y 0x1059 DEC $11,X 0x105B LDA CV,X 0x105D ??? 0x105E LDX L124F 0x1060 ??? 0x1061 ??? 0x1062 ??? 0x1063 ???This is just after I've identified that $5B0D is what I call CURSCR. When the screen changes, a number from 0 to 7 (presumably) is put in $5B0D. When the game starts, it puts $FF in there (presumably meaning "on the menu"). But hours of poring through code paths that mention $5B0D, and I'm no closer. So I zeroed in on something odd in this particular listing.
It's those question marks.
Not all 256 values are opcodes. Those ??? values are things that don't disassemble properly. And that instruction at 0x104E -- "Branch on Plus to L1001" makes no sense! Back in the first listing, you'll see that there's an instruction at 0x1000 and then one at 0x1003, because the first instruction is three bytes long. How can we be jumping back to 0x1001? Is this really a program that's sophisticated enough to be able to take advantage of gadgets embedded at offset code locations?
I doubt it. More likely, I'm looking at something that's not code.
The Apple ][, by virtue of that "if the high bit is set, then there's a key that's been pressed" feature, winds up setting the high bit on ASCII characters. So you can't see things like text from a straight hex dump. But if you XOR the whole thing by 0x80, then you might see something like...
Now we're talking. I'll just update the disassembler to dump those as strings rather than trying to disassemble them, and then...
0x1271 L1271 CLC 0x1272 RTS 0x1273 JSR F8ROM:INIT 0x1276 JSR F8ROM:HOME 0x1279 JSR $A7AD 0x127C LDA #$0 0x127E STA $A9A1 0x1281 JSR L5479 0x12B9 STR '\cM SAVE / RESET STATE\cM\cM\cM\cMCATALOG (Y OR N) _\0' 0x12B9 BIT KBDSTRB 0x12BC L12BC JSR GETKEY 0x12BF BCS L12BC 0x12C1 CMP #$D9 0x12C3 BNE L12D3 0x12C5 JSR L5479 0x12D3 L12D3 STR '\cM\cDCATALOG\cM\0' 0x12D3 L12D3 JSR L5479 0x12EF STR '\cM\cMSAVE OR RESET (S OR R) '... setting aside some of the minor formatting bugs in the disassembler: this is looking really interesting. Not only do I now see something that's talking about 'S' and 'R', but I also see a clear entry point to the function that is definitely responsible for saving and restoring! Since I never see these messages, I'm assuming that the magic memory regions $A7AD and $A9A1 are implicated in the crash.
Region $A000 is DOS. So this makes sense - we're about to try to invoke some DOS actions. The string at 0x12D3 is chr(4)CATALOG -- where control-D (character 4) is a magic way to tell DOS that you want it to execute a command. And CATALOG is the DOS way to 'ls' or 'dir'. Yes yes yes. This is it, folks. If I were playing hot/cold, we'd be boiling right about now.
The only trouble is that I have no idea what $A7AD is supposed to do. It's not familiar to me at all. None of my reference books (now PDFs; I've long since gotten rid of the paper copies, so thank you whomever scanned these things and put them on the Internet!) mention that vector. So I'm stumped until some Google-Fu lands me here, back in 1980, reading Micro, the 6502 Journal.
As was the fashion at the time, clever people were always coming up with little programs to do this-or-that better than before. They'd publish the listings in publications like BYTE Magazine. Or, apparently, Micro. And on page 9 is "A Little Plus for your Apple II", by Craig Peterson. Thank you Craig. And thank you Micro, for publishing him. Because right there, on the right side of the page, Craig says:
Also, this example is setup for use with 3.2 DOS on a 48K system. If you have 3.1 DOS and 48K memory, use DOS addresses $A7AD and $A99E in place of $A851 and $AA5B in lines 200, 210, 400, 640, and 690.
That's the Rosetta Stone. $A851 I know! That's the vector to re-initialize the DOS hooks so that you can make DOS calls from inside a binary program. Wow, that tells me a lot. Combined with the history I know about the TMI game (that I've read online), I can now reconstruct how this image came to be, and I think I know how to fix it.
TMI was purportedly originally an Integer BASIC game. I never saw it in that form; it was rewritten in machine language in 1980 - compiled, as far as I can tell, by a fairly inefficient compiler that stomps all over zero page and redoes work and whatnot - and must have been originally distributed, in that form, for Apple DOS 3.1.
It had to be for DOS 3.1. That's the DOS 3.1 entry point for the thing I know of in DOS 3.3, that my new bud Craig says was in DOS 3.2.
But this copy is on a DOS 3.3 diskette. How did that happen? And can we just put it back on a DOS 3.1 disk and run it? Well, no, we can't just put it back.
DOS 3.1 and 3.2 used 13 sectors per track on the disk. When Apple were working on 3.3, they found a sneaky way to increase that to 16 sectors per track, which gave them more storage on the media. But it's not backwards-compatible. People had to lug their Apple ][ back to the dealer for an EPROM replacement on the disk drive controllers in order to use DOS 3.3. Which means that my emulators would need a copy of the 13-sector ROM for DOS 3.1 in order to use it. Which I don't have, and have little interest in looking for; I'd have trouble figuring out how to transfer the binary back over to that disk, and then I'd still have some strange nonstandard emulator configuration that I couldn't run easily. Nor could I fix this problem for the world without crazy instructions about replacing ROMs (because, y'know, the world obviously wants Three Mile Island fixed so that they can all play it without it crashing - so that they can melt down a nuclear reactor in peace and all).
My guess is that all of the copies of this disk out on the Internet came from the same person. I'm guessing that someone used MUFFIN or some other Apple sector converter tool (hey, that's how I could get the binary on a 13-sector disk! Still doesn't fix the "need a special ROM to use it" problem, but anyway) - ahem, or some other Apple sector converter tool to copy it on to a 16-sector DOS 3.3 disk. And that one plays the game. Mostly. With a little crashy DOS 3.1 incompatibility left behind.
I doubt this is a copy from the manufacturer, though. I remember playing this game in middle school, and I don't recall it crashing. And I know we were running DOS 3.3, which means 16-sector disks, which means there was a DOS 3.3 compatible version of TMI. It had a well-printed label, as I recall. I'm reasonably sure it was an authentic original. So the manufacturer *had* a copy that was 3.3-clean; not this abomination of a 3.1 copy on a 3.3 disk.
But then again, we don't need to put it back on a 3.1 disk to make it work again.
All we have to do is change which DOS entry point it's using. It's just this one place in the game, these two little bytes (little-endian order), which need to read:
Save. Reboot to disk copy. Copy from ProDOS-mounted Mac volume on to virtual disk. Swap disks, reboot. Aaaannd...
HELLZ YEAH! No crashie. Time to stick my name in here for the old-timey "I done cracked a warez" feeling, but better, since I've just fixed a bug:
That's one 37-ish-year-old bug squashed.
If you want to grab a copy of the disk image for yourself, here's a copy on my webserver or you can now get a copy from the Asimov archive. Or you can grab the original from just about any Apple ][ disk archive and edit the binary yourself. You're welcome, world.
And now if you'll forgive me, I have a nuclear reactor that I have to go melt down.