If you look through a game’s routines long enough you’re bound to find something wrong. Maybe a register was clobbered. Maybe something was written twice when it didn’t need to be. Maybe something goes horribly wrong and just happens to work out, disaster narrowly avoided. It’s likely that your average player (and/or your average hacker) might never see these goofs, so I figured it’s time to have a thread for them.
Post whatever bugs, compiler goofs, etc. here. If anything this thread will keep me from pasting random snippets of ASM into the discord server, and hopefully this thread’ll be a good read.
Let’s get into it. I like to play around with FE5, so these examples will be 65816 assembly. I’d like to explain each of these so that someone with little to no ASM experience could hopefully follow along.
#Block Transfer To/From Anywhere
65816 has two block memory transfer opcodes, MVP and MVN, which are similar to THUMB’s ldmia+stmia for transferring data. The opcodes include the bank (the upper 8 bits of a pointer on the 65816) for both the source and the destination, with the number of bytes to transfer and the lower 16 bits of the source and destination in CPU registers. This poses a bit of a problem, as you can only copy data to/from locations known at compile time (you need to know the banks to write the opcode). To overcome this, FE5 has routines that build a block transfer routine in RAM. When you need to copy data, you fill out the banks of the opcode in RAM and hop to the routine. It’s quite clever in my opinion.
The first byte of the MVN/MVP opcodes are written to RAM on startup, along with another opcode to return from the routine:
Routine (written for the 64tass assembler)
blockcopy_copier
phb
php
phk
plb
sep #$20
ldx #size(mvn_routine) - 1
-
lda mvn_routine,x
sta $04AE,x
dex
bpl -
ldx #size(mvp_routine)
-
lda mvp_routine,x
sta $04B2,x
dex
bpl -
plp
plb
rts
mvn_routine
mvn #$00,#$00
rts
mvp_routine
mvp #$00,#$00
rts
unknown_routine
phb
If you don’t know 65816, this might be mumbo jumbo to you, so let’s break it down. We’re copying two routines, mvn_routine and mvp_routine, to RAM addresses $0004AE and $0004B2 respectively. We copy them end first, using a loop counter in the X register. We need this counter to be one byte less than the size of the routine because we’re looping with a BPL opcode (0 is considered positive). After each byte, we decrement the loop counter.
MVN Transfer breakdown
X Byte Part
0003 60 rts
0002 00 destination bank
0001 00 source bank
0000 54 mvn
FFFF end loop
Here’s the issue: when copying the MVP routine, the size wasn’t reduced by one, so the first byte of the next routine (a phb opcode) gets copied into RAM at $0004B6, overwriting whatever was there accidentally. Man, that’s a huge explanation for such a tiny thing, right? So, what was originally at $0004B6? $0004B6 is used exactly once when setting up the sound system, and probably didn’t even need to be used. Lucky us, nothing of value was lost. Even better, the only known routines that use this block memory copier look like this:
MVN Routine user (64tass syntax)
phb
php
; program bank -> data bank
phk
plb
phx
phy
; get the source, dest banks and
; build the mvn opcode
sep #$20
lda $04AB ; dest bank
sta $04AF
lda $04A8 ; source bank
sta $04B0
lda #$54 ; mvn opcode
sta $04AE
lda #$60 ; rts opcode
sta $04B1
rep #$20
; get params
ldx $04A6 ; source
ldy $04A9 ; dest
lda $04AC ; size
dec a
; cool trick, can rts
; because $0000-$2000
; of RAM mirrored to
; every bank
jsr $04AE
ply
plx
plp
plb
rtl
These rewrite the entire MVN/MVP routines anyway! $0004B6 was clobbered needlessly!
There’s some interesting other things to consider: The way the routine user loads the parts of the routine as literals and writes them to fixed points in RAM is faster than the startup routine. The startup routine would probably be faster if it actually used MVN/MVP to copy the MVN/MVP routines. And, finally, none of these seem to be called.
Same thing happens in FE4 in the same place, too.