← cd ../

~/labs/workshops/gba-firered/04-analise-estatica

Analise estatica no GhidraSeguir o boot a partir do unico endereco conhecido e deduzir a estrutura ate achar o loop principal.

Análise estática é estudar o código sem rodar o jogo. Você lê as instruções, segue os saltos e deduz o que cada trecho faz.

Antes de tudo, um aviso que vale para o capítulo inteiro. A gente não vai abrir a ROM e sair dando nome às coisas. Isso seria trapaça. No começo a gente conhece um único endereço: 0x08000000, onde o console começa a executar. Tudo o mais a gente descobre seguindo o código a partir dali. Cada nome que aparecer neste capítulo vem depois da análise que o justifica, nunca antes. Os nomes finais, bonitos, a gente confirma no fim cruzando com uma fonte pública, exatamente a mesma disciplina que o capítulo 06 usa.

O README.md na raiz tem o mapa completo do boot, com todos os endereços já resolvidos. Use ele como gabarito para conferir o seu trabalho, não como atalho para pular a dedução.

Passo 1. Importar a ROM

No Ghidra, com o projeto aberto:

File > Import File...
Escolha rom/PokemonFireRed.gba

A ROM do GBA não tem um formato executável como um programa de PC. Ela é um blob de bytes que o console mapeia. Então o Ghidra vai chamar isso de Raw Binary. Isso é o esperado, não é erro.

Antes de confirmar, configure três coisas. São as mais importantes do capítulo inteiro:

Language:      ARM:LE:32  (ARM, little endian, 32 bits)
Base Address:  0x08000000
Compiler:      default

LE é little endian, que o capítulo 01 explicou. A base 0x08000000 é onde o console coloca a ROM, então o Ghidra precisa saber disso para os endereços baterem com o jogo.

Confirme. Quando o Ghidra perguntar se quer analisar agora, pode aceitar a análise automática. Ela cria muitas funções sozinha, mas não todas. Vários trechos vão ficar como bytes crus, sem função. Isso é normal, e é justamente o cenário com que a gente vai trabalhar: o Ghidra ajuda, mas o caminho do boot a gente abre na mão.

Passo 2. Conferir se a base está certa

Abra a janela de memória:

Window > Memory Map

Você deve ver um bloco começando em 0x08000000:

ram: 08000000 - 08ffffff

Se o bloco começa em 0x00000000, a base ficou errada. Reimporte com a base 0x08000000. Esse erro estraga todos os endereços, então vale conferir.

Passo 3. A única coisa que a gente sabe

A gente não sabe onde está o main, nem o loop, nem nada. Sabe uma coisa só: o GBA começa a executar no primeiro byte da ROM, em 0x08000000. É o nosso ponto de partida, e o único.

Vá para lá:

Navigation > Go To...
08000000

Se aparecerem bytes soltos com ?? ao lado, quer dizer que o Ghidra carregou os bytes mas ainda não criou a instrução ali. Resolva assim:

1. Clique no byte em 0x08000000.
2. Se houver algum dado criado ali, use o botão direito, Clear Code Bytes.
3. Pressione D, ou botão direito, Disassemble.

O Ghidra junta os quatro bytes 7f 00 00 ea na instrução:

08000000  b  0x08000204

Pare e leia o que isso de fato é. b é um branch incondicional, que o capítulo 02 mostrou. A primeira coisa que o jogo faz, antes de qualquer outra, é dar um salto para frente, para 0x08000204. Pulou 0x204 bytes logo de cara. A pergunta natural é: por quê? O que tem entre 0x08000000 e 0x08000204 que o jogo não quer executar?

A resposta está em olhar o que vem logo depois do salto. Veja os bytes a partir de 0x08000004:

08000004  24 ff ae 51 69 9a a2 21 3d 84 82 0a ...

Isso não é código. É o cabeçalho do cartucho. Os primeiros 192 bytes da ROM são dados fixos que o GBA exige: o logo da Nintendo (esses 24 ff ae 51... são o logo comprimido), o título do jogo, o código do produto, um checksum. A BIOS lê e confere essa área no boot. Se a CPU tentasse executar esses bytes como instruções, sairia lixo.

