Wiki

RWP Hacking Class #5


Welcome to the fifth RWP Hacking Class. Thank you for coming. This one is a little different. It's just a chat transcript, and you'll have to ask me questions via pm. There may be a q and a session if necessary. With that, class 5 has begun.

In the past, we've done the hacking basics: unknown/known searches, moonjumps, bitflags, etc. Today we delve into advanced hacking, the really powerful stuff. You will need to have Nemu64 working successfully, as well as Renegade64, to do this sort of stuff.

First, we'll discuss about "mips". An n64 game is originally written in C++, then compiled into what we call assembly language. The specific one the N64 uses is MIPS. MIPS is a very simple language, so is very accessible to everyone. For a lot more detailed information, there's plenty of help online, but it's also best to look at books such as See MIPS Run. A good online tutorial is http://chortle.ccsu.edu/AssemblyTutorial/index.html Note it'll take a bit of time to do that one, but you could probably learn mips in a couple days of serious learning. I highly recommend you get a book after this class to learn it properly.

This class is divided into two parts. MIPS learning, and how to use it. The best thing you could do is read the mips book, then skip straight to part 2, since part 1 DOES NOT COVER ENOUGH, although it tries. There's simply too much.

Part 1 - MIPS

The MIPS language is composed of "instructions", which tell the processor what to do, step-by-step. MIPS instructions use different "registers", which can hold temporary values. They are all 32-bits wide, so hold 4 bytes. They are used by most instructions, so for example, if the game needs to update the amount of gunshots in Goldeneye, it will load the value to a register from memory (such as address 80002344), for example to register T0, then subtract 1 in register T0, then write it back to memory (80002344). The N64 has a bunch of registers, called names such as SP, T0, R0, FPR1, PC, etc. We will come back to these later.

There are three basic types of MIPS instructions: I-Type, J-Type, and R-Type
  • R-Type means Register Type
  • I-Type means Immediate Type
  • J-Type means Jump Type

There are a couple types of R-Type instructions, following separate molds. The first mold is INSTRUCTION REGISTERDESTINATIONNAME, REGISTERSOURCE1NAME, REGISTERSOURCE2NAME (example add T0, T1, T2). In laymen's terms, take a register (REGISTERSOURCE1NAME), perform some operation to it (add to REGISTERSOURCE2NAME), then store it into another register (REGISTERDESTINATIONNAME) An example instruction is add T0, T1, T2 The MIPS Add instruction of this pattern mean this: T0 = (T1 + T2) You could do the following: sub T0, T0, T2 This would mean T0 = (T0 - T2). So if T0 was 5 and T2 was 2, it would be evaluated as T0 = (5 - 2) = 3.You could just as easily do sub T0, T2, T0, which means T0 = 2 - 5 = -3. Negative numbers are ok, sometimes!

SLT means set on less than, and it returns either a 00000000 (false) or a 00000001 (true). So if you do SLT AT, T0, T1, if (T0 < T1), then AT = 00000001, if (T0 >= T1), then AT = 00000000 It's useful later.

Here's a list of instructions that fit that mold and perform identically, with the respective operations:
  • ADD/SUB
  • AND/OR/XOR
  • SLL/SRL/SRA
  • SLT
NOTE: You can actually make it so you don't have negative numbers. The normal instructions, with a U at the end, mean unsigned. Later you'll understand the difference between signed and unsigned.

The other primary very important R-Type instructions go to and from memory. This mold is INSTRUCTION REGISTERDESTINATIONNAM OFFSET(REGISTERSOURCE) They can be either bytes (8-bits), half-words (16-bits), or words (32-bits). There are loads, and stores, in this type. Load means take a value from memory to a register, and store takes a value in a register, and place it to memory.

Loads look like: LW T0, 4(T1) Load word (32-bits) at address specified in register T1, add 4 to it, then take that value and put into T0 So if T1 held 80003000, it would take 80003000, add 4, to be 80003004, then load whatever 32-bits were at 80003004. This could be, for example, be 10305070 there, and this is stored in T0. So in the end, T0 = 10305070. LH T0, -4(T2). This is similar to the previous instruction, except it's now only getting 16-bits. So say T2 held 80100002. It would add -4 to that, so 800FFFFE, then take whatever was there, such as FFFFFFFF, and put it into T0. LB T0, 8(T0) This only grabs a byte, but it's tricky, because it loads from T0, and then in the end stores into T0! So lets say T0 initially is 80000000. You add 8 to it, so now 80000008 is the memory address, and lets say 80000000 holds the byte 3A. So in the end T0 now contains 0000003A. These are unsigned versions by adding U to the end of the instruction. You generally want to use unsigned versions, for bytes and half-words. So normally use LW, LH, and LBU. For writes you just simply use SW, SH, and SB.

