~/labs/workshops/gba-firered/07-primeiro-patch
The first patchWrite, assemble, and install a hook that makes the enemy faint in one hit.
Now we put everything together. You already know how to read ARM and Thumb, found the instruction that applies damage, and understood the in-battle Pokemon struct. This chapter uses that to change the game’s behavior for real.
The patch goal:
The opponent's Pokemon faints from any hit.
Your Pokemon does not lose HP.
The attack point
In chapter 05, the watchpoint brought us to this Thumb block, inside the routine that updates HP in battle:
0801f8aa ldrh r0, [r2, #0x28] r0 = current HP
0801f8ac ldr r1, [r7] r1 = damage
0801f8ae cmp r0, r1
0801f8b0 ble 0x0801f8d0 if HP <= damage, go to fainting
0801f8b2 subs r0, r0, r1 HP = HP - damage
0801f8b4 strh r0, [r2, #0x28] writes the new HP
The naive idea would be to replace ldr r1, [r7] with something that puts huge
damage in r1. But that has two problems. First: this same routine runs for both
your Pokemon and the opponent, so huge damage would kill both. Second: a Thumb
instruction has only 2 bytes, and there is no room there for logic that decides
who is who.
The solution is a hook.
What a hook is
A hook replaces some original code instructions with a jump to new code, written by you, in a free area. That new code does what it needs to do and then returns to the original flow. The classic name for this is a trampoline.
original code at 0x0801F8AC
jumps to our code in a new area
our code decides: is the target player or opponent?
sets damage accordingly
returns to the original flow
To get a new area, we increase the ROM size. The original ROM is 16 MiB, from 0x08000000 to 0x08ffffff. The project build expands it to 32 MiB and places the new code at the first free address:
original ROM 0x08000000 to 0x08ffffff (16 MiB)
new area starting at 0x09000000 (where our code lives)
The rule for who is who
Pokemon in battle are numbered. The number, called battler, says which side they are on:
0 = player, left position
1 = opponent, left position
2 = player, right position
3 = opponent, right position
Notice the pattern: even number means player, odd number means opponent. To know the side, just inspect bit 0 of the number:
bit 0 = 0 -> even -> player
bit 0 = 1 -> odd -> opponent
The patch logic, in pseudo C, is:
battler = *gActiveBattler;
if (battler is odd) // opponent
damage = hp; // damage equals HP, so it faints
else // player
damage = 0; // no damage
if (hp > damage)
hp = hp - damage;
else
hp = 0;
Writing the Assembly
The hook code is in patches/enemy_one_hit.s, commented line by line. Its core is
this Thumb code:
enemy_one_hit:
ldrb r1, [r5] @ r1 = battler index
lsls r1, r1, #31 @ move bit 0 into the sign bit
bpl player_target @ if result is not negative, it is even (player)
@ opponent target: damage = hp
movs r1, r0
b compare_hp
player_target:
@ player target: damage = 0
movs r1, #0
compare_hp:
cmp r0, r1
bgt survives
ldr r1, =0x0801F8D1 @ fainting path, return to original
bx r1
survives:
subs r0, r0, r1
ldr r3, =0x0801F8B5 @ return to the original strh
bx r3
It is worth stopping on two tricks, because they show real Thumb details.
The first is lsls r1, r1, #31. Basic Thumb does not have an instruction to test
a bit with an immediate value. So the code shifts the number 31 positions left,
which moves bit 0 into the sign position. If bit 0 was 1, the result becomes
negative, and bpl, meaning branch if plus, does not branch. It is a compact way
to ask “is this number odd?”.
The second is the return addresses ending in 1:
0x0801F8D1 is the real fainting path at 0x0801F8D0, with the Thumb bit set
0x0801F8B5 is the original strh at 0x0801F8B4, with the Thumb bit set
That +1 is the Thumb bit explained in chapter 02. It keeps the CPU in Thumb mode
after the return jump. Without it, the CPU would try to execute ARM and crash.
Assembling the Assembly
Assembly text does not run. It must become machine bytes. The ARM toolchain assembler does that:
sudo apt install -y gcc-arm-none-eabi
Assembly has two steps. The first turns the .s file into an object. The second
extracts only the code section bytes into a raw binary:
arm-none-eabi-as -mcpu=arm7tdmi -mthumb -o build/enemy_one_hit.o patches/enemy_one_hit.s
arm-none-eabi-objcopy -O binary -j .text build/enemy_one_hit.o build/enemy_one_hit.bin
-mthumb is important: it assembles in Thumb, which is the mode used by that game
block.
The jump that replaces the original
In place of the original instructions at 0x0801F8AC, we write a long jump. In Thumb it looks like this:
ldr r1, [pc, #0] @ loads the destination address stored just below
bx r1 @ jumps there
.word 0x09000001 @ destination: 0x09000000 with the Thumb bit set
In bytes, this is eight bytes. It occupies exactly the space of the four original
instructions (ldr, cmp, ble, subs), which are therefore reproduced inside
our code.
Installing with the project build
You do not need to perform every step by hand. The script
tools/build_enemy_one_hit.py automates everything and also checks that nothing
moved:
python3 tools/build_enemy_one_hit.py
It does, in order:
1. Checks the SHA1 of the original ROM. If it is not the right ROM, it stops.
2. Checks that the bytes at 0x0801F8AC are still the originals
(39 68 88 42 0e dd 40 1a). This prevents patching the wrong place.
3. Assembles patches/enemy_one_hit.s.
4. Expands the ROM from 16 to 32 MiB.
5. Writes the new code at 0x09000000.
6. Writes the long jump at 0x0801F8AC.
7. Saves rom/PokemonFireRed_enemy_one_hit.gba.
The output shows the summary:
New code: 0x09000000 (... bytes)
Hook: 0x0801f8ac -> 0x09000000
Expanded ROM: 32 MiB
Output: rom/PokemonFireRed_enemy_one_hit.gba
The original-byte check in step 2 is a lesson worth repeating for any patch: before writing over code, confirm the target is really what you think it is.
From address to file offset
When you edit the ROM on disk, the file offset matters, not the memory address. The conversion is the one from chapter 01:
file offset = address - 0x08000000
So the hook at 0x0801F8AC is at file offset 0x001F8AC, and the new code at 0x09000000 is at offset 0x01000000. The script does this conversion by itself, but it is good to know where it comes from.
Testing
Open the patched ROM in mGBA:
rom/PokemonFireRed_enemy_one_hit.gba
Load the save, enter battle, and attack. The opponent should faint from the first hit, and your Pokemon should not lose HP even when hit.
How to restore normal player damage
The Assembly file already documents this. To make your Pokemon receive normal
damage again, change this line in the player_target label:
movs r1, #0 @ zero damage
to:
ldr r1, [r7] @ original damage
And assemble again. This is the kind of small adjustment that shows the class that the patch is just code, and code can be edited.
What to take from this chapter, and from the workshop
- A hook replaces original instructions with a jump to your code.
- Expanding the ROM gives free space for new code (starting at 0x09000000).
- The patch decides the target from battler bit 0: even is player, odd is opponent.
- Thumb return addresses end in 1 (the Thumb bit).
- Always check original bytes before writing over them.
- file offset = address - 0x08000000.
The full path you followed:
GBA architecture
-> reading ARM and Thumb
-> importing and mapping in Ghidra (static)
-> finding HP in RAM with mGBA and GDB (dynamic)
-> writing, assembling, and installing a patch
That is the game reverse engineering cycle. The target changes, the tools change a little, but the method is always this.