Fundamentos da arquitetura ARM
No artigo de setup do ambiente eu prometi: o próximo passo era descer de fato na arquitetura e entender o banco de registradores (R0–R15) e o CPSR com suas flags. É exatamente aqui que a gente para de tratar o ARM como "caixa-preta" e começa a ler qualquer objdump ou dump de registradores sem medo.
O arm-jitter emula de tudo: de ARMv4 até ARMv9 (64 bits, AArch64) e todas as extensões. Mas o arm-box — o runner que usamos no curso — hoje suporta de ARMv5 a ARMv7. Por isso este artigo foca na ARMv7-A (32 bits): é o que você consegue rodar e inspecionar de verdade por aqui. Os conceitos base (registradores, flags, load/store) são compartilhados por toda a família.
Por que decorar a arquitetura?
Diferente de uma máquina de pilha (tipo JVM ou x86 antigo), o ARM é um RISC load/store: a aritmética só acontece em registradores e a memória é acessada por instruções dedicadas. Quem entende o banco de registradores e as flags entende 80% do que um binário está fazendo — o resto é só combinar instruções.
O banco de registradores (R0–R15)
Há 16 registradores visíveis num dado momento, numerados R0 a R15. A maioria tem papel definido por convenção, não por hardware:
- R0–R3
- registradores de propósito geral, usados por convenção para passar argumentos e retornar resultados em chamadas.
- R4–R11
- registradores de propósito geral para variáveis locais.
- R12 (IP)
- intra-procedure — usado como scratch por alguns calling conventions.
- R13 (SP)
- o stack pointer; aponta para o topo da pilha.
- R14 (LR)
- o link register; guarda o endereço de retorno de uma chamada (
bl). - R15 (PC)
- o program counter; aponta para a instrução em execução.
O detalhe que confunde quem vem de x86: o ARM tem registradores banqueados (banked). Em modos privilegiados (ex.: svc, irq, fiq) o processador troca para cópias sombra de R13 e R14 — e o modo FIQ ganha até R8–R12 próprios. É por isso que uma interrupção "some" com o SP e o LR do código de usuário: ela está usando outro banco. O arm-jitter implementa exatamente esse banco de registradores, então o que você lê num dump é o estado real desse modelo.
O CPSR e as flags de status
O CPSR (Current Program Status Register) guarda, além do modo atual e das máscaras de interrupção, as flags de condição que todas as instruções de processamento podem atualizar. As quatro principais:
- N (Negative)
- bit 31 do resultado é 1 — ou seja, o resultado é negativo.
- Z (Zero)
- o resultado é exatamente zero.
- C (Carry)
- houve "vai-um" na operação sem sinal (útil em somas/rotações multi-palavra).
- V (Overflow)
- a operação com sinal estourou a faixa representável.
Além disso, o CPSR carrega os bits I e F (máscara de IRQ e FIQ), o bit T (indica se o processador está em THUMB) e os 5 bits M que codificam o modo atual. As flags só mudam quando a instrução tem o sufixo S — ADD não toca no CPSR, mas ADDS sim. Esse é o truque por trás da execução condicional do ARM.
Modos de execução
O ARMv7-A tem sete modos principais. O User é o único sem privilégios; todos os outros são privilegiados e existem para tratar exceptions (interrupções, erros, chamadas de sistema):
- User (usr) — código de aplicação, sem privilégio.
- Supervisor (svc) — entrado por
svc(antigoswi) e por reset; é onde o kernel vive. - IRQ / FIQ — interrupções comuns e rápidas (FIQ tem mais registradores banqueados para ser veloz).
- Abort (abt) — falha de acesso à memória.
- Undefined (und) — instrução inexistente.
- System (sys) — igual ao User em registradores, mas privilegiado.
Na prática do nosso curso quase tudo roda em User: o arm-box não finge um kernel, ele só executa seu binário. Mas vale saber que, num sistema real, cada interrupção troca o banco de SP/LR e o modo no CPSR.
O conjunto de instruções (ISA)
O ARM convive com três "sabores" de código:
- ARM — instruções de 32 bits, de 3 operandos e condicionalmente executáveis. É o formato mais direto de ler.
- THUMB — instruções de 16 bits, mais densas (código menor). O bit T do CPSR indica quando o processador está decodificando THUMB.
- THUMB-2 — mistura 16/32 bits, tentando unir densidade e performance; é o que a maioria dos binários modernos usa.
A característica que mais impressiona quem chega do x86 é a execução condicional: quase toda instrução ARM aceita um sufixo de condição, então em vez de um desvio você escreve addeq r0, r1, r2 ("some se Z=1"). Isso elimina montes de saltos. Condições comuns: eq (igual, Z=1), ne (diferente, Z=0), cs/hs (carry set), cc/lo (carry clear), mi/pl (negativo/positivo), gt/lt (maior/menor com sinal).
Modos de endereçamento
Como só LDR/STR tocam memória, o ARM coloca toda a flexibilidade de endereçamento nesses operandos. Os principais:
add r0, r1, #5 @ imediato: r0 = r1 + 5
add r0, r1, r2 @ registrador: r0 = r1 + r2
ldr r0, [r1, r2, LSL #2] @ registrador deslocado: r0 = mem[r1 + r2*4]
ldr r0, =tabela @ PC-relative: carrega endereço de 'tabela'
ldr r0, [r1, #4]! @ pré-indexado com writeback (!): r1 = r1+4
ldr r0, [r1], #4 @ pós-indexado: usa r1 e depois r1 = r1+4 O LSL #2 é o que faz um array de 4 bytes virar "índice vezes 4" sem instrução extra — muito comum em acessos a tabelas.
Princípio load/store
Regra de ouro: aritmética só em registradores. ADD, SUB, AND, ORR etc. nunca leem ou escrevem memória. Para operar sobre dados na RAM você faz LDR para um registrador, calcula, e STR de volta. É por isso que ver LDR r0, [r1] seguido de ADDS r0, r0, #1 é o padrão "incremente o valor na memória".
Exemplo prático: flags na prática
Um programinha mínimo que mexe nas flags. Depois de montar e rodar no arm-box, inspecione o CPSR:
.syntax unified
.arch armv7-a
.text
.global _start
_start:
mov r0, #10 @ r0 = 10
mov r1, #3 @ r1 = 3
adds r2, r0, r1 @ r2 = 13 -> CPSR reflete (Z=0, N=0)
subs r3, r1, r1 @ r3 = 0 -> flag Z = 1
subs r4, r0, #20 @ r4 = -10 -> flag N = 1
mov r0, #0 @ código de saída
bx lr Repare no S obrigatório: sem ele, nenhuma flag mudaria e a execução condicional deixaria de fazer sentido.
Como observar o resultado passo a passo (GDB)
O arm-box já imprime o dump final dos registradores ao encerrar — é o atalho rápido: procure o CPSR e confira Z=1 (do subs) e N=1 (do subs r4, r0, #20). Mas para ver as flags mudando instrução a instrução, a ferramenta certa é o GDB. Como o binário é ARM rodando num host x86_64, usamos o qemu-arm (user-mode) como "motor" e o gdb-multiarch (instalado no setup) como front-end. Se o qemu-arm não estiver presente, instale com sudo apt install qemu-user (Linux) ou brew install qemu (macOS).
Terminal 1 — sobe o binário sob o gdbserver do qemu na porta 1234:
qemu-arm -g 1234 flags.elfTerminal 2 — abre o GDB, aponta para a arquitetura e conecta:
gdb-multiarch flags.elf
(gdb) set architecture arm
(gdb) target remote localhost:1234
(gdb) break _start
(gdb) continue Agora vá passo a passo com stepi (ou si) e inspecione o estado a cada instrução. Comece pelo resultado que você já calculou no papel e confirme cada passo:
(gdb) stepi
(gdb) info registers r0 r1 r2 r3 r4
(gdb) p/t $cpsr # bits do CPSR: N Z C V ... I F T e o modo
(gdb) stepi
(gdb) info registers r2
(gdb) p $cpsrO que você deve ver, passo a passo:
- antes do
adds:r0=10,r1=3; - após
adds r2, r0, r1:r2=13, CPSR comZ=0eN=0; - após
subs r3, r1, r1:r3=0e a flagZ=1acende; - após
subs r4, r0, #20:r4=-10(0xFFFFFFF6) e a flagN=1acende.
É assim que se liga a teoria ao silício (virtual): cada stepi é uma instrução real e cada info registers é o raio-X do processador naquele exato instante. Quando o arm-box ganhar um modo de depuração, você o usaria da mesma forma — target remote e stepi.
Obs.: o exemplo retorna com bx lr (convenção do arm-box). Se for executá-lo até o fim sob qemu-user, troque o bx lr por mov r7, #1 seguido de svc #0 (exit do Linux) — o acompanhamento das flags é idêntico. Para repetir o teste de fumaça do setup, monte com arm-none-eabi-as + arm-none-eabi-ld e rode com java -jar target/armbox-1.0-SNAPSHOT.jar flags.elf.
Próximos passos
Agora você entende o "hardware" invisível por trás de qualquer dump. No próximo artigo mergulhamos na anatomia de um binário ELF: seções, o símbolo _start e como o linker monta o executável que o arm-box carrega — completando o ciclo entre o código que você escreve e o que a CPU (virtual) executa. Veja a lista de artigos para acompanhar.