Entry 3: My Remembery Is Faulty (or: What's That Address Again?)

A project log for Fixing 37-year-old software: Three Mile Island

I'm an Apple ][ fan. One game from my childhood keeps crashing on me... and it's time to finally fix it!

Jorj Bauer 01/09/2017 at 20:051 Comment

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      MAINLOOP
Well, 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                  RTS
Read 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.


Simon Scudder wrote 4 days ago point

Congratulations, great project!

I also have fond memories of TMI, and the inner workings of the program have always interested me, but I never got around to tinkering with it.

  Are you sure? yes | no