← cd ../

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

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.