← cd ../

~/labs/workshops/gba-firered/07-primeiro-patch

O primeiro patchEscrever, montar e instalar um hook que derruba o inimigo em um golpe.

Agora a gente junta tudo. Você já sabe ler ARM e Thumb, achou a instrução que aplica dano e entendeu a struct do Pokémon em batalha. Este capítulo usa isso para mudar o comportamento do jogo de verdade.

O objetivo do patch:

O Pokémon do oponente cai com qualquer golpe.
O seu Pokémon não perde HP.

O ponto de ataque

No capítulo 05, o watchpoint nos levou para este bloco, em Thumb, dentro da rotina que atualiza o HP na batalha:

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

A ideia ingênua seria trocar ldr r1, [r7] por algo que coloca um dano enorme em r1. Mas isso tem dois problemas. O primeiro: essa mesma rotina roda tanto para o seu Pokémon quanto para o oponente, então um dano enorme mataria os dois. O segundo: uma instrução Thumb tem só 2 bytes, e não cabe ali a lógica de decidir quem é quem.

A solução é um hook.

O que é um hook

Um hook substitui algumas instruções do código original por um salto para um código novo, escrito por você, em uma área livre. Esse código novo faz o que precisa e depois volta para o fluxo original. O nome clássico disso é trampoline.

código original em 0x0801F8AC
    salta para o nosso código em uma área nova
    o nosso código decide: alvo é jogador ou oponente?
    define o dano de acordo
    volta para o fluxo original

Para ter uma área nova, a gente aumenta o tamanho da ROM. A ROM original tem 16 MiB, de 0x08000000 a 0x08ffffff. O build do projeto expande para 32 MiB e coloca o código novo no primeiro endereço livre:

ROM original   0x08000000 a 0x08ffffff   (16 MiB)
área nova      a partir de 0x09000000     (onde fica o nosso código)

A regra de quem é quem

Os Pokémon em batalha são numerados. O número, chamado battler, diz de qual lado está:

0 = jogador, posição esquerda
1 = oponente, posição esquerda
2 = jogador, posição direita
3 = oponente, posição direita

Repare no padrão: número par é do jogador, número ímpar é do oponente. Para saber o lado, basta olhar o bit 0 do número:

bit 0 = 0  ->  par   ->  jogador
bit 0 = 1  ->  ímpar ->  oponente

A lógica do patch, em pseudo C, fica:

battler = *gActiveBattler;

if (battler é ímpar)     // oponente
    damage = hp;         // dano igual ao HP, então ele desmaia
else                     // jogador
    damage = 0;          // sem dano

if (hp > damage)
    hp = hp - damage;
else
    hp = 0;

Escrevendo o Assembly

O código do hook está em patches/enemy_one_hit.s, comentado linha a linha. O coração dele é este, em Thumb:

enemy_one_hit:
    ldrb r1, [r5]        @ r1 = índice do battler
    lsls r1, r1, #31     @ joga o bit 0 para o bit de sinal
    bpl  player_target   @ se o resultado não é negativo, é par (jogador)

    @ alvo oponente: damage = hp
    movs r1, r0
    b    compare_hp

player_target:
    @ alvo jogador: damage = 0
    movs r1, #0

compare_hp:
    cmp  r0, r1
    bgt  survives
    ldr  r1, =0x0801F8D1   @ caminho de desmaio, volta ao original
    bx   r1

survives:
    subs r0, r0, r1
    ldr  r3, =0x0801F8B5   @ volta para o strh original
    bx   r3

Vale parar em dois truques, porque eles mostram detalhes reais do Thumb.

O primeiro é o lsls r1, r1, #31. O Thumb mais simples não tem uma instrução para testar um bit com valor imediato. Então o código desloca o número 31 casas para a esquerda, o que joga o bit 0 lá para a posição de sinal. Se o bit 0 era 1, o resultado fica negativo, e o bpl, que significa branch if plus, não salta. É uma forma esperta de perguntar “esse número é ímpar?”.

O segundo são os endereços de volta terminados em 1:

0x0801F8D1   é o desmaio real em 0x0801F8D0, com o bit Thumb ligado
0x0801F8B5   é o strh original em 0x0801F8B4, com o bit Thumb ligado

Esse +1 é o bit Thumb que o capítulo 02 explicou. Ele mantém a CPU em modo Thumb depois do salto de volta. Sem ele, a CPU tentaria executar como ARM e travaria.

