Post

RomHacking with Ghidra, Part 1: Loading SNES ROMs

The S is for Super, not Standard

Adding to the myriad of (unfinished) projects I’ve still got on the backburner, I’ve recently started looking into RomHacking in order to translate the dialogs of a fighting game. Since my previous reverse-engineering endeavors in Ghidra were a very pleasant experience, I decided it would be great to keep using that tool for this next task.

I loaded the ROM in Ghidra, started it in the Mesen2 emulator, and started to delimit the first string I had found in the debugger. Alas, it was not in its expected place: I could find the exact bytes I was looking for, but they were in a completely different place.

Treachery! Ghidra had perfectly mapped the ELF sections the previous time, why wasn’t it doing the same here? What’s so special about an SNES ROM?

Straight from the shoulders of giants

Fresh from last time’s mistake, I decided to start fresh from what was already there: an archived ghidra-snes-loader repository exists. Let’s see…

Works with Ghidra v9.1 or later only.

So claimed the documentation. Unfortunately, “or later” apparently meant “but before your version”. Still, let’s check the code of that SnesLoader.java. Clearly that’s extending an abstract class, setting some boilerplate configurations through getters, and then the actual loading logic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SnesLoader extends AbstractProgramLoader {
  @Override
  public Collection<LoadSpec> findSupportedLoadSpecs(ByteProvider provider) throws IOException {
    // (...)
  }

  @Override
  protected List<Program> loadProgram(ByteProvider provider, String programName,
      DomainFolder programFolder, LoadSpec loadSpec, List<Option> options, MessageLog log,
      Object consumer, TaskMonitor monitor)
      throws IOException, CancelledException {
    // (...)
  }
}

That all looks very easy. So that findSupportedLoadSpecs obviously sets the language/architecture, and loadProgram will set up the ROM file1. Pretty basic. What’s next? Ghidra invokes loadProgram, passes it the ROM content in ByteProvider provider, and then it all goes in the memory map, that’s easy, and (as we’ll see later, dear reader!) the Ghidra extension model is actually pretty pleasant to work with.

Supporting the 65816 processor was pretty easy too, I just had to copy over the data/languages folder from the (archived) <gh repo=achan1989/ghidra-65816>ghidra-65816</gh> repository, fix a minor bug, add some specialization to the spec, so it would be tied to my extension, and call it a day.

This feels easy because it…sorta is. If all you want your loader to do is “recognize that file type, assign it that language/arch”, you can stop reading here.

But if you keep reading, the second line of the load method invokes detectRomKind(provider). I knew SNES ROMs had different sizes, but different types… I guess that’s not about RPGs and platformers?

Banks and rommers

I’m no stranger to romhacking, ROMs and ISOs, but I never thought too much about it. This changed with the 3DS and PSVita scenes, where translations and mods would be distributed using the LayeredFS and rePatch methods respectively. Those were no mere .ips or .xdelta patches, but an imitation of the game’s filesystem, only containing the modified files, with the system’s open(3) syscall being hooked with a conditional loader:

1
2
3
4
5
6
7
int open(const char *path, int oflag, ... ) {
  char *patchPath = replace(path, "/game_path/", "/patch_path");
  if (exists(patchPath)) {
    return _open(patchPath, ...)
  }
  return _open(path, ...)
}

A very smart piece of technology, which makes me wonder why it wasn’t invented before the 3DS era. Could it be because a ROM, being Read-Only Memory has no concept of a file system?

See, ROMs have always looked like the exact representation of a game’s cart, and nobody running ZSNES or Snes9x would dare challenge that idea, since all the data was there, and there was nothing missing, right? This was all a sweet lie perpetuated by emulator developers because the approach of emulators (at the time) was to use shortcuts in emulation to work in spite of the low specs of emulation machines.

