← Voltar aos artigosSub-rotinas em ARM: BL/BX, pilha e convenção de chamada

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: e92d4010  push  {r4, lr}
    8004: e1a04000  mov   r4, r0
    8008: e0844001  add   r4, r4, r1
    800c: e1a00004  mov   r0, r4
    8010: e8bd4010  pop   {r4, lr}
    8014: e12fff1e  bx    lr

00008018 <_start>:
    8018: e3a00003  mov   r0, #3
    801c: e3a01004  mov   r1, #4
    8020: ebfffff6  bl    8000 <add2>
    8024: e3a07001  mov   r7, #1
    8028: ef000000  svc   0x00000000

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
cond=1110 (AL) | 10 (desvio) | L=1 (BL: salva retorno) | offset = 0xfffff6
  • 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
cond=1110 (AL) | família especial de branch-exchange | Rm = lr (r14) nos bits 3..0 = 1110
  • 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)
cond=1110 (AL) | 10 (transferência múltipla) | Rn = sp (13) | writeback '!'
lista de registradores (bits 15..0) = 0x4010 = bits 4 (r4) e 14 (lr) setados
  • 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.