Primeiro programa em assembly ARM
Objetivo
Até aqui a trilha decodificou instruções ARM (à mão e via arm-jitter) e olhou código gerado
pelo compilador. Nesta lição a via é inversa: escrever assembly ARM do zero, sem C, sem
biblioteca — só instruções, um _start e o exit code como prova de que o resultado está
certo. Duas funções pequenas bastam para isso: fib(n), que exercita laço e flags, e max(a, b),
que mostra o ARM resolvendo um if sem desviar. No fim você vai ter montado (as), ligado
(ld) e rodado os dois binários no arm-box.
O contrato: r0 e bx lr
A convenção de chamada (AAPCS) do ARM, em uma frase: argumentos entram em r0–r3, o retorno sai em r0, e bx lr volta. bl funcao grava o endereço da instrução seguinte em lr (r14) e pula; bx lr desvia de volta para lá. Isso é tudo que uma folha (função que não chama ninguém) precisa saber.
Para provar o resultado sem printf, os exemplos usam o exit code: mov r7, #1 + svc #0 é o exit(r0) do Linux. O arm-box propaga o r0 do guest como código de saída do processo.
fibonacci(n)
A sequência de Fibonacci é definida por F(0) = 0, F(1) = 1 e F(n) = F(n-1) + F(n-2) para
n > 1 — cada termo é a soma dos dois anteriores (0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55...). A
versão recursiva direta da definição existe, mas refaz o mesmo cálculo várias vezes (custo
exponencial); a versão iterativa abaixo carrega só os dois últimos termos (a e b) e os
atualiza n vezes num laço — custo linear, sem pilha de chamadas, exatamente o tipo de laço
simples que serve para mostrar registrador, flag e desvio condicional em assembly puro.
fib.s (.elf) — versão iterativa, n em r0:
_start:
mov r0, #10 @ n = 10
bl fib
mov r7, #1 @ exit(r0)
svc #0
fib: @ r0 = n -> r0 = fib(n)
cmp r0, #1
bxle lr @ n <= 1: r0 já é a resposta (0 ou 1)
mov r1, #0 @ a = fib(0)
mov r2, #1 @ b = fib(1)
mov r3, r0 @ contador = n
1: add r12, r1, r2 @ next = a + b
mov r1, r2 @ a = b
mov r2, r12 @ b = next
subs r3, r3, #1
bne 1b @ repete n vezes
mov r0, r1 @ resultado = a
bx lrMontando, ligando e rodando:
arm-none-eabi-as -march=armv7-a -o fib.o fib.s
arm-none-eabi-ld -Ttext=0x10000 -o fib.elf fib.o
java -jar target/armbox.jar --arch=armv7a fib.elf ; echo $?55fib(10) = 55: o arm-box confirma que o laço está certo. Vale conferir o binário
montado contra o fonte — é o objdump -d fib.elf, mostrando endereço, bytes da
instrução e o mnemônico que o arm-none-eabi-as gerou a partir do que escrevemos:
Três pontos que não aparecem só lendo o fonte:
bxle lré condicional, não umifescondido.BXaceita o mesmo sufixo de condição de qualquer instrução ARM (o campocondque ocupa os 4 bits mais altos da palavra). Um nibble é meio byte — 4 bits, exatamente o tamanho de um dígito hexadecimal — então dá pra ler o campoconddireto no primeiro dígito hex da instrução, sem converter nada pra binário: emd12fff1e, o primeiro dígito,d, é oconde codificaLE. Ou seja: essa linha só desvia quando a comparação anterior (cmp r0, #1) deu "menor ou igual". Quandon <= 1, ela substitui umif (n <= 1) return n;inteiro sem nenhumb(branch) de verdade.- O laço depende de
subs, não desub. É oSno fim que faz a subtração atualizar as flags (Z, N, C, V); é essa flag Z que obne(branch se Z=0) lê logo depois para decidir se repete. Trocar porsubsemSquebra o laço — é exatamente o bug do exercício mais abaixo. 1bno fonte é um rótulo numérico local: "o1:mais próximo para trás" (por isso o disassembly mostra o endereço real,10024, no lugar do1bsimbólico). Útil em laços curtos sem precisar inventar um nome só para eles.- r12 (
ip) é o único registrador que a AAPCS reserva como scratch garantido — dá pra usar como temporário (aqui, para guardara + b) sem se preocupar em salvar o valor antigo.
max(a, b) — sem um único desvio
Depois de um laço, o segundo exemplo é o oposto: uma função sem nenhum laço e sem
nenhum desvio condicional, mesmo resolvendo um if. max(a, b) devolve o maior dos
dois argumentos — o tipo de função tão simples que o compilador normalmente reduz a
um cmp + jl + mov em x86. Em ARM dá pra fazer melhor, e o motivo é o mesmo do
bxle acima: qualquer instrução pode ser condicional.
_start:
mov r0, #17
mov r1, #42
bl max
mov r7, #1
svc #0
max: @ r0 = a, r1 = b -> r0 = maior
cmp r0, r1
movlt r0, r1 @ se a < b: r0 = b (senão mantém a)
bx lrjava -jar target/armbox.jar --arch=armv7a max.elf ; echo $?42Desmontado:
Três instruções, e a segunda repete o mesmo truque do bxle de cima: em b1a00001, o
primeiro dígito hex, b, é o campo cond e codifica LT. Ou seja, movlt r0, r1 só
copia r1 pra r0 se a última cmp deu "menor" — nas outras vezes é um nop disfarçado,
r0 fica com o valor que já tinha (a).
Em x86 isso seria um cmp + jl + mov + label — um desvio de verdade, que o preditor de
desvios da CPU pode errar (e pagar o preço de esvaziar o pipeline). No ARM não existe desvio
nenhum aqui: movlt é uma instrução comum de mover registrador, só que com um bit de condição
embutido nela. Essa é a predicação da qual o artigo de flags fala, aqui pagando o aluguel.
O compilador faz o mesmo: no artigo de C, o
-O2transformou umifnum parcmp/movXXsempre que o corpo cabia numa instrução.
Exercício
Quebre de propósito e veja o arm-box reportar:
- Troque
movltpormovgtemmax— o programa passa a devolver o menor. - Em
fib, esqueça osubs(deixesub r3, r3, #1, sem oS): obnepassa a olhar uma flag velha e o laço nunca termina. Oarm-boxroda até o limite e você percebe o hang. - Passe
n = 13emfib:fib(13) = 233, ainda cabe em 8 bits.n = 14dá377, que satura o exit code em377 & 0xff = 121— um bug clássico de "resultado maior que o canal de saída".
Glossário
| Termo | O que é |
|---|---|
| Folha (leaf) | função que não chama nenhuma outra; não precisa salvar lr |
bl / bx lr | chamar (grava retorno em lr) / voltar |
| Rótulo numérico local | 1: … 1b (back) / 1f (forward) — alvo anônimo reutilizável |
| Predicação | executar (ou não) uma instrução conforme as flags, sem desviar |
ip (r12) | registrador scratch da AAPCS |
Comandos
| Comando | O que faz |
|---|---|
arm-none-eabi-as -march=... -o x.o x.s | monta |
arm-none-eabi-ld -Ttext=ADDR -o x.elf x.o | liga com o código em ADDR |
java -jar target/armbox.jar --arch=armv7a x.elf ; echo $? | roda e mostra o exit code |
java -jar target/armbox.jar --gdb=3333 x.elf | roda sob GDB para inspecionar |
Próximo passo
Você montou com as e ligou com ld sem pensar muito no -Ttext. O próximo artigo abre o processo de montagem e ligação: o que o linker faz, o linker script e o mapa de memória.
Trilha: Curso de Arquitetura ARM · Módulo 3, lição 1. Ver também: Carga e armazenamento no ARM, Sub-rotinas em ARM.