← Voltar aos artigos

Sub-rotinas em ARM: BL/BX, pilha e convenção de chamada

No artigo anterior a gente decodificou instruções num laço linear. Programa de verdade tem funções: um trecho de código que roda, faz um cálculo e volta. Chamar função no ARM traz três problemas novos — como lembrar onde voltar, como não perder registradores no meio do caminho e como passar argumentos. É o que este texto resolve: BL, BX, a pilha e a convenção de chamada.

O exemplo: chamar uma função

Uma função add2(a, b) que soma dois inteiros e é chamada pelo _start. Montamos com arm-none-eabi-as -march=armv5te e desmontamos — os bytes abaixo são reais:

00008000<add2>:
8000:e92d4010push{r4, lr}
8004:e1a04000movr4, r0
8008:e0844001addr4, r4, r1
800c:e1a00004movr0, r4
8010:e8bd4010pop{r4, lr}
8014:e12fff1ebxlr
00008018<_start>:
8018:e3a00003movr0, #3
801c:e3a01004movr1, #4
8020:ebfffff6bl8000 <add2>
8024:e3a07001movr7, #1
8028:ef000000svc0x00000000

O _start coloca 3 em r0 e 4 em r1, chama add2 e, quando volta, o resultado (7) já está em r0. Vamos abrir cada peça.

BL: o desvio que se lembra de voltar

bl 8000 = ebfffff6. É da família dos desvios (bits [27:26] = 10), mas com o bit de link ligado:

ebfffff6 = bl add2
  • O BL faz duas coisas: desvia para add2e guarda o endereço de retorno em LR (r14) — o PC+4 da instrução seguinte, para onde o bx lr vai voltar.
  • O alvo usa a mesma matemática do b: (PC + 8) + (offset << 2). Na instrução em 0x8020, PC+8 = 0x8028; o offset 0xfffff6 é −10 em 24 bits com sinal, −10 << 2 = −40; 0x8028 − 0x28 = 0x8000 = add2. Bateu.
  • É por isso que funções não "sabem" quem as chamou: o retorno vem do LR, não de um endereço fixo.

BX: a volta (e o truque do Thumb)

bx lr = e12fff1e copia LR para o PC e encerra a função:

e12fff1e = bx lr
  • O Rm (registrador de destino) é o lr — dá para ver no último nibble e = 1110 = r14.
  • O BX também faz interworking: o bit 0 do endereço diz se a volta é em ARM (0) ou Thumb (1). Nosso binário é ARM puro, então o bit 0 é 0. É o que permite misturar ARM e Thumb no mesmo programa — assunto do próximo próximo texto.
  • (Numa folha simples, bx lr bastaria. Empilhamos o lr abaixo para mostrar a pilha.)

A pilha: PUSH e POP

push {r4, lr} = e92d4010 e pop {r4, lr} = e8bd4010. O assembler traduz esses mnemônicos amigáveis para store/load múltiplo com a pilha: stmdb sp!, {r4, lr} e ldmia sp!, {r4, lr}.

e92d4010 = push {r4, lr}
  • Os bits 15..0 formam uma máscara de 16 bits, um por registrador r0..r15. 0x4010 acende o bit 4 (r4) e o bit 14 (lr) — exatamente o que empilhamos.
  • Rn = sp (r13) e o ! (writeback) atualiza o sp sozinho: o pushdecrementa a pilha e o pop a incrementa (a pilha do ARM cresce para baixo).
  • Por que empilhar r4? Porque r4 é "callee-saved" — quem usa deve devolver do jeito que achou. O lr também vai pra pilha para suportar função que chama outra (aninhamento).

Glossário: papéis dos registradores (AAPCS)

A AAPCS (ARM Architecture Procedure Call Standard) é o contrato que C, assembly e o arm-box respeitam para conversarem. Resumo:

Papéis dos registradores na chamada (AAPCS)
RegistradorPapel
r0–r3Argumentos de entrada (e r0 leva o valor de retorno). Sobram de graça — sem pilha.
r4–r11Callee-saved: a função pode usar, mas precisa restaurar antes de voltar (via pilha).
r12ip — temporário intra-procedimento (pode ser mexido).
r13sp — ponteiro da pilha.
r14lr — link register (retorno do BL).
r15pc — program counter.

No nosso exemplo: argumentos em r0,r1, retorno em r0, e r4 (callee-saved) foi salvo/restaurado com push/pop. É a AAPCS aparecendo em assembly puro.

Glossário: instruções e comandos de chamada

Comandos e instruções de sub-rotina
Instrução / ComandoO que faz
bl alvoDesvia e salva o retorno em lr (chamada de função).
bx regCopia reg para pc; bit 0 escolhe ARM/Thumb (retorno).
push {regs}Empilha registradores (alias de stmdb sp!).
pop {regs}Desempilha (alias de ldmia sp!).
arm-none-eabi-objdump -d f.elfMostra os bytes das chamadas para decodificar.
info registers lrNo GDB: inspecta o endereço de retorno salvo.

O que o arm-jitter faz com isso

Quando o arm-box executa o bl, o arm-jitter escreve o PC+4 no LR do core emulado e pula; no bx lr, copia LR para PC. A pilha mexe no sp exatamente como os bits writeback mandam. É a mesma codificação que decodificamos — só que agora com o contrato da AAPCS por cima. É por isso que o busybox estático roda no arm-box: cada chamada de libc segue essas regras, e o emulador só precisa obedecer ao que os bits dizem.

Próximo passo

Até aqui todas as instruções tinham 32 bits fixos. O próximo texto quebra essa regra de vez: o Thumb e o Thumb-2, onde instruções têm 16 ou 32 bits e o decodificador precisa ler o tamanho antes do opcode — e onde o BX e o bit 0 viram essenciais para trocar de modo. Até lá, tente escrever uma função sua que receba 3 argumentos e veja onde o 4º (se houvesse) teria que ir: na pilha.