N64 Code Injection

From en64 wiki
Jump to navigation Jump to search

rominsert is discussed here, but utilizes very rudimentary methodology which isn't comparable to the one below.


This document will cover the theory and methods behind cleanly inserting code into an N64 binary. As with all arts, you can preform many different methods to produce the same, or identical outcomes. This method is considered a bit "bloated" to some, but nonetheless is good to post here in an attempt to inform the masses!
Examples:

  • An example of this with Mario Kart 64! The names "Stage 1" and "Stage 2" are named in accordance with the below method. The modification displays your current x/y/z coordinates while racing.
  • Yaz0 compression (to) -> LZO/RC4 LZO compression + RC4 encryption. The primary goal with this was to keep out thieving scrubs from taking modifications and claiming it as their own. Moreover, RC4 being a simple encryption algorithm (in order to not sacrifice speed), it can easily be cracked by someone who knows what they're doing. As stated though, it's to keep out noobs. (Kind of like what LunarMagic does)

Prerequisites

The following is a list of things you should know before you set about using this procedure. These values will vary depending on what game you're working with.

  • Where in the ROM you want to store the data to be loaded.
  • A RAM address where the data can be loaded, preferably a large one. I use 0x80400000 on games which do not utilize the expansion pak.
  • An initial stack pointer. Currently, since I have thus far only used the binary loader on games which only utilize 4MB of RAM, I have this set to 0x807FFFF0. The boot loader will usually not load more than 2MB of the game's code to the RAM at the time our binary is initially being run, so the stack pointer is not going to conflict with something else.
  • Not mandatory to get your code to run at startup, but you'll need to disable any code in the game which might overwrite your memory or overwrite registers ($gp). You will also need to set up a hook.

Loading Stages

  • Stage 1: This is the code that replaces the game's entry point (usually at 0x80000400, but it works at others). All it does is copy data from the ROM to the area of RAM that you specified. It then jumps to aforementioned area of RAM.

    * In practice, the stage 1 binary needs to know about: stack pointer, ROM source, RAM target, and the size of stage 2 binary.

  • Stage 2: This stage replaces our modified entry point with the preserved original one. It initializes the $gp, and also clears the BSS section of your binary (take note: your binary is linked in with stage2). After the BSS is cleared and the entry point is restored, your binary's main (n64main) is invoked.

    * In practice, the stage 2 binary needs: the original entry point data, and the length of said data.

  • Your binary: Not officially a part of the process, but during the initial run of your binary you should be sure to disable code in the parent game that disables/overwrites or otherwise cripples your code. Things to look out for: memory clearing, $gp overwriting (the $gp is usually overwritten by the exception handlers). You will also need to set up a hook if you wish your binary to be called more times (see hook.S and main.c).

The Process

  • Build your binary. There is no linking done, but in my process I bundle the output files into an archive: mips-ar -rcs -o bin.a file1.o file2.o and so on.
  • Copy the original entry point data (at 0x1000 in the ROM) to a file named "game-ep.c" in the stage2 directory. The program "f2c" in the distribution can help you accomplish this. How much you need depends on the size of the final stage 1 binary. You don't need more than a few hundred bytes. Just to be safe I copy 1K.
  • Build stage two. Make sure that your bin.a gets linked in with the final product. Any symbols from the game that you were using have to be put in stage 2's conf.ld.
  • Generate a conf.h for stage 1. Constants it needs: BL_STACK_TMP, BL_ROM_START, BL_ROM_END, BL_RAM_START which are, respectively: the temporary stack pointer, the address in ROM the data will be injected at, the end address in ROM of your data, and where in RAM you want the data to be copied to.
  • Build stage one.
  • Copy the stages to their respective locations: stage 1 @ 0x1000 in ROM, stage 2 @ the predetermined ROM target address.
  • Fix the ROM CRC.

The Script

Keeping the above in mind, here is a pastebin of the script I'm using at the time of writing. It's also included in the download. The script utilizes a few utility programs which I couldn't find (or figure out how to use the core GNU utilities) which I'd like to detail (they are all in the distribution):

  • f2c: "file-2-c" - f2c [symbol name]

This program reads from standard input and converts the input binary data to a C source file. It takes a symbol name as the first argument. After the array of data, a const int [symbol point]_len is added, which is initialized to the size of the array.

  • putbin: "put binary" - putbin [binary file] [target file] [address (hex)]

I tried using dd for this - and you probably can - but I couldn't figure it out, so I wrote a tiny PHP script to do it for me. All it does is copy the binary file into the target file at the specified address, and nothing else.

Distribution

You can download the source code to all the stages, an example binary, and the script itself right here. You will also need to download this and extract it into your N64 toolchain prefix. It includes newlib's libraries/headers, and my custom linker script (which supports extra configuration by way of conf.ld in your project directory). The script assumes the following about your setup (newlib can be in the hyperlink below, but I don't have the linker script used here...):

  • You have binutils/GCC installed targeting MIPS. It assumes the prefix of the binaries to be "mips-". If you need to build these two, do: ./configure --target=mips.
  • You have newlib built targeting MIPS. I use the following configuration options: ./configure --target=mips --prefix=$INSTALL_PATH --disable-multilib --disable-threads --disable-shared.
  • NOTE: when I say targeting MIPS, I mean targeting 32-bit MIPS, not 64-bit! So to those of you who I told to build GCC/binutils with the --target=mips64 option, I apologize profusely.
  • That you have ucon64 (the CLI utility) installed on your system (and, of course, that it is in your PATH).


The program in the "bin" folder will draw a semi-transparent blank rectangle across the screen in Mario 64 (NTSC) if you hold the "L" button. Happy hacking.

Debugging

  • If your game provides an easy interface for printing text to the screen, you can always use that, but in most cases -
  • You may just have to open Nemu's memory viewer and inspect values manually. Use `nm` (or in the toolchain: `mips[64]-nm`) to find the addresses of your symbols. The default action of nm is to sort by name, so I recommend using this flag in addition: --numeric-sort.
  • In the above case, you can get "text output" by doing something like this:
void
myDebugFunc ( void )
{
    static char buff[512];
    snprintf( buff, sizeof(buff), "Internal variables %i, %i, %s, 0x%08X", ...data... );
}
  • You would then look for the "buff" symbol's address in the `nm` output and find that in Nemu's memory viewer.



Taken from the now defunct 64.vg.
The dead hyperlinks can be found at either one of the two locations. I've yet to sift through any of the downloads, much less view them, but zzt told me they're at one of those Google code pages.