The statu quo was questioned when byuu/Near first published the bsnes/higan emulator: it required powerful (at the time) machine specs to run, but was the first to entirely emulate the SNES down to each of its circuits, be they inside the console, or inside the game cart. Because yes: there are different chips inside the carts, and the ROM was only one of them!

This new model completely reset the preservation efforts for the console: GoodSNES2 only included the ROMs and there was no way to know which chips came with it. The emulators were just guessing with heuristics, or maintaining their internal database, which was the only way an emulator would know that you could save your game. Thanks to byuu/Near’s preservation efforts, all game carts were identified by boards, chips, ROMs,… and mappings. But those “chips” didn’t explain why the bytes in my debugger didn’t line up with the bytes in my ROM, nor with the bytes in Ghidra!

See, one thing we didn’t touch on is that the SNES is a 16-bit console, which means it performs calculations over 16 bits, 2 bytes, in the hexadecimal range 0x0000-0xFFFF (so, 0-65,535 in decimal). If it could only address 64KB3, how could it access data over a 1MB4? The same way everyone else did: Bank switching. Depending on a single 8-bit register (0x00-0xFF, so 0-255), memory accesses would now be directed to the chosen “page” of the ROM. You’re in page 4 and want to read something in page 6? Easy: just bank-switch!

I like that “page” analogy. It’s just a shame that the real model is closer to the Necronomicon than to a neatly numbered book.

O ye’ll take the high ROM and I’ll take the low ROM

The full system is a bit complicated5, but in short, the SNES and the game cart will form a single unit, and share the same memory layout. As shown in the following image (courtesy of SNesDev Wiki), some areas will ALWAYS be dedicated to the SNES I/O and RAM:

  • In bank $00, from 0x0000 to 0x1FFF are the first 8KB of RAM.
  • In bank $00, from 0x2000 to 0x5FFF is the full SNES I/O in multiple registers.
  • These two mappings are also accessible from $01 to $3F and from $80 to $BF. It is not a separate mapping but a mirror that functions just the same, like a shortcut.
  • In banks $7E to $7F, from 0x0000 to 0xFFFF, the whole SNES WRAM can be accessed.

And then, there is the rest, which the cart is free to dedicate to ROM mappings as it pleases. In practice, there are two (and a half) canonical mapping types:

  • LoROM will split the ROM into chunks of 32KB (so 0x8000 bytes), and map them to banks $806 (128) to $FF (255). Since the chunks are half the size of a bank, they will be mapped from 0x8000 to 0xFFFF.
  • HiROM will split the ROM into chunks of 64KB (so 0x10000 bytes7). Since the lower parts of banks $80 to $BF already contain mirror mappings of the SNES RAM and I/O, these ROM chunks will instead be mapped in banks $C0 to $FF.
  • ExHiROM is an extension of HiROM and will map its first 64 banks the same way, then map the next ones from $40 to $7D (and not $7F since it’s used by the RAM!), and can ALSO map two final banks in $3E and $3F, for a total of 128 banks.

This mapping was a bit of a pain to implement, but nothing that a good loop cannot solve, although I must admit that for the mapping of the last two banks of an ExHiROM, I decided this was “a problem for someone skilled enough to be working on the games that would require them”. Don’t thank me, I love providing my peers with pull request opportunities.

So, now that we know how to map, how do we know whether our ROM is LoROM or HiROM?

The safest method is to calculate the ROM’s SHA256 digest and compare it to bsnes/higan’s Super Famicom.bml, a COMPLETE database of all identified SNES carts. If we get a result, we can identify the board model, then use the nocash specifications to identify the ROM type. Needless to say, it won’t work on RomHacks, since they are not included in the Super Famicom.bml database.

The old-school but proven method is the heuristic emulators usually rely on, based on detecting the ROM’s title label:

  • In LoROM, this is ROM offset 0x7FC0.
  • In HiROM, this is ROM offset 0xFFC0.
  • In ExHiROM, this is ROM offset 0x40FFC0.

