← cd ../

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