0x000 Every fighter is a module
Load a match in Super Smash Bros. for Nintendo 3DS and the game assembles it out of individual items. Mario's moveset is a handful of assets. The stage under him is a handful of assets. The Assist Trophy that just launched you off the top is a handful of assets. Obviously, none of them are bound to or live inside the main executable code; they're just puppets that the code pieces together to give you a cohesive video game. The original Nintendo 3DS has 128 MB of RAM, and Smash has fifty-odd fighters, a forty-plus stages, and Assist Trophy summons, menus, minigames. Most of it are discrete assets such as animation command files that dictate animation speed, which bones hitboxes get attached to, how much damage they do. Parameters are parts of physics or game logic that is shared between assets to different degrees, and the value of the param determines their intensity. All of these different assets reference the same game code. The game loads each piece off the cartridge the instant it's needed and drops it the instant it's done with its share of CPU time.
The format many of these pieces are stored in is CRO, named for its four magic bytes, CRO0. If you've modded a PC game, it is the 3DS equivalent to DLLs: self-contained programs that carry their own code, their own data, and the records needed to slot them into memory and wire them up to other software at runtime. Each fighter in Smash 4 is therefore basically its own mini program. There are 189 of them on one Smash cart.
That's the problem. We can already do plenty to a fighter via param edits or via AnimCMD, which basically is an engine that attaches properties such as animation speed, coloured overlays, and attached hitboxes to the bones of each mode. All of Smash 3DS modding was just modifying existing assets; swap fighter textures, import a new model, import new music, retune hitbox damage and knockback through parameter tables. What you can't do is change what a fighter does, meaning giving it a new special, a mechanic no parameter controls, behaviour that only exists as compiled code inside the CRO. And you certainly can't add a fighter that was never in the game. Getting there takes two things: putting your own code into these modules, and eventually building new ones from scratch. Everything below is the slow walk toward those two things.
ShinyQuagsire's SaltySD is the mod loader that all Smash 3DS modding relies on, and it is the reason any of this is testable at all. More on exactly what it does in a minute. Picking up from there, the aim is to understand CRO well enough to hijack existing programs and eventually to author them from scratch, leaning on an automated agent for the parts that are more tedious than anything.
0x040 The format that describes itself
The header carries four cryptographic checksums, each taken over a different region of the file: the header, the code, the data, and a fourth covering all the verification in between. Every module Nintendo shipped keeps all four valid, which looks like a defense, but really isn't for us. By the time you're concerned with running custom CRO, you're running either on a Luma3DS setup with signature verification removed, or an emulator which never had them to begin with. Some people in the community did simple edits for state-transition modding, such as enabling jump-cancels for Fox, or ending Fox Illusion's aerial version with Special Fall instead of normal fall. A decade of raw hex-edited CROs ran fine with stale checksums. My tooling recomputes all four on every write anyway, not to appease any loader but so an edited file still passes the same byte-exact verifier that validated the originals.
Underneath the checksums it is a small linker in its own right, and three details of it mattered later.
First, a module is split into segments: one holds executable code, one holds constants, one holds writable data, one is zero-filled scratch space. Every pointer inside the module is written as a segment plus an offset rather than a raw memory address, because the module has no idea where in memory it will land until the moment it's loaded.
Second, a module's exported function names are looked up through a Patricia trie, which is a compact search tree that identifies a name by checking its individual bits one at a time instead of comparing whole strings. Understanding of it can be verified by resolving all 3,852 exported names across the 189 modules.
Third, every pointer that needs a real memory address is written as zero on disk. A separate table, called the relocations, lists all those empty spots, and the loader fills each one in once it knows where the module landed. Which means you cannot find a pointer by searching the file for its value, because the value does not exist yet. You find it in the relocation table.
0x0A0 Splicing in code
With a byte-exact writer in hand, the path from an idea to new fighter behaviour looks like this:
C source → devkitARM (a compiler for the 3DS's CPU) → an object file
→ pull out its code and data sections
→ append them onto the module's segments
→ turn the compiler's relocations into CRO relocations, add jump stubs
→ rebuild every offset, recompute the four checksums
→ an injected .cro
Two quirks of the 3DS's processor make this fiddlier than a normal build. It is an old enough ARM design that it cannot load a full memory address in a single instruction, so every address the code refers to has to be stashed nearby as data and pulled in, which is one more thing the relocation table has to track. And when the injected code calls an engine function the fighter didn't already use, it needs a tiny jump stub wired into the module's import list to reach it. A structural checker of 24 rules the format is supposed to obey comes back clean at 24 / 24, producing a modified menu module with one extra function spliced in. It goes into Azahar simply because menu is among the first things to load in the game, so it's easy to cycle through opening the game ad infinitum to check for progress.
0x100 Byte-perfect, and it hangs
According to my checker, it was a perfect patch. But it hung Azahar. Black top screen, the emulator's speedometer pinned at 0%, frozen there for four minutes with a debugger attached. The emulated console simply stopped the instant the module loaded.
Everything you can check says the module is fine. All four checksums verify. Every offset is in bounds, every relocation is well formed, the name lookup resolves, the checker is green, and the real loader takes one look and locks up the whole console. Passing every rule that could be written down, it turned out, was necessary but nowhere near sufficient. There is a gap between "a legal CRO file" and "a file the loader will actually accept once you've grown it," and the rest of this is a tour of that gap.
0x140 Two bytes
To find a gap like that, you go looking for it the slow, dumb way: change one thing at a time and watch which change flips the result from working to broken. To do that you need to get builds onto real hardware fast, and this is where SaltySD earns its billing. shinyquagsire23's loader patches the game so that a file sitting on the SD card quietly replaces the one packed inside the cartridge. Its 1.2 release added exactly the redirect this work needs: drop your module at sdmc:/saltysd/smash/cro/fighter/mario and the game loads yours instead of Nintendo's, with no repacking and no reflashing. Every single test here is one file copied onto an SD card. Without SaltySD there is no loop at all.
The rest of the setup is Azahar, a 3DS emulator, booting the game, plus a debugger attached to read the console's memory. It may well have been setup-specific; whether it was a Citra/Azahar issue in the first place went unchecked. The issue with the GDB stub on Azahar is that it answers exactly one interruption per connection. The console freezes once, gives up what it's asked for, and that's all there is. Ask a second time and it never replies. So every measurement below is a single, carefully aimed shot — a constraint that turned out to be unnecessary, though that only became clear near the end, around the same time everything came together.
An automated agent grinds through the search: inject a legal probe, then undo parts of the injection one step at a time until the hang disappears. It disappears at exactly one step, and the step is something easily treated as harmless bookkeeping. A handful of the module's pointers are aimed at the very end of a segment, so their stored value happens to equal that segment's size. When you grow a segment, the obvious thing to do is bump those values to match the new size. On fighter/mario, the entire difference between a frozen console and a playable Mario was two bytes: one of those end-of-segment values, 0xA35C nudged up to 0xA368, plus the checksum that depended on it.
The checker never stood a chance. Its rule was "this value must be no larger than the segment," and both the old and new values pass that. Only the live loader knew the difference. The fix was one setting: leave those pointers alone. Why they mattered still wasn't clear. That answer was three problems away, and it turned out to be the same secret behind almost everything else.
0x1A0 Does the code actually run?
Loading isn't the same as running. A module can map into memory, but it can also just sit there. So the next question is whether injected code inside a fighter actually executes. The engine's "load Mario" function, the one it calls to spawn the character, can be replaced with a small piece of compiled C that bumps a counter in the module's memory and then calls the original. A solo match then spends the one debugger interruption reading that counter.
The engine had found the function by name, through that bit-testing lookup tree, and jumped into it. It runs, which means, in principle, custom behaviour in a shipping fighter. The proofs stack up from there: injected code calling engine math, injected read-only data, injected writable data, and one oddity worth filing away for later. Every module keeps a little table near the end of its scratch space listing the cleanup routines it runs when it unloads. Grow the module and, watching in the debugger, that table slides to a new address to match, because the loader recomputes where it belongs from the edited size. A tidy confirmation of a formula — or so it seems at this point.
0x200 The control object
Among all its cryptically named C++ functions, every module exports one plainly named one: nnroControlObject_, with a header field pointing straight at it. Format notes called it "the control-command dispatcher" and left it there. Taken apart, mario's is a five-way switch on a number handed to it in a register:
ldr r2, [pc, #...] ; a module-global pointer
cmp r1, #5
ldrcc pc, [pc, r1, lsl #2] ; if the number < 5: jump to that handler
b default ; else: return an error code
The number is a command, 0 through 4. Redirecting that function to a stub that logs every call and then chains back to the original reveals what the loader asks for. As a module loads, the loader calls nnroControlObject_ five times, once per command, in the order 1, 0, 3, 2, 4. Which makes it a way in at load time: code hooked there runs the moment a fighter loads, before the game otherwise touches it. This can be wrapped into a tool, where you write a plain function and the injector builds the chaining stub for you; it fires all five times. That is a foothold for anything that has to set itself up before a match starts, which is exactly what a custom mechanic needs.
0x260 From scratch
Editing existing fighters was half the goal. The other half, the one that eventually means new characters and new stages, content the game never shipped, is building a module from nothing. A routine produces an empty but valid CRO container, and the checker flags it right away for what it lacks: no nnroControlObject_. No control object. And by now it's established that the loader calls that object five times before it will accept the module.
One can be written in C: a small five-command switch that gets and sets the module's object, and returns some segment boundaries for commands 3 and 4. Compiled into a from-scratch module named "mario" and dropped on the fighter slot, it hard-crashes. This time the debugger won't answer at all, which is what happens when the emulator faults so badly it can't even be closed. Basically a hard crash. The dispatcher was valid, the loader hated it. So it wasn't just checking that the function existed. It was using what the function returned, and the return values were wrong in a way no compiler and no checker could see.
0x2C0 What command 3 really was
The clean way to find out is to take real mario, with all its real functions intact so it still spawns normally, and swap in only the control object, changing one returned value at a time and watching what the loader does with each.
Command 3 returns two pointers, and getting them wrong is fatal. mario's are a tiny range at the very end of its code, only 0x4C bytes long. What lives there is a list of function pointers, nineteen of them, each aimed at one of the module's setup routines. Command 3 hands the loader the start and end of that list: the module's constructors, the setup functions a C++ program runs before anything else, and the loader walks the range and calls every one of them.
It is why that little cleanup table from 0x1A0 fills up as the module loads: the constructors doing the filling are called right here. Return the whole code section as the range and you have told the loader that all of your code is a list of function pointers, so it calls your first four bytes as if they were a function, and dies. Return zero and it calls a null pointer. Return the real, tiny list and the setup runs and the fighter lives. And 0xA35C, the segment size, the value from two bytes back at 0x140, is the far end of that very list. Bumping it had been quietly dragging the end of the constructor list into freshly injected code. The end-of-segment pointers and the control object were the same secret all along.
For a fresh module with no setup code of its own, the fix is almost funny in how small it is: return an empty range, where the start equals the end. The loader walks nothing.
0x320 "CRO mario loaded"
Now the dispatcher can be built properly. The injector is taught a few placeholder names that resolve to a module's own segment boundaries, so a from-scratch control object, written in plain C, can hand back its own segment ends and an empty constructor list:
extern char __cro_seg0_end[]; // the code segment's end
extern char __cro_seg1_end[]; // the constants segment's end
int nnroControlObject_(void* arg, int cmd) {
unsigned int* out = (unsigned int*)arg;
switch (cmd) {
case 0: out[0] = 0; break;
case 1: g_object = arg; break; // set the module object
case 2: out[0] = (unsigned int)g_object; break; // get it back
case 3: out[0] = out[1] = (unsigned int)__cro_seg0_end; break; // empty setup list
case 4: out[0] = out[1] = (unsigned int)__cro_seg1_end; break;
default: return (int)0xD8E12C21; // not supported
}
return 0;
}
A module minted from the empty-container routine with that dispatcher, named "mario," dropped on the fighter slot and started in a match, wedges the debugger again. But this time the place to look is where SaltySD's loader had been writing all along: the emulator's log.
CRO "mario" loaded. The loader accepted a module built from an empty container. It mapped it, filled in its pointers, ran the five-command handshake, walked the empty setup list, and registered its functions. It loaded. The crash a millisecond later is the game trying to spawn this "new Mario" and reading off a pointer that isn't there, because the module is a shell with no actual fighter inside it. That is the line between the two halves of the goal: getting the game to load a from-scratch module is done; making that module a real fighter is the work that comes next. The question of whether the game would load something that was never on the cartridge is answered, in its own log: yes.
0x380 The tooling was the real obstacle
SaltySD's loader had been narrating the answer the whole time. It prints debug logs over everything it redirects. On Azahar, those show up through the logs. GDB was by and large pointless with all the issues it was giving, especially and namely due to the fact that it also required me to power-down after my one hit. SaltySD writes a line to the log every time it loads a module, with the module's name and address, and a line every time the game crashes. One search through that log file is worth more than the one-shot debugger and far more than the GDB test. The best instrument in the entire project was a text file I didn't read until the last day... because of a combination of both Comcast having an outage and a customer at the coffee shop I was working in unplugging my battery-less laptop. Twice. I basically had to go to the Azahar logs to look for the latest progress instead of reading it off of GDB, which turned out to be infinitely smarter to do in the first place. No worrying about timing a singular probe. In retrospect, I probably should have look into why the stub was failing so often, but I digress.
Here is what is now possible for Super Smash Bros. for Nintendo 3DS that wasn't before:
• Custom compiled code inside any of the 189 modules, meaning a fighter that does something no parameter could express, valid and loader-accepted, its calls wired up to the engine's 6,654 functions.
• Arbitrary code that runs the instant a module loads, through the control-object hook, without waiting for the game to call one of your functions first.
• A module built from nothing that the real loader accepts, because the control object it interrogates, down to the setup list.
• And the "real fighter inside" is no longer hypothetical either: mario's entire module — the fighter factory, all four of his weapons, every one of the 46 callback installs, the status bindings, even the pieces Kirby borrows for his copy ability — now exists as first-party C++ source that compiles into a from-scratch CRO against a pinned ABI contract: 37 exports, 106 imports, byte-for-byte structural verification. What it hasn't cleared yet is the gate that actually matters: proving it plays identically to Nintendo's Mario across a full 1.1.7 match, fireballs, F.L.U.D.D., Kirby hats and all.
The last mile is getting the game to load a genuinely new module, one it never asks for by default. Dropping a new module onto an existing slot works today. A true extra character needs one more piece of research, the loader's own load-a-module-by-name call, which a module could make from its own on-load hook..