So in the same file, we read 21 bytes at each of those addresses, and depending on which is the most plausible title, we know the ROM type. Those numbers may seem arbitrary, but it’s because the SNES expects the ROM header contents (which include the title label) in bank $00 at 0xFFC0.

Wait, I never said this area was mapped, no?

Mirror, mirror, on the ROM, who’s the fairest of them all?

By this point, I had already seen the CPU vectors defined in the language specs, and I knew of the VEC_RESET_EMULATION vector, which is where the console looks for the ROM entry point, but I had never noticed its value: $00:FFFC.

And what the hell, even using Ghidra’s “Go to”, I could never access this address: it just DID NOT exist in my listing, as if there was nothing there, and obviously there was nothing, because we’ve already discussed where the ROM chunks are mapped. So how is the SNES expecting to find ROM code here?

Well, do you remember how I told you the bank $00’s I/O and RAM mappings were also accessible from $01 to $3F and from $80 to $BF?

When you stare into the SNesDev documentation for too long, it stares back at you.

Yes, the area from $00:8000 to $00:FFFF is a complete mirror of one of the canonical mappings of the ROM. And the best part? It’s not always the same.

  • In LoROM, it mirrors $80:8000 to $80:FFFF.
  • In HiROM, it mirrors $C0:8000 to $C0:FFFF.
  • In ExHiROM, it mirrors $40:8000 to $40:FFFF.

While this is enough pain on its own, it’s at least kind of standard. Would you believe me if I told you there were EVEN MORE mirror mappings, and that all of them are dependent on the physical board in the cart?

Smashing the banks for fun and profits

At this point of my research, the first thing that came to my mind was just a question: “How did the emulators of old manage to work that well?”. The truth is that most of them were KINDA duct-taped, and using heuristics to deduce the mirror mappings, and TECHNICALLY, you can just see the memory layout as eight zones, with two taken by the SNES I/O and RAM, and mirror the canonical ROM banks everywhere you can.

And then, the emulator has to add more duct-tape because “actually, this game does NOT have ROM mapping here, and uses that behavior8 to check that it’s running on real hardware”.

Discovering it made me imagine an emulator-proof board and ROM: a ROM can have its entry point at an unexpected offset, but as long as the board properly maps it to $00:FFFC, a console will run the game, and an emulator will fail, until a specific board mapping rule is added for the emulator.

But really, don’t I have enough work mapping the mirrors I already know about?

99 Bottles of Chips on the wall

So in practice, I now have a loader that currently only exposes the canonical mappings directly, and leaves the weirder board-specific mirrors to future work (and user-defined mappings). Which is honestly fine for a first version: by that point, the goal wasn’t to perfectly emulate every possible SNES board, but to finally have Ghidra display the same addresses as the debugger (though if the code relies on mirror mappings, they aren’t visible yet).

Finally, for that one ROM I was working on, once I reimported it, suddenly, the bytes lined up. Time to hack.

Credits and Acknowledgements


  1. In Ghidra parlance, a “program” is a file you’re analyzing. ↩︎

  2. The GoodTools are tools to properly rename ROMs according to their hash, and the set of renamed files it produces are called GoodSets, with the one related to the SNES being GoodSNES↩︎

  3. Of course, it’s a bit more complicated: the 65C816 processor uses 16-bit registers, but a 24-bit address space split into banks. Most instructions operate within the currently selected bank, which keeps addresses smaller and operations faster. ↩︎

  4. The two largest games released for the console, Tales of Phantasia and Star Ocean, both used 6MB ROMs. ↩︎

  5. But you can read the full specifications by nocash if you’re curious! ↩︎

  6. In SNES development lingo, bank numbers are usually prefixed with a dollar sign, like $00 to $FF↩︎

  7. Yes, in hex, the double of 0x08 is 0x10↩︎

  8. Technically, you can read/write to non-mapped areas of the memory layout to trigger console-only behavior. ↩︎

This post is licensed under CC BY 4.0 by the author.