~/labs/workshops/gba-firered/06-encontrando-a-funcao
Encontrando e limpando a funcaoFechar a funcao do HP no Ghidra e deixar o decompile legivel: antes e depois reais.
No capítulo 05 a análise dinâmica entregou um endereço: 0x0801F8AC, a instrução que
escreve o HP. Sabemos o lugar exato. Agora vem o trabalho estático de transformar
aquele endereço solto em uma função inteira, com nomes que fazem sentido.
Este capítulo tem uma particularidade que vale dizer logo. Em uma aula real, a análise do Ghidra costuma ser feita parcial, de propósito, para não esperar a máquina processar a ROM inteira. Isso deixa muita coisa por fazer, e é justamente o cenário mais comum no mundo real: você cai em um trecho que o Ghidra ainda não entendeu. Os passos abaixo são todos manuais, do jeito que o aluno vai fazer na própria mão. No fim do capítulo tem uma nota sobre como automatizar tudo com script, mas aprenda na mão primeiro.
Passo 1. O endereço está em terra de ninguém
Abra o Ghidra e vá para o endereço que a dinâmica encontrou:
Navigation > Go To...
0801F8AC
Numa análise parcial, três sintomas aparecem aqui:
- não existe função definida nesse ponto;
- o painel de funções não mostra nada que contenha esse endereço;
- os bytes em volta podem estar como dados crus, sem virar instrução.
Uma forma rápida de confirmar: peça ao Ghidra a função que contém o endereço. Ele vai
devolver a função definida mais próxima para trás, que não tem nada a ver com o HP.
No projeto da aula, ela cai em algo como FUN_08017248, uma função pequena e sem
relação. Isso é a prova de que o trecho do HP não pertence a nenhuma função fechada.
Passo 2. A armadilha de criar função no fragmento
A tentação é colocar o cursor em 0x0801F8AC e apertar F para criar uma função ali
mesmo. Não faça isso esperando um bom resultado. Aquele endereço é o meio de uma
função, não o começo. Se você criar uma função no meio, o Ghidra tenta decompilar a
partir dali, sem o início e sem o contexto, e o resultado é lixo:
void FUN_0801f8a0(void)
{
/* WARNING: Bad instruction - Truncating control flow here */
halt_baddata();
}
A lição é direta: um fragmento não é uma função. Para ter um decompile que presta, você precisa de duas coisas, o início real da função e o corpo inteiro disassemblado no modo certo.
Passo 3. Achar o início da função
Existem três caminhos para achar onde a função começa. Use o que for mais natural para você. Os três levam ao mesmo lugar.
Caminho A. Subir procurando o prólogo
Quase toda função Thumb começa salvando registradores na pilha. O prólogo clássico é:
push {r4, r5, r6, r7, lr}
Em bytes, esse push é f0 b5. Então você pode rolar o Listing para cima a partir do
trecho do HP, procurando o primeiro push {..., lr} que aparece. No projeto da aula,
ele está em:
0x0801F684: push {r4, r5, r6, r7, lr}
Esse é o início da função.
Caminho B. Seguir o salto para trás
A dinâmica caiu em 0x0801F8A0. Olhando o código logo acima, existe um salto que
manda a execução para lá:
0801F850: b 0x0801F8A0
Ou seja, o trecho do HP é alcançado por um salto interno, vindo de mais acima na mesma função. Seguindo esses saltos para trás, você sobe até o começo. Esse é o raciocínio de fluxo de controle, e é o que mais ensina, porque mostra como as partes da função se ligam.
Caminho C. A tabela de comandos de batalha
FireRed roda a batalha como uma lista de comandos. Existe uma tabela em que cada posição aponta para uma função de comando. A função que estamos olhando é um desses comandos, o que atualiza o HP. Achando a tabela e o índice do comando, você chega no endereço da função sem precisar procurar o prólogo na mão. Esse caminho é mais avançado, mas é o mais limpo quando você já conhece a estrutura do jogo.
Passo 4. O problema das literal pools
Se você rolar o Listing entre o início e o trecho do HP, vai ver linhas estranhas no meio do código, do tipo:
subs r5, #0xd0
lsls r2, r0, #8
Não se assuste. Isso não é código. São constantes que a função guarda no meio das
instruções, chamadas literal pools. Lembre do capítulo 02: o ARM não cabe um endereço
de 32 bits dentro da instrução, então ele guarda esse endereço perto e carrega com
ldr rX, [pc, #...]. Esses valores guardados, quando lidos como instrução, viram esse
tipo de lixo. O Ghidra, ao analisar a função direito, marca essas regiões como dados e
para de tentar lê-las como código.
Passo 5. Disassemblar e fechar a função, na mão
Agora o conserto. Tudo no CodeBrowser, sem script.
1. Selecione do início da função até um pouco depois do fim.
No projeto da aula: de 0x0801F684 até cerca de 0x0801FA70.
2. Clique direito > Clear Code Bytes.
Isso limpa qualquer instrução ou função errada que já exista no range,
inclusive um fragmento que você tenha criado por engano.
3. Com o range ainda selecionado, clique direito > Set Register Values...
Defina TMode = 1. Isso diz ao Ghidra que o trecho é Thumb.
4. Vá ao início, em 0x0801F684, e pressione D para disassemblar.
O Ghidra segue o fluxo e monta as instruções, pulando as literal pools.
5. Com o cursor em 0x0801F684, pressione F para criar a função.
Pronto. Agora existe uma função que vai de 0x0801F684 até por volta de 0x0801FA69,
e ela contém o trecho do HP em 0x0801F8AC.
Passo 6. O decompile cru
Abra a janela Decompile com a função selecionada. O resultado vem assim, cru:
undefined4 FUN_0801f684(void)
{
ushort uVar1;
byte bVar4;
int iVar8;
uint *puVar14;
byte *pbVar16;
undefined4 in_lr;
if (*DAT_0801f6ac != 0) {
return in_lr;
}
/* ... muitas linhas ... */
iVar8 = (uint)*pbVar15 * 0x58 + DAT_0801f8c8;
uVar1 = *(ushort *)(iVar8 + 0x28);
if ((int)uVar13 < (int)(uint)uVar1) {
*(ushort *)(iVar8 + 0x28) = uVar1 - (short)uVar13;
}
else {
*(undefined2 *)((uint)*pbVar15 * 0x58 + iVar10 + 0x28) = 0;
}
}
Isto funciona, mas é difícil de ler. Tudo tem nome de máquina: FUN_, DAT_,
uVar1, pbVar15. Mesmo assim, dá para reconhecer o que a dinâmica já tinha mostrado.
O * 0x58 é o tamanho do slot de cada Pokémon. O + 0x28 é o offset do HP. O
*(ushort *)(... + 0x28) é o HP atual. Estática e dinâmica batendo no mesmo ponto.
Passo 7. Dar nomes e forma, na mão
Agora a parte que transforma o decompile. A ideia é ensinar o Ghidra três coisas: o que é a struct do Pokémon, onde estão os dados globais e como a função se chama.
Criar a struct BattlePokemon
A dinâmica do capítulo 05 já deu o mapa: cada Pokémon em batalha ocupa 0x58 bytes, o HP fica em 0x28 e o HP máximo logo depois. Crie a struct na mão:
1. Abra a janela Data Type Manager.
2. Clique direito no arquivo do programa > New > Structure.
3. Dê o nome BattlePokemon.
4. Ajuste o tamanho total para 0x58.
5. Defina os campos que você já conhece, pelo offset:
0x00 species u16
0x28 hp u16
0x2A level u8
0x2C maxHP u16
Os outros offsets podem ficar como undefined por enquanto.
Nomear e tipar os dados globais
No decompile cru, o DAT_0801f8c8 é o endereço que guarda a base da lista de Pokémon.
Lá no capítulo 05 a gente viu que esse valor é 0x02023BE4. Faça assim, em cima do
DAT_0801f8c8 no Listing ou no Decompile:
1. Pressione L para renomear o símbolo para gBattleMons.
2. Pressione T (ou Ctrl+L) para mudar o tipo para BattlePokemon *.
Repita a ideia para o ponteiro do dano, que guarda 0x02023D54:
Renomeie para gBattleMoveDamage, tipo int *.
E para o índice do battler, renomeie para gActiveBattler ou gBattlerTarget conforme o caso, com tipo u8 *.
Renomear a função
Por fim, clique no nome da função e pressione L. Pelos símbolos públicos do projeto pret/pokefirered, essa função é o comando que atualiza o HP na batalha:
Cmd_datahpupdate
Passo 8. O decompile limpo
Reabra o Decompile. O mesmo código de antes agora fica assim:
undefined4 Cmd_datahpupdate(void)
{
ushort uVar1;
BattlePokemon *pBVar2;
int iVar15;
byte *pbVar16;
/* ... */
pBVar2 = gBattleMons;
iVar15 = *piVar12; // dano
if (iVar15 < 0) { // dano negativo significa cura
gBattleMons[*gActiveBattler].hp = gBattleMons[*gActiveBattler].hp - (short)iVar15;
if (pBVar2[bVar5].maxHP < pBVar2[bVar5].hp) {
pBVar2[bVar5].hp = pBVar2[bVar5].maxHP; // não passa do HP máximo
}
}
else {
uVar1 = gBattleMons[*pbVar16].hp;
if (iVar15 < (int)(uint)uVar1) {
gBattleMons[*pbVar16].hp = uVar1 - (short)iVar15; // HP -= dano
*gBattleMoveDamage = iVar15;
}
else {
pBVar2[*pbVar16].hp = 0; // HP zera, Pokémon desmaia
}
}
}
Compare as duas versões. Onde antes estava *(ushort *)(iVar8 + 0x28), agora está
gBattleMons[*pbVar16].hp. O mesmo trecho de máquina, lido como gente. E veja que o
caminho de dano é exatamente o que a gente vai alterar no próximo capítulo:
se o dano for menor que o HP, subtrai o dano do HP;
senão, zera o HP e o Pokémon desmaia.
Passo 9. Conferir com fontes externas
Uma boa prática de engenharia reversa é cruzar o seu achado com material público. O
projeto pret/pokefirered é uma reconstrução do código fonte do jogo. Procurando por
Cmd_datahpupdate lá, você encontra a função original em C, com a mesma lógica de HP,
o mesmo tratamento de cura e dano, e os mesmos nomes de globais. Isso confirma que o
seu trabalho estático e dinâmico chegou no lugar certo.
https://github.com/pret/pokefirered
Outros métodos que valem a pena no workshop
O caso do HP usou busca dinâmica mais watchpoint mais leitura estática. Vale mostrar para os alunos que existem outros caminhos, e que o bom é combinar:
Seguir xrefs depois de fechar a função, peça ao Ghidra quem a chama.
Isso leva à tabela de comandos de batalha e ao interpretador.
Tabela de ponteiros achar uma tabela de funções e renomear todas de uma vez, usando
os nomes do pret como guia.
Comparar com save alterar um valor no jogo, salvar, e comparar dumps para isolar
onde aquele dado fica, como no capítulo 05.
Strings procurar textos do jogo na ROM e seguir quem os usa para achar
menus, falas e telas.
Cross-check público comparar sempre com pret/pokefirered para validar nomes e structs.
A regra geral: estática mostra a lógica, dinâmica mostra o dado vivo, e as fontes públicas confirmam os nomes. Quando os três concordam, você pode confiar no achado.
Nota sobre automação
Tudo o que foi feito na mão aqui também pode ser feito por script. O Ghidra tem um executor de Python que dá acesso à mesma API: dá para marcar Thumb, disassemblar, criar a função, criar a struct, renomear e tipar tudo de uma vez. Em uma turma, o caminho na mão ensina os conceitos. O caminho por script serve depois, quando você precisa repetir o processo em várias funções ou em várias versões do jogo. Aprenda na mão primeiro, e automatize quando o trabalho virar repetição.
O que levar deste capítulo
- Um endereço solto da dinâmica quase nunca é o início de uma função.
- Criar função no meio gera lixo. Ache o início real primeiro.
- O início se acha por três caminhos: prólogo push, salto para trás, tabela de comandos.
- Análise parcial deixa código sem disassemblar. Marque Thumb, D e F na mão.
- Criar a struct e nomear os globais transforma o decompile em algo legível.
- Sempre confira com pret/pokefirered.