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