Montando o Assembly

Texto em Assembly não roda. Ele precisa virar bytes de máquina. Quem faz isso é o montador da toolchain ARM:

sudo apt install -y gcc-arm-none-eabi

A montagem tem dois passos. O primeiro transforma o .s em um objeto. O segundo extrai só os bytes da seção de código para um binário cru:

arm-none-eabi-as -mcpu=arm7tdmi -mthumb -o build/enemy_one_hit.o patches/enemy_one_hit.s
arm-none-eabi-objcopy -O binary -j .text build/enemy_one_hit.o build/enemy_one_hit.bin

O -mthumb é importante: ele monta em Thumb, que é o modo daquele trecho do jogo.

O salto que substitui o original

No lugar das instruções originais em 0x0801F8AC, a gente escreve um salto longo. Em Thumb ele é assim:

ldr r1, [pc, #0]     @ carrega o endereço de destino que está logo abaixo
bx  r1               @ salta para lá
.word 0x09000001     @ destino: 0x09000000 com o bit Thumb ligado

Em bytes, isso são oito bytes. Eles ocupam exatamente o espaço das quatro instruções originais (ldr, cmp, ble, subs), que por isso são reproduzidas dentro do nosso código.

Instalando com o build do projeto

Você não precisa fazer cada passo na mão. O script tools/build_enemy_one_hit.py automatiza tudo e ainda confere se nada saiu do lugar:

python3 tools/build_enemy_one_hit.py

Ele faz, em ordem:

1. Confere o SHA1 da ROM original. Se não for a ROM certa, para.
2. Confere se os bytes em 0x0801F8AC ainda são os originais
   (39 68 88 42 0e dd 40 1a). Isso evita aplicar o patch no lugar errado.
3. Monta o Assembly de patches/enemy_one_hit.s.
4. Expande a ROM de 16 para 32 MiB.
5. Grava o código novo em 0x09000000.
6. Escreve o salto longo em 0x0801F8AC.
7. Salva em rom/PokemonFireRed_enemy_one_hit.gba.

A saída mostra o resumo:

Codigo novo: 0x09000000 (... bytes)
Hook: 0x0801f8ac -> 0x09000000
ROM expandida: 32 MiB
Saida: rom/PokemonFireRed_enemy_one_hit.gba

A checagem dos bytes originais no passo 2 é uma lição que vale repetir em qualquer patch: antes de escrever por cima, confirme que o alvo é mesmo o que você acha que é.

Do endereço ao offset no arquivo

Quando você edita a ROM no disco, o que importa é o offset, não o endereço de memória. A conta é a do capítulo 01:

offset no arquivo = endereço - 0x08000000

Então o hook em 0x0801F8AC fica no offset 0x001F8AC do arquivo, e o código novo em 0x09000000 fica no offset 0x01000000. O script já faz essa conversão sozinho, mas é bom você saber de onde ela vem.

Testando

Abra a ROM com patch no mGBA:

rom/PokemonFireRed_enemy_one_hit.gba

Carregue o save, entre em uma batalha e ataque. O oponente deve cair com o primeiro golpe, e o seu Pokémon não deve perder HP nem quando for atingido.

Como restaurar o dano normal do jogador

O arquivo de Assembly já deixa isso documentado. Para o seu Pokémon voltar a receber dano normal, troque, no rótulo player_target, a linha:

movs r1, #0          @ dano zero

por:

ldr  r1, [r7]        @ dano original

E monte de novo. Esse é o tipo de ajuste pequeno que mostra para a turma como o patch é só código, e código se edita.

O que levar deste capítulo, e do workshop

- Um hook substitui instruções originais por um salto para código seu.
- Expandir a ROM dá espaço livre para o código novo (a partir de 0x09000000).
- O patch decide o alvo pelo bit 0 do battler: par é jogador, ímpar é oponente.
- Endereços de volta em Thumb terminam em 1 (o bit Thumb).
- Sempre confira os bytes originais antes de escrever por cima.
- offset no arquivo = endereço - 0x08000000.

O caminho completo que você percorreu:

arquitetura do GBA
  -> ler ARM e Thumb
  -> importar e mapear no Ghidra (estática)
  -> achar o HP na RAM com mGBA e GDB (dinâmica)
  -> escrever, montar e instalar um patch

Esse é o ciclo da engenharia reversa de jogos. O alvo muda, as ferramentas mudam um pouco, mas o método é sempre este.