This post will give a brief introduction for the tools and techniques you need to start reverse engineering and decompiling a N64 Game.
One way to get a hint of the programming languages that a rom was written in (normally either c or c++) is to look for any left over references to the filenames in the rom.
One really nice website that lists this information is: http://shygoo.net/n64-uncompiled/n64-filenames.txt
Very occasionally you can be lucky and see a snippet or two of the original source code left in the rom, you can view a list of these here: http://en64.shoutwiki.com/wiki/N64_C_Repo
N64 Split is a tool initially developed for Super Mario 64 for extracting files out of the n64 rom but it is now general enough to be used for any N64 game.
This is the tool we will be using to start reversing the game of your choice.
You can download N64 Split here: queueRAM/sm64tools: Collection of tools for Super Mario 64 ROM hacking
If someone has already started reversing a game you may be lucky and they could have generated a config file that we can use in N64Split: https://github.com/queueRAM/sm64tools/tree/master/configs
If the game you want to reverse engineer does not yet have a split file config then we can create one using the following template:
# ROM splitter configuration file
name: "Your Game Name (Region)"
# checksums from ROM header offsets 0x10 and 0x14
# used for auto configuration detection
checksum1: 0xA03CF036
checksum2: 0xBCC1C5D2
# base filename used for outputs - [please, no spaces)
basename: "yourGameName.region"
# ranges to split the ROM into
# types:
# asm - MIPS assembly block. Symbol names are in 'labels' list below
# behavior - behavior script
# bin - raw binary, usually data
# header - ROM header block
# instrset - instrument set
# level - level commands
# m64 - M64 music sequence bank
# mio0 - MIO0 compressed data block. may have texture breakdown
# ptr - RAM address or ROM offset pointer
#
# textures types:
# rgba - 16-bit RGBA - [5-5-5-1)
# ia - 16/8/4/1-bit greyscale
# skybox - grid of 32x32 16-bit RGBA
ranges:
# start, end, type, label
- [0x000000, 0x000040, "header", "header"]
- [0x000040, 0x001000, "bin", "boot"]
- [0x001000, 0x0B6A40, "asm", "main", 0x80241800]
# Labels for functions or data memory addresses
# All label addresses are RAM addresses
# Order does not matter
labels:
- [0x80241800, "EntryPoint"]
Save this with a filename in the form: gameName.region.yaml
in the config folder next to n64split executable.
You will notice in the above template that there are 2 checksum addresses:
checksum1: 0xA03CF036
checksum2: 0xBCC1C5D2
These are currently set to the checksum for Mario64 which will be no good for any other game, so lets get some proper values for this!
You will need to change the checksum1 and checksum2 values to the values of your specific rom.
You can find the values by opening the Z64 ROM in a hex editor and going to address 0x10 and copying the hex value at that location. The same for checksum2 at 0x14 in the ROM.
In two sections of the above template we have reference to the hex address 0x80241800
.
One is in the labels section:
labels:
- [0x80241800, "EntryPoint"]
The other is in the main range section:
- [0x001000, 0x0B6A40, "asm", "main", 0x80241800]
This is called the Entry Point of a game and it is start point for the game code in RAM, the value above is for the Super Mario ROM.
So we need to replace this value with the start address of the game you are wanting to reverse.
Again open your ROM in a hex editor and go to address: 0x08 and use the 4 bytes at that address to set the entry point for you rom.
This is much trickier, so while we know that the assembly code starts at 0x001000 we don’t know where it finishes. This will require some disassembling and detective work which is what we will do in the next section so for now keep it at 0x0B6A40
which is where the initial Mario code ends. We will find the proper value for this later on.
If you have put your new config file inside the config folder beside the n64split executable then you should be able to run this:
./n64split GameName.z64
It detects the correct config file using the checksum settings we defined earlier.
If however you would rather keep the config file in a different directory you can directly specify it like so:
./n64split -c gameName.region.yaml GameName.z64
If all goes well the output should be similar to:
root@69b99468cc18:/sm64tools# ./n64split GameName.z64
Using config file: configs/gameName.region.yaml
Splitting into "gameName.region.split" directory
ROM split statistics:
Total decoded section size: B5A80/800000 (8.87%)
This will create a gameName.region.split
directory, where gameName and region are replaced with your specific rom settings. Don’t worry about that low percentage of decoded section we still have a few more ticks which can help increase the percentage decoded quickly.
Lets briefly look at the output of our initial run of n64split, if you open the directory it created it contains a number of different files listed below:
Name | Description |
---|---|
Makefile | |
Makefile.split | |
bin | This is where the binary blobs get extracted to, initially you will have yourGameName.region.000040.boot.bin which contains the boot code common in all roms |
geo | geometry will be extracted here (currently none) |
geo_commands.inc | |
globals.inc | This is where all the labels you defined will be placed allowing compilers/assemblers to refer to them |
levels | Game Levels will be placed here when we tell n64split how to decode the levels |
macros.inc | |
models | 3D Model data will be placed in this folder |
textures | Game textures will be placed here when we tell n64split where in the rom the textures are |
yourGameName.region.ld | |
yourGameName.region.s |
The MakeFile generated by n64 split is a great start but you will notice running make
doesn’t actually manage to rebuild the rom as certain settings will need to be changed first.
While changing these there is also a number of other modifications to do to the makeFile so I tend to just replace the entire makefile with one based on the Sm64 project. This allows us to set it up to compile with the IDO compiler initially and we can easily change to a n64 version of gcc when we find out which compiler is used for this ROM.
Here is the new MakeFile:
TODO: Get the contents of the file
Notice you can now run make
and it will try to create the ROM from the split sections, cool!
Note that currently it will fail with lots of errors when you run make
but this is actually good as we will cover this in the next section as it is giving us hints as to where the assembly code stops and the data section begins!
This will form the basis of your reversing project, soon it will be filled with .c files, 3d models and other data and will all be able to be rebuilt back into the original rom using make
!
However the current content of the folder is not very useful at the moment as we haven’t actually split the rom into any proper areas yet but that is what we will do in the Next section.
So now at the end of this section you will have a folder with files that will eventually become the reversed source code of the game, we just need to tell n64split where the code, models, levels and other assets are in the game. To do that we will need to do some digging to understand the internals of your chosen game rom.
You will notice that running make
will produce tons of assembler errors, well there is a good reason for this, remember when we talked about finding the end of the assembly section and we just kept the value from Mario64?
Well that is whats throwing these errors, it is trying to assemble bytes that are not valid instructions and there for are data.
Ok so lets see what the first error that its throwing as that will give as a hint to say that address is definetly past the end of the assembly section, lets pipe the output of make to a file called output.txt:
make &>output.txt
So if we open this output.txt file and look at the top we get:
yourGameName.region.s: Assembler messages:
yourGameName.region.s:1111: Error: opcode not supported on this processor: vr4300 (mips3) `movn $zero,$zero,$zero'
Now open yourGameName.region.s
and go to line number it says the first error is at (in the example above its 1111).
A quick way to do this on linux with nano install is to run:
nano +1111 yourGameName.region.s
You will notice a line similar to
/* 001F00 80242700 0B */ movn $zero, $zero, $zero
The instructions and addresses will most likely be completely different but what we are interested in is the first part of the comment 001F00
this is the offset where we got an invalid instruction so we know this is not supposed to be asm.
Having one giant .s assembly file is not very manageable and I can (almost) guarantee it wasn’t that way when originally developing the rom. So lets try to split up the file into smaller chunks by looking for hints of file boundaries
inside the assembly.
These can often be a certain number of nops between 2 functions symbolising that each function is in a different file or we can use the rodata for clues as to the file boundary information.
TODO
The tricky part is coming up with names for each of these files based on the functions they contain. Initially I create dummy file names and then rename them later when we can figure out what functions the file contains and name the file something more appropriate like saveGame.c
or drawLevel.c
.
You can use this tool: https://github.com/shygoo/n64sym
n64sym paper_mario.bin -l libs/libgultra_rom.a
So now that you have all the assembly code in seperate .s files we can start decompilation!
The technique we will use is called Linker reinsertion and it requires that your Linker supports scripts (*.ld files), we will be using the gcc mips ld for this task.
The idea of Linker reinstertion is to slowly replace the assembly files with an equivalent C file, function by function, maing sure that the resulting assembly generated by the C code is identical to the original assembly.
So pick an interesting looking .s file to start with and create a .c file with the same name but in the source directory.
Lets say your assembly file was called target.s and located in the asm directory, then just create a target.c file in the src directory.
For example:
src/target.c
asm/target.s
Now pick one function from target.s file and create it in target.c, constantly monitor the assembly output as we want it to match exactly to the original.
Youtube: https://www.youtube.com/watch?v=PC0Tlz6oiN0
youtube - https://www.youtube.com/watch?v=yUX1Vga6amg
When loading Turok .z64 into IDA pro an error window pops up with:
The boot code is not loaded into the database, use manual load to load it