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