~/labs/workshops/gba-firered/02-arm-e-thumb
Lendo ARM e ThumbRegistradores, instrucoes e como ler o codigo de maquina da CPU.
A CPU do GBA executa instruções. Cada instrução é um pedaço de código de máquina, ou seja, alguns bytes que mandam a CPU fazer uma coisa pequena: copiar um valor, somar, comparar, pular para outro lugar. Engenharia reversa é, no fundo, ler essas instruções e entender a história que elas contam juntas.
Este capítulo ensina a ler. Não decore tudo. Volte aqui como referência sempre que aparecer uma instrução nova.
Os registradores
A CPU não trabalha direto na memória o tempo todo. Ela tem um punhado de gavetas rápidas chamadas registradores. No modo normal do GBA você vê 16 deles:
r0 a r7 registradores baixos, uso geral
r8 a r12 registradores altos, uso geral
r13 (sp) stack pointer, aponta para o topo da pilha
r14 (lr) link register, guarda o endereço de retorno de uma função
r15 (pc) program counter, aponta para a instrução atual
Existe também um registrador de status chamado cpsr. Ele guarda as flags de
comparação e o modo da CPU. Duas coisas dele importam para a gente:
flags N, Z, C, V. São ligadas por comparações e somas. Decidem os saltos.
bit T diz se a CPU está executando ARM (T=0) ou Thumb (T=1).
Guarde os apelidos. Quando você vir sp, lr ou pc no disassembly, são apenas
r13, r14 e r15.
ARM e Thumb são o mesmo processador
O ARM7TDMI executa dois formatos de instrução:
ARM cada instrução ocupa 4 bytes (32 bits). Mais poderosa por instrução.
Thumb cada instrução ocupa 2 bytes (16 bits). Mais compacta, ocupa menos ROM.
É o mesmo processador e os mesmos registradores. O que muda é o tamanho e o formato da codificação. O GBA usa Thumb na maior parte do jogo, porque a ROM é cara e Thumb ocupa metade do espaço. A inicialização costuma ser ARM.
A CPU sabe em qual modo está pelo bit T do cpsr. Isso gera a regra mais importante
e a fonte de erro mais comum do iniciante:
Se você disassemblar Thumb como se fosse ARM, ou o contrário, sai lixo.
Quando o Ghidra mostra um monte de instrução inválida ou fluxo sem sentido, quase sempre o problema é esse: modo errado naquele endereço. O capítulo 04 mostra como marcar o modo certo no Ghidra.
Little endian, na prática
O GBA é little endian. Isso significa que um número guardado na memória aparece com o byte menos significativo primeiro. Você já viu os primeiros bytes da ROM:
offset 0x00000000: 7f 00 00 ea
Para montar a instrução ARM de 32 bits, leia os quatro bytes de trás para frente:
bytes na memória: 7f 00 00 ea
valor de 32 bits: 0xea00007f
Esse valor 0xea00007f é uma instrução de branch. O 0xea no topo é o código do
branch incondicional em ARM, e o resto é a distância do salto. O resultado é:
08000000: b 0x08000204
Que é exatamente o que o disassembler confirma:
python3 tools/disasm_gba.py 0x08000000 --size 0x10
08000000: b #0x8000204
Não precisa decodificar branch na mão na aula. O ponto é entender por que os bytes parecem embaralhados: é o little endian.
A anatomia de uma instrução
Quase toda instrução segue o mesmo desenho:
mnemônico destino, origem(ns)
O mnemônico é o nome curto da operação. O destino costuma vir primeiro, as origens depois. Exemplos:
mov r0, #0x12 coloca o número 0x12 em r0
add r2, r0, r4 r2 = r0 + r4
ldr r1, [r7] carrega para r1 o valor que está no endereço guardado em r7
O # marca um número literal, chamado imediato. Os colchetes [ ] significam “o
valor que está nesse endereço de memória”, não o endereço em si. Essa diferença é
central no ARM e está explicada logo abaixo.
ARM é uma máquina de load e store
O ARM não faz contas direto na memória. Para mexer em um valor que está na RAM, ele precisa de três passos:
1. load trazer o valor da memória para um registrador (ldr, ldrh, ldrb).
2. operar fazer a conta nos registradores (add, sub, mov, cmp).
3. store gravar o resultado de volta na memória (str, strh, strb).
O tamanho do dado aparece no sufixo da instrução de memória:
ldr / str palavra de 32 bits (4 bytes)
ldrh / strh meia palavra de 16 bits (2 bytes), o h é de halfword
ldrb / strb um byte de 8 bits
Esse detalhe não é decoração. Quando você vê strh, já sabe que o valor escrito tem
16 bits. Foi assim que, no RAM.md, deduzimos que o HP do Pokémon é um u16: a
instrução que grava o HP é um strh.
Um bloco ARM real, linha a linha
Este é o começo real do código do FireRed, o start_vector. É ARM. Foi dumpado
direto da sua ROM:
python3 tools/disasm_gba.py 0x08000204 --size 0x34
08000204: mov r0, #0x12 coloca 0x12 em r0
08000208: msr cpsr_fc, r0 escreve r0 no status da CPU (troca o modo)
0800020c: ldr sp, [pc, #0x28] carrega o stack pointer de uma constante próxima
08000210: mov r0, #0x1f coloca 0x1f em r0
08000214: msr cpsr_fc, r0 troca o modo da CPU de novo
08000218: ldr sp, [pc, #0x18] configura outro stack pointer
0800021c: ldr r1, [pc, #0x1c] carrega um endereço para r1
08000220: add r0, pc, #0x20 calcula um endereço relativo ao pc
08000224: str r0, [r1] grava r0 na memória apontada por r1
08000228: ldr r1, [pc, #0x14] carrega o endereço de AgbMain para r1
0800022c: mov lr, pc guarda o endereço de retorno em lr
08000230: bx r1 salta para o endereço em r1 (entra em AgbMain)
08000234: b 0x08000204 se voltar, repete tudo
Não precisa entender cada modo de CPU. A história em voz alta é curta:
configura a pilha, prepara as interrupções e salta para o main do jogo.
Repare em ldr sp, [pc, #0x28]. O ARM não tem espaço dentro da instrução para guardar
um endereço de 32 bits inteiro. Então ele guarda esse endereço numa constante perto do
código e carrega com um ldr relativo ao pc. Você vai ver esse padrão o tempo todo.
No Ghidra ele aparece com o valor já resolvido ao lado, o que ajuda bastante.
Thumb, o modo do jogo
Agora o mesmo tipo de leitura, mas em Thumb, que é onde a maior parte de FireRed vive.
Este é o começo de AgbMain, o main de verdade do jogo:
python3 tools/disasm_gba.py 0x080003A4 --size 0x20 --thumb
080003a4: push {r4, r5, r6, r7, lr} salva registradores na pilha (entrada de função)
080003a6: mov r7, r8 move r8 para r7
080003a8: push {r7} salva r7 na pilha também
080003aa: movs r0, #0xff coloca 0xff em r0
080003ac: bl #0x81e3b80 chama uma função (branch with link)
080003b0: movs r1, #0xa0 coloca 0xa0 em r1
080003b2: lsls r1, r1, #0x13 desloca r1 para a esquerda 0x13 bits
080003b4: ldr r2, [pc, #0xa0] carrega uma constante para r2
080003b6: adds r0, r2, #0 r0 = r2 + 0
080003b8: strh r0, [r1] grava 16 bits de r0 no endereço de r1
Duas coisas para notar.
A primeira: push {r4, r5, r6, r7, lr} na entrada de uma função é o padrão clássico
de prólogo. A função salva os registradores que vai usar e o endereço de retorno (lr)
para devolver tudo no fim. Quando você vê isso, quase sempre está no começo de uma
função.
A segunda: em Thumb muitas instruções aritméticas terminam em s, como movs,
adds, subs, lsls. Esse s significa “atualize as flags”. Em Thumb isso costuma
ser automático. Guarde por ora, porque as flags são o que decide os saltos
condicionais que vêm a seguir.
Saltos e condições
Os saltos são o que dá forma ao código. Os mais comuns:
b salto incondicional, sempre pula.
bl salto para função, guardando o retorno em lr (branch with link).
bx salto para o endereço que está num registrador.
ble, bgt, beq, bne, bpl, ... saltos condicionais, olham as flags do cpsr.
Os condicionais sempre vêm logo depois de uma comparação ou de uma operação que mexeu
nas flags. A dupla cmp seguido de um b condicional é um if da vida real:
cmp r0, r1
ble alvo se r0 <= r1, vá para alvo. senão, siga em frente.
O bit Thumb dos endereços
Um último detalhe que confunde e aparece em todo patch. Quando o código salta para
uma função Thumb usando bx, o endereço usado tem o bit 0 ligado, ou seja, soma 1.
A função começa em 0x080003A4
O endereço usado é 0x080003A5
Esse +1 não muda onde a função começa. Ele é um aviso para a CPU: “entre em modo
Thumb”. O endereço real ainda é o par. Por isso, no patch do capítulo 06, quando a
gente salta de volta para o código original com bx, o endereço termina em 1. Não é
erro, é a forma de manter a CPU em Thumb.
Tabela de bolso
Cole isto na parede durante a aula:
mov r0, #imm r0 = número
movs r0, #imm igual, e atualiza flags
add rd, ra, rb rd = ra + rb
sub rd, ra, rb rd = ra - rb
mul / muls multiplicação
lsl / lsls desloca bits para a esquerda
ldr rd, [rn] rd = memória de 32 bits em rn
ldrh rd, [rn, #off] rd = memória de 16 bits em rn+off
ldrb rd, [rn] rd = 1 byte em rn
str / strh / strb o mesmo, mas grava na memória
cmp ra, rb compara, só liga as flags
b alvo salta sempre
bl alvo chama função, retorno em lr
bx rn salta para o endereço em rn
ble/bgt/beq/bne salta se a condição das flags bater
push {lista} salva registradores na pilha (entrada de função)
pop {lista} restaura registradores (saída de função)
O que levar deste capítulo
- 16 registradores. sp, lr e pc são r13, r14 e r15.
- ARM tem 4 bytes por instrução, Thumb tem 2. Modo errado gera lixo.
- Little endian: os bytes da instrução aparecem invertidos.
- ARM carrega da memória, opera nos registradores e grava de volta.
- O sufixo h/b diz o tamanho do dado. strh = 16 bits.
- cmp liga as flags, o b condicional decide o salto. Isso é um if.
- Endereço de função Thumb usado em bx termina em 1.