"SNESRC" is an acronym meaning Super Nintendo Entertainment System Re-Compiler. When referring to the program, it is spelled with lowercase letters: snesrc.
Snesrc was an experiment intended to reproduce the complete assembler source code for SNES games. Its main algorithm is a recursive half-emulation of the 65c816 CPU. The theory is that doing this, it can trace all possible program paths to create a "map" of the ROM file which can disassemble the file with proper code/data separation.
git clone https://github.com/parasyte/snesrc.git
The map contains information about whether a byte is part of an instruction; is read as data by another instruction; is the destination of a branch, jump or pointer; or was fixed or not marked 'clean'. The map also contains some information about the state of the CPU when that instruction was encountered.
Because of the recursive nature of the algorithm, it will never trace over a byte twice. This aspect also gives it some severe design flaws. The biggest one being that the CPU status attempts to evolve throughout the recursive tree. A branch- or jump-instruction creates a new branch (fork) within the recursive tree; when that branch ends, the algorithm backs out to the last fork and continues running.
The three conditions which can end a branch are: interrupt returns, hitting an instruction which was already traced, or hitting an instruction which appears to be invalid. In the latter case, the algorithm attempts to make an adjustment to the CPU state and re-run over the last branch of instructions (which are still marked 'dirty'). If the branch then ends on one of the other two conditions, the map is marked 'clean' (all dirty bits are removed) and the algorithm continues from the last fork.
The biggest concern with the 65c816 is that its instruction sizes can change dynamically as the program runs (similar to the ARM line of CPUs). This makes it difficult to disassemble the program; let alone to separate code from data. Therefore, it is possible for a string of instructions to be interpreted differently depending on the CPU state when the string is executed. The CPU can have four possible states which can change instruction sizes. These states depend upon the three CPU status bits which change the size of the accumulator and index registers: I, M, and E:
- When I is set, the index registers X and Y are 8-bit. When clear, X and Y are 16-bit.
- When M is set, the accumulator register is 8-bit. When clear, the accumulator is 16-bit.
- When E is set, both the index registers X and Y, and accumulator register are 8-bit. When clear, X, Y and accumulator sizes depend on the states of I and M, respectively.
Another common problem area encountered is with jump tables: seen with JMP ($xxxx,X) and JSR ($xxxx,X) instructions, where $xxxx is often a ROM address listing several pointers. Some of these tables can be found and recursed into by snesrc, but not in all cases.
When $xxxx is a RAM address, we have no idea what it's trying to jump to; this is also a problem for JMP (indirect) and JMP [indirect long] instructions. (See Known Bugs.) This is common when a function pointer is sent to a subroutine as an argument, or when a pointer needs to be modified in some way. These corner cases are difficult to anticipate and handle correctly. Especially with this particular algorithm.
The recursion itself is problematic; RTS/RTL instructions do not back out to the last fork, but instead try to pull the return address off the stack. This leaves the CPU state as it was when it left the subroutine (technically correct). But we still run into strange cases where the emulator will throw a warning or an error (usually an invalid instruction). This seems to be caused by incorrectly following the program path, but expecting the CPU status to be correct in all cases.
- JMP (indirect) and JMP [indirect long] instructions are not supported.
- The -fcop, -fstp, and -fwdm command line arguments are ignored.
- Needs a command line argument to force the header size (to 0 or 512).
$ ./snesrc snesrc - The SNES Recompiler v0.01 Copyright 2005 Parasyte Usage: snesrc [options] <input file> <output dir> Options: -l: Force LoROM -h: Force HiROM -r<n>: Set pointer table range check size -fbrk: Attempt to fix code which reaches a BRK instruction -fcop: Attempt to fix code which reaches a COP instruction -fstp: Attempt to fix code which reaches a STP instruction -fwdm: Attempt to fix code which reaches a WDM instruction
I used the 2.68 MHz Demo by Abandon for most of the testing. It's not possible to force the header size, so you will want to chop off the first 512 bytes of the .smc file using a hex editor. (This 512 bytes is the SMC header; it's mostly null bytes). This ROM file is also not padded correctly, so you must force snesrc to read the ROM with the LoROM algorithm, using the -l command line argument.
$ ./snesrc -l -fbrk 2mhz.smc 2mhz
This will create a new subdirectory called 2mhz/ where you will find the disassembled bank files (bankXX.asm) and the disassembler pass logs: pass1.log, pass3.log. Since Pass 2 is specific to flushing map bytes, it does not currently log anything.
This experiment shows some promise that it is possible to reproduce valid (although not always complete) assembler source code out of simple SNES ROMs. In my tests, I've seen the most success with small "public domain" SNES ROMs. (Technically, these are homebrew ROMs, and are subject to the author's copyright as applicable.)
It would be better to replace the recursive tree with a FIFO system. Thus, any time a conditional branch or subroutine call is found, the destination address and CPU status are placed on top of the FIFO. Pull that information out of the FIFO for each run until the FIFO is empty. This would make tracing much easier to debug than a maze-like recursion tree. It should also be much closer to the path taken by an assembler as it was creating the ROM.