← Voltar aos artigosFundamentos da arquitetura ARM

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 SADD 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 (antigo swi) 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.elf

Terminal 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 $cpsr

O que você deve ver, passo a passo:

  • antes do adds: r0=10, r1=3;
  • após adds r2, r0, r1: r2=13, CPSR com Z=0 e N=0;
  • após subs r3, r1, r1: r3=0 e a flag Z=1 acende;
  • após subs r4, r0, #20: r4=-10 (0xFFFFFFF6) e a flag N=1 acende.

É 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.