Então o trabalho inteiro da primeira instrução é um só: pular o cabeçalho e cair no primeiro byte de código real. Esse endereço de entrada a gente pode chamar de Start, porque foi isso que a análise mostrou que ele é, o ponto onde a execução nasce.

Passo 4. O código de verdade, em ARM

Siga o salto. Vá para o destino:

Navigation > Go To...
08000204

Numa análise parcial, é comum não existir função aqui ainda. Se vier byte cru, faça Clear Code Bytes se precisar e pressione D. Esse trecho é ARM, então cada instrução ocupa 4 bytes:

08000204  mov  r0, #0x12        modo IRQ
08000208  msr  cpsr_fc, r0       troca a CPU para esse modo
0800020c  ldr  sp, [pc, #0x28]   stack do modo IRQ  (= 0x03007fa0)
08000210  mov  r0, #0x1f         modo System
08000214  msr  cpsr_fc, r0       troca de novo
08000218  ldr  sp, [pc, #0x18]   stack do modo System (= 0x03007e40)
0800021c  ldr  r1, [pc, #0x1c]   r1 = 0x03007ffc
08000220  add  r0, pc, #0x20     r0 = 0x08000248
08000224  str  r0, [r1]          grava 0x08000248 em 0x03007ffc
08000228  ldr  r1, [pc, #0x14]   r1 = 0x080003a5
0800022c  mov  lr, pc            guarda o retorno
08000230  bx   r1                salta para 0x080003a5
08000234  b    0x08000204        se voltar, recomeça tudo

Você não precisa decorar os modos da CPU. Mas dá para ler a história inteira, e ela é reveladora. Três blocos:

Primeiro, as pilhas. Os pares mov mais msr trocam o modo da CPU, e cada modo recebe seu próprio stack pointer. É a CPU preparando o terreno: cada modo de execução precisa de uma pilha. O Ghidra mostra os valores resolvidos ao lado, 0x03007fa0 e 0x03007e40, ambos na IWRAM.

Segundo, a interrupção. Olhe o par em 0x08000220 e 0x08000224. Ele calcula o endereço 0x08000248 e grava esse endereço em 0x03007ffc. Esse 0x03007ffc não é um lugar qualquer: é o slot fixo onde a BIOS do GBA espera encontrar o endereço do tratador de interrupção. Ou seja, o jogo está dizendo à BIOS “quando der uma interrupção, pule para 0x08000248”. A gente acabou de descobrir, por dedução, que existe um tratador de interrupção em 0x08000248. Ainda não analisamos ele, mas já sabemos o que ele é e onde fica, porque vimos seu endereço ser instalado. Vamos chamá-lo de intr_main.

Terceiro, o salto para o jogo. Em 0x08000228 o código carrega 0x080003a5 em r1 e em 0x08000230 faz bx r1. Aqui está a resposta para uma pergunta que parecia mágica: como a gente sabe onde fica o main e que ele é Thumb? A startup acabou de contar. Ela salta para 0x080003a5. Lembre do capítulo 02: quando um bx salta para uma função Thumb, o endereço usado tem o bit 0 ligado, ou seja, soma 1. Então o endereço real é 0x080003a4, e o +1 é o aviso “entre em modo Thumb”. A startup nos entregou de bandeja o endereço do main e o modo dele.

Por último, o b 0x08000204 depois do bx é uma rede de segurança: se o main algum dia retornar, a execução cai aqui e recomeça tudo. Num jogo, o main nunca deve retornar.

Crie a função e dê o nome que a análise justifica:

Clique em 0x08000204
Pressione F
Renomeie para start_vector

Resumo do que esse bloco faz, em voz alta:

configura as pilhas, instala o tratador de interrupção e salta para o main em Thumb.

Passo 5. Entrar no main

A startup mandou a gente para 0x080003a4, em Thumb. Vá para lá:

Navigation > Go To...
080003A4

Aqui mora a pegadinha mais comum. A gente já sabe, pela análise da startup, que esse trecho é Thumb. Mas se você só apertar D, o Ghidra pode tentar ler como ARM e mostrar lixo. Primeiro marque o modo Thumb naquele trecho:

Selecione a partir de 0x080003A4
Botão direito > Set Register Values...
TMode = 1

Se a sua versão do Ghidra não tem TMode, procure por Set Processor Context ou algo com ARM/Thumb. A ideia é sempre a mesma: dizer ao Ghidra que ali é Thumb antes de desmontar. Depois pressione D. Agora sai certo, em instruções de 2 bytes:

080003a4  push {r4, r5, r6, r7, lr}   prólogo de função
080003a6  mov  r7, r8
080003a8  push {r7}
080003aa  movs r0, #0xff
080003ac  bl   ...                     começa uma sequência de chamadas
080003b0  ...

Aquele push {r4, r5, r6, r7, lr} na primeira linha é o prólogo clássico, o jeito que quase toda função Thumb começa, salvando registradores e o retorno. Isso confirma que a gente está no início de uma função de verdade, e não no meio de uma. Crie a função com F.

Por enquanto, o nome honesto para ela é só “o main”, porque é para onde a startup saltou. O nome canônico AgbMain a gente aplica no fim, depois de conferir com a fonte pública.

Passo 6. Ler a inicialização

Role o Listing para baixo a partir do push. O que você vê é uma fileira longa de chamadas, uma atrás da outra:

080003ac  bl  ...
080003ba  bl  ...
080003c6  bl  ...
080003ca  bl  ...
080003ce  bl  ...
080003d2  bl  ...
080003d6  bl  ...
080003da  bl  ...
080003de  bl  ...
...

Você não precisa entrar em cada uma agora. O formato já conta a história. Uma função que não faz quase nada além de chamar dezenas de outras funções em sequência, sem laço, é uma rotina de inicialização. É o jogo ligando os subsistemas um por um: vídeo, som, controle, sistema de save, e por aí vai. A gente deduz isso pela forma, uma reta de chamadas, e confirma os detalhes depois, função por função, quando precisar de alguma delas.

O ponto aqui não é nomear cada chamada. É reconhecer o padrão: setup é uma sequência reta de chamadas. O interessante vem logo depois dela.

Passo 7. Achar o loop principal

Aqui está a dedução central do capítulo. A gente quer achar o loop principal do jogo sem que ninguém diga onde ele está.

Comece pelo raciocínio. Um jogo roda para sempre, um quadro por vez, até você desligar. Logo, em algum lugar tem que existir um loop infinito: um salto que volta para trás e não tem como sair. Não é um if, não é uma chamada. É um b para um endereço anterior, sem condição. Achar esse salto é achar o loop.

Continue rolando o Listing para baixo, passando a sequência de inicialização. Por volta de 0x08000418 a fileira de chamadas de setup termina e começa um trecho com outra cara, que se repete. Siga descendo até encontrar um branch que aponta para trás. Ele aparece:

080004aa  b  0x0800041a

Esse é o loop. Um b incondicional que volta para 0x0800041a. Sem condição antes, sem saída depois. A execução entra em 0x0800041a, desce até 0x080004aa e volta para 0x0800041a, para sempre. A gente não foi avisado do endereço: a gente seguiu o fluxo de controle e encontrou o laço fechando. Esse é o jeito de achar um game loop na marra.

Agora olhe o corpo do laço, do começo ao fim. As duas pontas dizem muito:

0800041a  bl  0x080005e8    primeira coisa do quadro
   ...                      no meio, a lógica do estado atual do jogo
080004a6  bl  0x08000890    última coisa antes de repetir
080004aa  b   0x0800041a    volta para o topo

Entre na primeira chamada do laço, 0x080005e8, e na última, 0x08000890, e leia o que elas fazem. A primeira lê o estado dos botões do controle. A última faz uma chamada de BIOS e espera. O padrão é o de todo jogo: ler o controle no topo do quadro, processar, e no fim esperar a tela terminar de desenhar antes de começar o próximo. Pela função, dá para chamá-las de ReadKeys e WaitForVBlank, e a gente confirma esses nomes na fonte pública no passo 8.

Sobra o miolo do laço, entre ler o controle e esperar o quadro. É ali que o jogo decide o que mostrar: menu, mundo, batalha, loja. Repare que o laço chama várias vezes a mesma função, 0x080004b0. Entre nela e você vai ver que ela não tem a lógica de batalha escrita dentro. Em vez disso, ela chama um endereço guardado numa variável, um ponteiro de função. Isso é um callback.

Essa é a ideia mais útil daqui, e ela explica como o jogo todo se organiza:

O loop principal é sempre o mesmo.
O estado do jogo troca o ponteiro de callback que o loop chama.
Menu, mundo, batalha e loja são callbacks diferentes pendurados no mesmo loop.

Por isso os patches de gameplay não mexem no main nem no loop. Eles mexem na lógica de batalha, de encontro selvagem ou de loja, que são callbacks bem mais adiante. E é exatamente por isso que, nos capítulos 05 e 06, a gente não vai procurar a batalha descendo do main. Vai usar a análise dinâmica para cair direto na instrução do HP, e de lá subir até a função, com o mesmo método de seguir o fluxo que você usou aqui para achar o loop.

Passo 8. Confirmar os nomes na fonte pública

Tudo o que a gente fez até aqui foi dedução a partir do código. A forma a gente descobriu sozinho: entrada que pula o cabeçalho, uma startup ARM que arma pilhas e interrupção e salta para um main Thumb, e um main que inicializa os subsistemas e cai num loop infinito que lê o controle e espera o quadro. Os nomes finais, a gente confirma cruzando com material público.

O projeto pret/pokefirered é uma reconstrução do código fonte do jogo, com os nomes originais das funções. Conferindo o boot lá, a forma que você deduziu bate, e os nomes canônicos aparecem. Aplique só os que a sua análise justificou até agora:

0x08000000  Start          a entrada que pula o cabeçalho
0x08000204  start_vector   a startup ARM
0x08000248  intr_main      o tratador de interrupção que a startup instalou
0x080003A4  AgbMain        o main para onde a startup salta
0x080005E8  ReadKeys       primeira chamada do loop, lê o controle
0x08000890  WaitForVBlank  última chamada do loop, espera o quadro

Repare no que a gente não fez: não saiu nomeando os callbacks do miolo do laço nem as dezenas de funções de inicialização. A gente ainda não analisou essas. Nomear agora seria voltar a chutar. Quando uma delas importar para o nosso objetivo, a gente vai até ela com o mesmo método de sempre: achar a instrução, achar o início da função, analisar, e só então dar o nome. É isso que os capítulos 05 e 06 fazem com a função do HP.

A lista completa de endereços do boot está no README.md, como gabarito.

Conferir sem o Ghidra

Para checar qualquer trecho sem depender do Ghidra, use o disassembler do projeto. Ele lê os bytes direto da sua ROM:

python3 tools/disasm_gba.py 0x08000204 --size 0x34          # a startup, em ARM
python3 tools/disasm_gba.py 0x080003A4 --size 0x100 --thumb # o main, em Thumb

Os endereços que saem dele têm que bater com os do Ghidra. Quando batem, você sabe que não errou o modo nem a base.

O que levar deste capítulo

- Comece pelo único endereço certo: 0x08000000. Tudo o mais é dedução a partir dele.
- A primeira instrução é um branch que pula o cabeçalho de 192 bytes do cartucho.
- A startup ARM arma as pilhas, instala a interrupção e salta para o main em Thumb.
- O bx para 0x080003a5 entrega o endereço do main e o modo Thumb (o +1 é o bit T).
- Thumb precisa de TMode = 1 antes do D, senão sai lixo.
- Setup é uma sequência reta de chamadas. O loop é um b incondicional para trás.
- O loop é fixo. O estado do jogo mora nos callbacks que ele chama.
- Nome só depois da análise que o justifica. Os nomes finais, confira com pret.