Skip to content
tolmar edited this page Dec 31, 2023 · 4 revisions

This is just a quick crash course, you'll probably need to cross-reference other assembly tutorials on the internet if you're going in from scratch.

What is assembly?

The SNES CPU reads a byte of data, interprets it as a command, possibly reads a few more bytes to clarify the command, does whatever that command is, and repeats with the next byte. This is more or less how all computers worked until the mid 90s (now they jump through a lot of hoops to pretend they work this way). Assembly (often shortened to asm) is a simple programming language that gives a short name to each command byte, so you can say "JMP $C41b56" (jump to the code at that address) instead of manually writing the bytes "5C 56 1B C4". Different CPUs have different assembly languages, normally named after the CPU. The SNES CPU is a MOS 65816, so we call its assembly language 65816.

How do I modify it in Earthbound?

Coil Snake doesn't support assembly hacking, but it does support entering raw bytes. H.S. has already went through the trouble of writing a whole bunch of commands that translate assembly-like syntax to raw bytes. That's asm65816.ccs, which you can find in the CCScript Library. This tutorial will assume you're using that.

To write an ASM patch, do:

ROM[0xAddressInHex] = {
  bunch of code
}

This will overwrite whatever address you specify with your assembly. Note that assembly takes up space, so your patch needs to take up the same space as whatever you're overwriting. Strategies for dealing with this can be found later in the tutorial.

You can insert new code with:

MyNewCode: {
  bunch of code
}

This will insert your code in an arbitrary bit of free space. Your code won't be run by default. You'll have to have your patched code jump to your inserted code.

You can find addresses using Earthbound ROM Explorer. There is also a better commented and labeled version at ebsrc, but unfortunately it doesn't include addresses. The best strategy is usually to find what you want to modify in ebsrc, then look up the function in ROM Explorer and find the actual address. It's a little annoying, but it's what we have.

What are the commands?

You'll want an opcode reference. Different people prefer different references, you might want to search for others and see if you like one better. The one that linked is just Tolmar's favorite, since he wrote this tutorial.

Any opcode reference is going to be slightly different from the syntax in asm65816.ccs, since it's not a real assembler. It uses suffixes like _d and _i for different addressing modes. If you're ever in doubt about how to translate an assembly command, look at the byte code in asm65816.ccs's source and your reference, and find the ones that match.

SNES internals

  • You have three registers (16-bit variables) you can use as you please: A, X, and Y. Most math commands only have versions that operate on A. Indexing commands only have versions that use X and Y, or one or the other. There are other registers for special purposes, which you only occasionally need to care about.

  • There are three useful processor flags, which are automatically set by certain commands. The important ones are C (carry, means the last math operation overflowed (roughly)) and Z (zero, means the last math or load operation resulted in 0) and N (negative, means the last load/math operation had a result that could be interpreted as negative). There are other ones that are occasionally useful.

  • The SNES has a stack. This is a place where you can save values and get them back out in the opposite of the order you saved them (and only that order). For the sake of this beginner tutorial, don't mess with the stack except where told to. Due to the way EB is structured, you only need it for one thing.

