~/labs/workshops/gba-firered/05-analise-dinamica
Dynamic analysis: finding HPFind HP in RAM with mGBA and GDB, and find the instruction that applies damage.
Dynamic analysis means studying the game while it runs. We pause the game, read memory, observe what changes, and discover which instruction touched each value.
The goal of this chapter is concrete: find where a Pokemon’s HP lives during battle, and discover the exact instruction that applies damage. This is the prerequisite for the patch in chapter 07.
The root RAM.md has this hunt written in detail, with all dumps. Here is the
compact script to follow in class.
The central idea
Battle state lives in EWRAM, which starts at 0x02000000. Somewhere in it is your Pokemon’s HP. You do not know the address, but you know the value: the number shown on screen.
The technique is difference search. You snapshot memory with one value, change the value in the game, snapshot again, and compare both snapshots. The right address is one of the few that changed exactly from one value to the other.
HP on screen = 24. Snapshot EWRAM.
Take damage. HP on screen = 21. Snapshot again.
Search for who was 24 and became 21.
Step 1. Enter battle and read HP
Load the save, walk in the grass, and enter a battle. Write down your Pokemon’s current HP. In the project example, Charmander has 24.
Step 2. Snapshot EWRAM
Connect GDB to mGBA, as chapter 03 showed. With the game paused, save all EWRAM to a file:
dump memory ewram_hp24.bin 0x02000000 0x02040000
This writes from 0x02000000 to 0x0203ffff, the entire EWRAM, into
ewram_hp24.bin.
Step 3. Take damage and snapshot again
Let the enemy attack once. Your Pokemon’s HP drops. In the example, it goes from 24 to 21. Pause the game again and take the second snapshot:
dump memory ewram_hp21.bin 0x02000000 0x02040000
Now you have two files: EWRAM with HP 24 and with HP 21.
Step 4. Compare the two snapshots
The script ram_find.py performs the comparison. By default it already searches
for the change from 24 to 21, testing the three possible value sizes:
python3 ram_find.py
Output:
[u8] 24 -> 21
0x2023c0c
0x20242da
hits: 2
[u16 little-endian] 24 -> 21
0x2023c0c
0x20242da
hits: 2
[u32 little-endian] 24 -> 21
hits: 0
The entire memory became two candidates: 0x02023c0c and 0x020242da. The value does not appear as u32, so it is 8 or 16 bits. Keep both addresses.
Step 5. Separate real HP from screen HP
Two candidates, but only one is the real HP used by battle logic. The other is a copy used only to draw the bar on screen. To discover which is which, force each one back to 24 and see what happens:
set *(unsigned short*)0x020242da = 24
If changing 0x020242da changes only the number on screen, but the game still treats the Pokemon as wounded, then this is visual HP, not logical HP. That is what happened in the project example. The other address, 0x02023c0c, remains as the real HP candidate.
The definitive proof comes next.
Step 6. The watchpoint
A watchpoint tells GDB to stop the game the instant any code writes to an address. Place one on the real HP candidate:
watch *(unsigned short*)0x02023c0c
c
The c continues the game. Now let the enemy attack again. The moment the game
writes the new HP, GDB stops by itself, paused exactly at the instruction that did
the write. Read the registers:
info reg pc lr r0 r1 r2 r3
In the example, pc stopped near 0x0801f8b4. Look at the instructions around
pc:
x/12i $pc-12
And the damage-applying sequence appears. Read it slowly, because it is the heart of the patch that comes later:
0801f8aa: ldrh r0, [r2, #0x28] current HP
0801f8ac: ldr r1, [r7] damage
0801f8ae: cmp r0, r1
0801f8b0: ble 0x0801f8d0 if HP <= damage, faint
0801f8b2: subs r0, r0, r1 HP = HP - damage
0801f8b4: strh r0, [r2, #0x28] writes the new HP
The instruction strh r0, [r2, #0x28] writes HP. The watchpoint found it for you.
This is the central finding of the day.
Step 7. Discover the structure
The registers at the stop point reveal the whole structure. In the example:
r2 = 0x02023be4 base of the in-battle Pokemon
The instruction writes to [r2, #0x28], meaning r2 plus 0x28. Since the watchpoint
hit 0x02023c0c, the math closes:
r2 + 0x28 = 0x02023c0c
r2 = 0x02023c0c - 0x28 = 0x02023be4
And the instructions right above show how the game reaches that r2:
movs r3, #0x58 each Pokemon takes 0x58 bytes
muls r0, r3, r0 r0 = index * 0x58
adds r2, r0, r4 r2 = base + index * 0x58
This is an array of structs. Put it all together and you have the data model:
array base = 0x02023be4
slot size = 0x58 bytes
HP offset = 0x28, type u16
HP of Pokemon index N = 0x02023be4 + N * 0x58 + 0x28
Your Pokemon is index 0:
0x02023be4 + 0 * 0x58 + 0x28 = 0x02023c0c
The enemy is probably index 1:
0x02023be4 + 1 * 0x58 + 0x28 = 0x02023c64
Step 8. Prove the enemy hypothesis
Test the enemy address directly. Force its HP to 1:
set *(unsigned short*)0x02023c64 = 1
Return to the game and use any move. If the enemy faints, you confirmed the address, slot size, and HP offset all at once.
Step 9. Reconstruct the struct
With the base, size, and memory dump, you can map almost every field. Comparing RAM
values with the Pokemon summary screen (species, attack, defense, HP, level),
RAM.md arrives at this structure:
struct BattlePokemon {
/*0x00*/ u16 species;
/*0x02*/ u16 attack;
/*0x04*/ u16 defense;
/*0x06*/ u16 speed;
/*0x08*/ u16 spAttack;
/*0x0A*/ u16 spDefense;
/*0x0C*/ u16 moves[4];
/*0x28*/ u16 hp;
/*0x2A*/ u8 level;
/*0x2C*/ u16 maxHP;
/*0x2E*/ u16 item;
// ... remaining fields
};
You do not need to memorize the struct. What matters is the method: the on-screen value becomes a number, the number becomes an address, the address becomes an instruction, and the instruction reveals the shape of the data.
What to take from this chapter
- Game state lives in EWRAM (0x02000000).
- Difference search: snapshot, change the value, snapshot, compare.
- ram_find.py turns the whole RAM into a few candidates.
- One candidate is logical HP, the other is the screen copy.
- A watchpoint finds the exact instruction that writes the value.
- The registers at the stop point reveal the struct base, size, and offset.