← cd ../

~/labs/workshops/gba-firered/05-analise-dinamica

Analise dinamica: achando o HPAchar o HP na RAM com mGBA e GDB e a instrucao que aplica dano.

Análise dinâmica é estudar o jogo enquanto ele roda. A gente para o jogo, lê a memória, observa o que muda e descobre qual instrução mexeu em cada valor.

O objetivo deste capítulo é concreto: achar onde mora o HP de um Pokémon durante a batalha, e descobrir a instrução exata que aplica o dano. Esse é o pré-requisito do patch do capítulo 07.

O RAM.md na raiz tem essa caçada escrita em detalhe, com todos os dumps. Aqui está o roteiro enxuto para seguir na aula.

A ideia central

O estado da batalha mora na EWRAM, que começa em 0x02000000. Em algum lugar dela está o HP do seu Pokémon. Você não sabe o endereço, mas sabe o valor: o número que aparece na tela.

A técnica é a busca por diferença. Você fotografa a memória com um valor, muda o valor no jogo, fotografa de novo, e compara as duas fotos. O endereço certo é um dos poucos que mudaram exatamente daquele valor para o outro.

HP na tela = 24. Tire uma foto da EWRAM.
Leve dano. HP na tela = 21. Tire outra foto.
Procure quem era 24 e virou 21.

Passo 1. Entrar na batalha e ler o HP

Carregue o save, ande na grama e entre em uma batalha. Anote o HP atual do seu Pokémon. No exemplo do projeto, o Charmander está com 24.

Passo 2. Fotografar a EWRAM

Conecte o GDB ao mGBA, como o capítulo 03 mostrou. Com o jogo parado, salve a EWRAM inteira em um arquivo:

dump memory ewram_hp24.bin 0x02000000 0x02040000

Isso grava de 0x02000000 a 0x0203ffff, que é a EWRAM toda, no arquivo ewram_hp24.bin.

Passo 3. Levar dano e fotografar de novo

Deixe o inimigo atacar uma vez. O HP do seu Pokémon cai. No exemplo, vai de 24 para 21. Pare o jogo de novo e tire a segunda foto:

dump memory ewram_hp21.bin 0x02000000 0x02040000

Agora você tem dois arquivos: a EWRAM com HP 24 e com HP 21.

Passo 4. Comparar as duas fotos

O script ram_find.py faz a comparação. Por padrão ele já procura a mudança de 24 para 21, testando os três tamanhos possíveis do valor:

python3 ram_find.py

Saída:

[u8] 24 -> 21
0x2023c0c
0x20242da
hits: 2

[u16 little-endian] 24 -> 21
0x2023c0c
0x20242da
hits: 2

[u32 little-endian] 24 -> 21
hits: 0

A memória inteira virou dois candidatos: 0x02023c0c e 0x020242da. O valor não aparece como u32, então ele é de 8 ou 16 bits. Guarde os dois endereços.

Passo 5. Separar o HP real do HP da tela

Dois candidatos, mas só um é o HP de verdade que a lógica da batalha usa. O outro é uma cópia que serve só para desenhar a barra na tela. Para descobrir qual é qual, force cada um de volta para 24 e veja o que acontece:

set *(unsigned short*)0x020242da = 24

Se você mudar 0x020242da e só o número na tela mudar, mas o jogo continuar tratando o Pokémon como ferido, então esse é o HP visual, não o lógico. Foi o que aconteceu no exemplo do projeto. Sobra o outro, 0x02023c0c, como candidato a HP real.

A prova definitiva vem do próximo passo.

Passo 6. O watchpoint

Um watchpoint manda o GDB parar o jogo no instante em que algum código escreve em um endereço. Coloque um no candidato a HP real:

watch *(unsigned short*)0x02023c0c
c

O c continua o jogo. Agora deixe o inimigo atacar de novo. No momento em que o jogo escreve o novo HP, o GDB para sozinho, parado exatamente na instrução que fez a escrita. Leia os registradores:

info reg pc lr r0 r1 r2 r3

No exemplo, o pc parou perto de 0x0801f8b4. Olhe as instruções em volta do pc:

x/12i $pc-12

E aparece a sequência que aplica o dano. Leia com calma, porque é o coração do patch que vem depois:

0801f8aa: ldrh r0, [r2, #0x28]   HP atual
0801f8ac: ldr  r1, [r7]          dano
0801f8ae: cmp  r0, r1
0801f8b0: ble  0x0801f8d0        se HP <= dano, desmaia
0801f8b2: subs r0, r0, r1        HP = HP - dano
0801f8b4: strh r0, [r2, #0x28]   grava o HP novo

A instrução strh r0, [r2, #0x28] é a que escreve o HP. O watchpoint a encontrou para você. Esse é o achado central do dia.

Passo 7. Descobrir a estrutura

Os registradores no momento da parada contam a estrutura inteira. No exemplo:

r2 = 0x02023be4    base do Pokémon na batalha

A instrução escreve em [r2, #0x28], ou seja, em r2 mais 0x28. Como o watchpoint bateu em 0x02023c0c, dá para fechar a conta:

r2 + 0x28 = 0x02023c0c
r2        = 0x02023c0c - 0x28 = 0x02023be4

E as instruções logo acima mostram como o jogo chega nesse r2:

movs r3, #0x58       cada Pokémon ocupa 0x58 bytes
muls r0, r3, r0      r0 = índice * 0x58
adds r2, r0, r4      r2 = base + índice * 0x58

Isso é um vetor de structs. Junte tudo e você tem o modelo do dado:

base do vetor   = 0x02023be4
tamanho do slot = 0x58 bytes
offset do HP    = 0x28, tipo u16

HP do Pokémon de índice N = 0x02023be4 + N * 0x58 + 0x28

O seu Pokémon é o índice 0:

0x02023be4 + 0 * 0x58 + 0x28 = 0x02023c0c

O inimigo, provavelmente o índice 1:

0x02023be4 + 1 * 0x58 + 0x28 = 0x02023c64

Passo 8. Provar a hipótese do inimigo

Teste o endereço do inimigo na marra. Force o HP dele para 1:

set *(unsigned short*)0x02023c64 = 1

Volte ao jogo e dê um golpe qualquer. Se o inimigo cai, você confirmou o endereço, o tamanho do slot e o offset do HP, tudo de uma vez.

Passo 9. Reconstruir a struct

Com base, tamanho e o dump da memória, dá para mapear quase todos os campos. Comparando os valores da RAM com o resumo do Pokémon na tela (espécie, ataque, defesa, HP, level), o RAM.md chega a esta estrutura:

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;
    // ... resto dos campos
};

Não precisa decorar a struct. O importante é o método: o valor da tela vira um número, o número vira um endereço, o endereço vira uma instrução, e a instrução revela a forma do dado.

O que levar deste capítulo

- O estado do jogo mora na EWRAM (0x02000000).
- Busca por diferença: fotografe, mude o valor, fotografe, compare.
- ram_find.py transforma a RAM inteira em poucos candidatos.
- Um candidato é o HP lógico, o outro é a cópia da tela.
- O watchpoint encontra a instrução exata que escreve o valor.
- Os registradores na hora da parada revelam base, tamanho e offset da struct.