The usual commands

  • Load (LDA, LDX, LDY) commands copy a value into a register.

  • Store (STA, STX, STY) commands copy a value out of a register.

  • There are transfer commands between the registers (TAX, TXA, TAY, TYA, TXY, TYX).

  • JMP changes the next command that will be run to the provided one.

  • ADC adds a value to A and stores it in A. It'll add one more if the carry flag is set. CLC clears the carry flag, you should use it before ADC if you don't know what the carry is set to. ADC sets the carry flag if the number overflows (the idea is that you could chain multiple ADCs together to add a big number, but this is rarely actually used).

  • SBC subtracts a value from A and stores it in A. It'll subtract one more if the carry flag is clear. SEC sets the carry flag, you should use it before SBC if you don't know what the carry is set to. SBC is interesting in that it sets a lot of useful flags - Z if the numbers were the same, C if A was the same or bigger than the number, and N if the number was bigger than A.

  • CMP sets the flags the same way SBC does, but doesn't modify A.

  • Branches are like JMP, but only jump if a specific flag is set/unset (otherwise they go to the next line). BEQ branches if Z is set (branch if equal), BNE sets if Z is unset (branch not equal), BCS branches if C is set (branch carry set), BCC branches if C is unset (branch carry clear), BMI branches if N is set (branch if minus), and BPL branches if N is unset (branch if plus). Unfortunately, the second byte of these is "how many bytes to jump" instead of "where do I jump to" so you have to manually count how many bytes to move (a real assembler would do the counting for you, but we don't have one). asm65816.ccs cleverly gets around this with commands like BEQ_a(address), but they're 5 bytes long, where a real BEQ is only 2.

  • JSL jumps to a specific place, like JMP, but it also saves your current code location as three bytes on the stack. It's designed to be paired with an RTL.

  • RTL pulls three bytes from the stack and jumps to that location. It's designed to be paired with a JSL.

  • JSR and RTS are like JSL/RTL, but they only use two bytes, and can only jump within a single bank. You can safely never use them. But a lot of Earthbound's functions use RTS, which is a problem if you want to call them, because you're probably not in the same bank. You can use jsl_rts.ccs to get around this, but it's still kind of annoying (really confuses debuggers).

  • NOP does nothing, and takes up only one byte. This is incredibly useful when you're trying to fit patches into places they weren't meant to be.

Addressing Modes

I'll write these all using CMD (not a real command), but they apply equally for LDA, STA, AND, etc. Some command/addressing mode combinations don't exist though. The first example of how it's written is how asm65816.ccs, the second is the normal way you'll see in most opcode references and debuggers.

  • Immediate. Written CMD_i(num) or CMD #$FourDigitNumInHex. Literally the number you provided. LDA_i(12) loads 12 into A.

  • Absolute. Written CMD_a(address) or CMD $FourDigitNumInHex. Loads an address from memory, specifically 7E0000 through 7EFFFF. Technically it can refer to other addresses, but in Earthbound it's stuff in 7E everywhere except really deep in the game's internals.

  • Dynamic page. Written CMD_d(num) or CMD $TwoDigitNumInHex. Uses a numbered local variable in memory. Technically it adds your number to the DP register and looks up that place in memory, but the Earthbound calling convention turns it into local variables.

Earthbound calling conventions

Most functions (things you JSL into) in Earthbound follow this format:

  // Reset process bits. Harmless command, rarely actually matters, but EB usually does it.
  REP (0x31)
  // Push Dynamic Page to stack. This saves what set of variables _d currently refers to.
  PHD
  // Subtract X from Dynamic Page.
  // X is the number of bytes you want to allocate for local variables. For various reasons, it should be an even number and at least 20 (0x12).
  TDC
  ADC_i(-X) // Yeah, negative X. Not sure why they didn't use SBC.
  TCD
  // YOUR CODE HERE
  // YOUR CODE HERE
  // YOUR CODE HERE
  // restore the set of _d variables we saved, so whoever JSL'd here has the right ones
  PLD 
  RTL

This effectively turns the Dynamic Page register into a second stack for allocating local variables. This is why you don't need to use the real stack in EB (except to set up this very trick). It's a very nice trick, use it! You almost never need to worry about where to stash your values this way.

Earthbound's functions mostly use A as their first argument, X as the second, and Y as the third. If they need more arguments, these tend to go in 0x0E and 0x10. Those last ones are your "local variables," but it's just a stack, so a function you call can read them by reading outside of its stack space.

How do I fit my patch?

The trickiest thing about assembly hacking is often how you fit your shiny new code in. There's plenty of space in the ROM overall, but you can't move things around much without breaking things, so there might not be space exactly where you want it.

Here are the standard techniques:

  • Monkey Jump: Find a JSL near where you want your code to be. Replace it with a JSL to code you inserted, and then add the original JSL to the start of your code (before your function header). Nice and reliable, even though no human would ever write code that works this way. A slightly more advanced version - you can replace any four-byte control code with one of these.
  • Springboard: If you don't have room to monkey jump, you can use the same trick with a JSR (3 bytes, you have to find a place to jump to in the same bank) or a BRA (2 bytes, have to have free space within 128 bytes, extremely annoying to work with). You can put your new code wherever you sent this, or just JSL from there.
  • Rewrite: Just optimize whatever code you're working on until there's enough room for what you want to do, or at least enough room to do a JSL to your code. In Earthbound, look for reads and writes to $06 and $08; whatever code generator they used loves to write values to these then copy them to where it actually wants the values. You can just write the values where they're supposed to go in the first place.

There are also some advanced techniques that involve stack manipulation. You can use stack operations like PEA and PLA to change where an RTL will go to, even the next RTL after the end of your function returns. These are hard to set up and reason about, so it's best to use one of the above techniques whenever possible. But if you're really stuck, know you can JSL into a function that returns somewhere other than the JSL.

Why did my patch break?

I dunno lol. Step through it in a debugger. bsnes-plus and Mesen-S have debugging modes. Set a breakpoint on the code you patched over, then follow it until you get to your change.

  • If things exploded horribly: Look at the end of your patch and see if the code after your patch is the same as it was beforehand. Because SNES instructions take a variable number of bytes, it's possible for it to end up misaligned and end up halfway into a command, reading a bunch of garbage until it falls over. Your patch needs to be exactly same size as the code you replaced.

  • Things exploded horribly after an RTS: Did you JSL into a function that returns with RTS? That doesn't work. TODO: Get Catador to put JSL_with_RTS into the CCScript library so I can link it here.

  • If things just didn't do what you want: Watch the values of the registers as you step through your patch, and make sure they do what you think they should be doing. You may have to go through several times for it to sink in.

Clone this wiki locally