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
BLfaz duas coisas: desvia paraadd2e guarda o endereço de retorno emLR(r14) — oPC+4da instrução seguinte, para onde obx lrvai voltar. - O alvo usa a mesma matemática do
b:(PC + 8) + (offset << 2). Na instrução em0x8020,PC+8 = 0x8028; o offset0xfffff6é −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) é olr— dá para ver no último nibblee=1110= r14. - O
BXtambé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 lrbastaria. Empilhamos olrabaixo 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..0formam uma máscara de 16 bits, um por registradorr0..r15.0x4010acende o bit 4 (r4) e o bit 14 (lr) — exatamente o que empilhamos. Rn = sp(r13) e o!(writeback) atualiza ospsozinho: opushdecrementa a pilha e opopa incrementa (a pilha do ARM cresce para baixo).- Por que empilhar
r4? Porquer4é "callee-saved" — quem usa deve devolver do jeito que achou. Olrtambé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:
| Registrador | Papel |
|---|---|
r0–r3 | Argumentos de entrada (e r0 leva o valor de retorno). Sobram de graça — sem pilha. |
r4–r11 | Callee-saved: a função pode usar, mas precisa restaurar antes de voltar (via pilha). |
r12 | ip — temporário intra-procedimento (pode ser mexido). |
r13 | sp — ponteiro da pilha. |
r14 | lr — link register (retorno do BL). |
r15 | pc — 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
| Instrução / Comando | O que faz |
|---|---|
bl alvo | Desvia e salva o retorno em lr (chamada de função). |
bx reg | Copia 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.elf | Mostra os bytes das chamadas para decodificar. |
info registers lr | No 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.