SNES ROM loader extension for Ghidra, bundled with a
DBR/DP tracking analyser tuned for the 65816 architecture. Originally based on
achan1989/ghidra-snes-loader (archived upstream) and re-modernised for the
current Ghidra release line.
This is the loader half of the setup. It depends on the companion 65816 processor module:
ghidra-65816— language ID65816:LE:16:default. Without it the loader has no language to bind to.
- Auto-detects LoROM, HiROM, ExHiROM with and without an optional 512-byte SMC copier header.
- Decodes the cartridge header at
$00:FFC0/$7F:FFC0and surfaces the title, region, ROM/SRAM size and version code. - Recognises the coprocessor / cartridge add-on advertised in the cartridge-type byte: SA-1, SuperFX (GSU-1/2), S-DD1, OBC-1, S-RTC, Cx4, SPC7110, ST010/011/018, plus the older DSP family.
- Maps every ROM bank into the 65816 24-bit bus (LoROM, HiROM, ExHiROM aware).
- Maps WRAM (
$7E:0000-$7F:FFFF) and the LowRAM mirror at$00:0000-$00:1FFF, with byte-mapped mirrors in every bank$01-$3Fand$80-$BFso code that runs with a non-zero DBR resolves direct/absolute reads cleanly. - Maps the hardware-register window
$00:2000-$00:43FF(covers MSU-1 plus the PPU/CPU/APU/DMA windows in one block), again mirrored into banks$01-$3Fand$80-$BF. - Maps cartridge SRAM at the canonical LoROM (
$70:0000) or HiROM ($30:6000) location when the header advertises any.
- Names every PPU register (
INIDISP,BGMODE,VMADDL,CGADD, …), CPU register (NMITIMEN,WRMPYA,RDDIVL, …) and all 8 DMA channels (DMAP0,BBAD0,A1TL0, …) with a per-cell EOL comment so the disassembly reads symbolically instead of as bare addresses. - Names the MSU-1 / SD2SNES streaming registers at
$00:2000-$00:2007(status/identifier on read, seek and audio control on write). - Names the SA-1 / SuperFX / S-DD1 coprocessor register windows when the header advertises that chip.
- Applies a
SnesCartridgeHeaderdata type at$00:FFC0so the cart metadata is decoded inline in the listing. - Resolves and names every native- and emulation-mode interrupt vector
(
vector_RESET,vector_NMI_native,isr_irq_native, …) and creates a function at each target so analysis starts from real entry points.
A bundled SNES DBR/DP Tracker analyser walks the disassembly looking for
common 65816 idioms and writes the resulting Data Bank Register / Direct Page
values into the program context so the decompiler can resolve absolute and
direct-page addresses correctly. Patterns recognised today:
| Pattern | Effect |
|---|---|
PHK ; PLB |
DBR := PBR (current bank) |
LDA #imm8 ; PHA ; PLB |
DBR := imm |
PEA #imm16 ; PLB ; PLB |
DBR := imm.high |
LDA #imm16 ; TCD |
DP := imm |
LDA #imm16 ; PHA ; PLD |
DP := imm |
PEA #imm16 ; PLD |
DP := imm |
Every value is committed forward to the end of the containing function (or
until the next conflicting write), so e.g. a LDA $1234,X that follows a
PHK ; PLB decompiles as a load from <currentBank>:1234,X instead of from
the loader's tracked default of $00:1234,X.
The bundled SNES Function Discovery analyser makes the first decompiler pass
less sparse by ensuring direct 65816 JSR/JSL call targets have functions
when Ghidra has decoded the call flow but not already created one. It
intentionally does not guess through computed jumps. Instead, indirect
JMP/JSR/JSL sites are labeled as candidate_indirect_* so jump-table and
dispatcher work can be handled by a human or by a later game-specific script.
The extension is built with Gradle and a local Ghidra installation. Set
GHIDRA_INSTALL_DIR to the unpacked Ghidra root.
Linux / macOS:
cd SnesLoader
GHIDRA_INSTALL_DIR=/abs/path/to/ghidra ./gradlew buildExtensionWindows (PowerShell):
cd SnesLoader
$env:GHIDRA_INSTALL_DIR = 'C:\abs\path\to\ghidra'
.\gradlew.bat buildExtensionThe output is SnesLoader/dist/ghidra_<version>_PUBLIC_<date>_ghidra_snes_loader.zip.
- Install
ghidra-65816intoGhidra/Processors/65816and runsupport/sleigh -aonce to compile the.slaspecto.sla. - In Ghidra, File → Install Extensions → + → pick the zip above. Restart.
- Open any
.smc/.sfc/.swcfile: it should pick up automatically as a SNES ROM with the 65816 language.
Without step 1, the loader has nothing to bind to and import will silently fall back to a different language.
Visible at import time and via the headless -loader-snes* command-line
flags. All default to on.
| Option | CLI flag |
|---|---|
| Map SNES hardware registers | -snesHwRegs |
| Mark interrupt vectors | -snesVectors |
| Apply Cartridge Header datatype | -snesHeader |
Map LowRAM mirror at $00:0000 (and all bank mirrors) |
-snesLowRamMirror |
| Map cartridge SRAM (when present) | -snesSram |
| Label MSU-1 streaming registers | -snesMsu1 |
| Label coprocessor registers (SA-1, GSU) | -snesCoproc |
| Mirror hardware-register labels into all banks | -snesMirrorHwLabels |
The Build Ghidra extension workflow runs on every push and pull
request, on ubuntu-latest and windows-latest in parallel:
-
Downloads the requested Ghidra release.
-
Clones the companion
ghidra-65816processor module and compiles its SLEIGH spec (both65816and65802). -
Builds the loader as a Ghidra extension zip with Gradle 8.5 / JDK 21.
-
Installs the extension into the Ghidra checkout.
-
On Linux only, runs three behavioural smoke tests, each synthesised on the fly (no committed ROM blobs) and run through
analyzeHeadlesswithtests/PrintSnesArtifacts.javaas the post-script:ROM What it pins synth-min-lorom.pyLoROM detection, every vector label, bank-mirror blocks, mirrored NMITIMEN @ $00/$01/$3F:4200,Resetfunction entry,ctx_E/M/X=1at$00:8000.synth-idiom-lorom.pyDBR/DP analyser correctness: LDA #$80; PHA; PLBpropagatesDBR=$80to$00:8009,LDA #$1234; TCDpropagatesDP=$1234to$00:8012.synth-min-hirom.pyHiROM mapping: bank- $C0primary block +$00:8000and$80:8000upper-half mirrors, map-mode byte$21at$00:FFD5.
A missing marker fails the build and uploads the headless log, the
marker file, and the synthesised ROM as a smoke-test-* artefact for
offline diagnosis. The Windows job verifies that the extension zip
builds cleanly under Git-Bash + Gradle's Windows shims, which is where
most user-visible packaging issues have historically appeared.
This gives a regression test for the loader, the DBR/DP analyser, the HiROM mapper, the mirror-block construction, and the per-vector context overrides — without shipping any binary ROM in the repo.
docs/GHIDRA-STACK-WORKFLOW.mddescribes how to use this loader withghidra-65816andghidra-spc700.tests/ghidra-stack-smoke.ps1compiles the local 65816 and SPC700 modules inside theghidra-mcpcontainer, builds/installs the local SNES loader, and imports a synthetic LoROM through that loader.tests/real-rom-smoke.ps1can sweep a local private ROM directory and write local-only marker summaries under.local-test/; these may include short instruction text markers and must not be committed.tests/export-structure.ps1runs a local import and writes a payload-free structural JSON summary for downstream validation notes.tests/export-structure-batch.ps1sweeps a private ROM set and writes a count-only local batch summary for comparison.tests/export-apu-candidates.ps1writes a local payload-free list of functions/instruction addresses that reference or contain scalar candidates for APU ports$2140-$2143.tests/export-flow-candidates.ps1writes local payload-free indirect-flow and pointer-table candidates for manual dispatcher/table review.docs/SMT1-VALIDATION-SEEDS.mdlists high-value real-ROM validation questions without making SMT-family games committed fixtures.
Real games such as SMT1, SMT2, and SMT if... are validation targets only. Do not commit ROMs, decoded text, copied disassembly, screenshots, graphics, audio samples, maps, scripts, save payloads, or raw byte ranges.
Originally based on
achan1989/ghidra-snes-loader
(archived upstream) under MIT. This fork adds cartridge add-on detection, the
DBR/DP tracking analyser, MSU-1 / SA-1 / SuperFX / S-DD1 register labels, an
ExHiROM-aware memory map, and a refreshed cartridge-header datatype.
Released under the original MIT license — see LICENSE.