← cd ../

~/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.