One final note here is that the register R0 or (also called $zero) always holds the value 0 and cannot be written to. It's useful, so if you want to copy T0 to T1, you can say add T1, T0, R0, which means T1 = T0 + 0 = T0. You could also have used SUB T1, T0, R0, or many others.

[OPTIONAL]
Now since we know the memory operations, lets get back to multiplication and division. Since the N64 only has 32-bit registers, when you multiply or divide, it stores the result in two registers actually. It always stores them to the special registers HI and LO. So you don't specify the register you store it in. If you want to multiply T0 by T1, you just do the instruction multi T0, T1. You know it's going to be stored in the two special registers, HI and LO, implicitly. In multiplication, the result is spread across the HI and LO register. So if you multiply the 10 x 10, the lowest 32-bits are in LO, and the higher 32-bits are in HI. So the instruction mult T0, T0 where T0 = 10 in decimal, yields T0 = 10 x 10 = 100, which in hex is 64, so HI is 00000000 and LO would be 00000064. The result is 0000000000000064 (if you combine the two). Division is similar, where HI stores the quotient result, and LO the remainder. An example instruction is div T0, T1, where T0 = 5, and T1 = 3.
So 5/3, HI would get 1, and LO would get 2
Now, to get the result of the multiplication or division, we need to use the following instructions
mfhi REGISTERNAME
mflo REGISTERNAME
So if you did multiplication, if you want to put the topmost 32-bits to register T0, and the lower-most 32-bits to register T1, you have
  • MFHI T0
  • MFLO T1
[ENDOPTIONAL]

Here's a list of most of the R-type instructions.
  • LW/SW/LH/SH/LB/SB
  • LHU/LBU
  • ADD/SUB
  • AND/OR/XOR
  • SLL/SRL/SRA
  • MULT/DIV/MFHI/MFLO
The next type is I-Type. In the previous register type, we always used registers. You couldn't load a specific value, unless it was in memory, or was zero (using R0). Lets say you want to add 5 to register T0. Now, we can use these immediate types (immediate is a value, such as 5). addi T0, T0, 5. This means T0 = T0 + 5 Generally, you want to use the unsigned versions of everything, but not always.

Another important one is LUI. This means Load Upper Immediate. It's generally done to create your own pointers. LUI T0, XXXX (where XXXX is a half-word 16-bits), means make T0 = XXXX0000. So LUI T0, 8000, means T0 = 80000000. If you want to then make this a memory address, you could do for example,
  • LUI T0, 8010
  • ORI T0, T0, 2340
