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