At the end of those two instructions, T0 = 80102340. You could then combine with our lw instruction so LW T1, 0(T0). (grab the 32-bit value at T0's address, then put in T1, so for example if 80102340 held 12345678, you would now have that in T1. You can addiu to T1: addiu T1, T1, 2, so T1 is 12345678 + 2 = 12345680. Then we can store it back! SW T1, 0(T0). Store the value 12345680 to the address (80102340). Wow, we're getting somewhere!

The last type is J-type. It controls how it knows what instructions to load. The PC register holds the current instruction you are on. So if it held 80004000, it means you're currently on the instruction at memory address 80004000.

You can jump, which means go to a different spot. So if you do j 80005000, it will go to the address 80005000, and then start executing there.
Here's the weird thing about MIPS. It executes the address BELOW this instruction ALWAYS. It's called the delay slot. So:
  • j 80005000
  • addiu T0, T0, 5
What happens is you do T0 = T0 + 5, THEN you jump to 80005000.

You need to be careful, when you read or write code. It always executes the instruction below it. If you don't want it to do nothing, put a nop there. So:
  • j 80005000
  • nop
That just jumps.

The other version is called branch. It moves a specified offset away, so you could branch 4 instructions forward. Remember it also uses the delay slot. It's useful in combination with the SLT (set on less than) from before. You have BNE (branch if not equal), BEQ (branch if equal), and a few others So you could do:
  • SLT AT, T0, T1
  • BEQ AT, R0, 10
  • NOP

So AT = T0 < T1. 00000001 if true, 00000000 if false. Now we check if AT = 0 (R0 always holds 0), if it does, we branch 10 instructions forward. This means that if (T0 >= T1), we branch. Look up again and see why. If BEQ AT, R0, 10 was BNE AT, R0, 10, we would branch 10 instructions forward (execute 10 instructions forward) if (T0 < T1)

Not in any type but very important is the NOP command. It means No-operation (do nothing!). So why would we ever use this!? Well, hackingwise, if you have an instruction, you might want to change it to do nothing. So you "nop it".

Part 2 - Assembly Hacking

We will use Nemu64 for our assembly hacking. Lets figure out how it works. Our primary tools are the ability to breakpoint, which means PAUSE execution, when a condition is met. There are three types: Breakpoint on Execute (BPX) (You give a memory address, and if it ever executes that instruction, it pauses and shows you the instructions and address) Breakpoint on Write (if the memory address is written to, it pauses execution and shows you the instruction and address). Breakpoint on Read (if the memory address is read, it pauses execution and shows you the instruction and address).

The advantage is that when you breakpoint, it tells you what what instruction caused the break, and you can look at the instruction, as well as having the ability to step through the instructions that follow. So if you breakpoint write on the memory address in Goldeneye where it holds your bullets, if you shoot a bullet, it will try and write the new bullet value to that address. Since it tried to write to that, Nemu64 breakpoints, and shows you that address, and you can step through it and look at the instructions. So lets take a look at how that works. Open up Nemu64 and load a Banjo Kazooie ROM. We're going to take a look at infinite lives in assembly.

Load this save state:
http://www.rarewitchproject.com/medi.../banjosave.nss
If you have problems getting to a level in BK in Nemu, use the warp mod from RWP's codes.

Now, for us, we know infinite health is located at: 80385F8B If you didn't know the code for infinite lives, you'd need to hack it first. Before you do any assembly hacking, you need to do regular to hacking to find an address that will help you. So we know infinite health in the regular gs code way. Lets make an assembly code version. So knowing programming, we have a variable for lives. If you want to lower your life count, you need to write to this variable.

So lets do a breakpoint write. Pause Nemu64, then Plugins -> Debugger Memory. Go to 80385F8B by typing it in. You should see your current life count here (starts at 03). On the top right you can see "Watch type on set". Click Write, and uncheck Read. To do a breakpoint on read (if the variable is read), have Read checked. You can do both read/write at the same time. So just choose Write, then right click on the first memory location, 80385F88, and it should turn purple. Go back to Banjo, and jump into the quicksand to try and kill Banjo. Whoa, what just happened? Some window just popped up called Commands. It looks like that variable was written to. He's not dead, but it seems they use the variable in multiple ways. OK, so it broke at 80346160, you can see this says SW V0, 0x0058(T0). If you hover over the instruction, you can see the value of V0 and T0. This instruction means its store V0 (currently 03) to our address (T0 = 80385F30 + 0x58 = 80385F88, nemu does this conversion for us in memory address!)

OK, this isn't really what we want right? We want it to break after we die. So lets nop this address (80346160). That means we make it 00000000, mean execute nothing. This way when it gets to this instruction, it won't do anything, and it'll continue on so we can find the right spot. Note sometimes this can crash your game, so sometimes you can't just do this. OK in the memory editor, go to 80346160. Click on the instruction AD020058, and type 0, then hit enter. OK, now go back to 80385F88, and right click on the 00000003 twice, so it reenables the breakpoint.

Now kill Banjo, it shouldn't break at this spot now, and we can finally find what we want. When he respawns, we get another break. This is probably the one we want. It's at 803460C0 and says SW T2, 0(V1). Means store whats held in T2 (02 now, one live less), and to our spot, 80385F88. If you look a little above our instruction, at 803460B4 you see it load the value into T2 (our new lives amount). So we're going to modify this instruction, so we give ourselves a different amount.

Lets say we want Banjo to always have 10 lives. So we want to make T2 have 10 lives in that instrucvtion (803460B4), so it saves the value, it will be 10. Lets open up Renegade64 so we can make our own modifications, and try them here. In Renegade64, ASM -> Assemble Codes. On the left side, you type in instructions, and on the right side, you click Assembly and it makes the hex.

Now, to always make a value something, there are a couple ways, but one easy way is: (Renegade uses hex, 10 = A). ADDIU T2, R0, A. Now click assembly, and we get our instruction, 240A000A.OK, reload our save state, and then in the memory editor of Nemu64, go to 803460B4, and enter in 240A000A. You can disable our breakpoint while you're there too. Try and kill Banjo.Well, that's kind of strange, Banjo doesn't have 10 lives, but he is invincible. It's probably because this routine lowers the number by 1, so if you move up to 10, it's messed up. Well, didn't quite work as we had hoped.This happens often. This is just an intro to ASM hacking. Next time we'll talk about branching and jumping, so you can actually add code to something, instead of just editing one line like we did here.
%s1 / %s2