← Voltar aos artigos

Do C ao binário ARM: gcc + objdump

A função

soma.c — soma de 1 a n:

int soma_ate(int n) {
    int acc = 0;
    for (int i = 1; i <= n; i++) {
        acc += i;
    }
    return acc;
}

Compilando para um objeto ARM (sem ligar, -c), alvo ARMv4T em estado ARM:

arm-none-eabi-gcc -c -O0 -march=armv4t -marm soma.c -o soma-O0.o
arm-none-eabi-gcc -c -O2 -march=armv4t -marm soma.c -o soma-O2.o
arm-none-eabi-objdump -d soma-O0.o

-O0: a tradução literal

00000000<soma_ate>:
0:e52db004push{fp}
4:e28db000addfp, sp, #0
8:e24dd014subsp, sp, #20@ abre o frame (20 bytes)
c:e50b0010strr0, [fp, #-16]@ salva o argumento n na pilha
10:e3a03000movr3, #0
14:e50b3008strr3, [fp, #-8]@ acc = 0
18:e3a03001movr3, #1
1c:e50b300cstrr3, [fp, #-12]@ i = 1
20:ea000006b40 <soma_ate+0x40>@ pula pro teste do for
24:e51b2008ldrr2, [fp, #-8]@ corpo: carrega acc
28:e51b300cldrr3, [fp, #-12]@ carrega i
2c:e0823003addr3, r2, r3
30:e50b3008strr3, [fp, #-8]@ acc += i
34:e51b300cldrr3, [fp, #-12]
38:e2833001addr3, r3, #1
3c:e50b300cstrr3, [fp, #-12]@ i++
40:e51b200cldrr2, [fp, #-12]@ teste: carrega i
44:e51b3010ldrr3, [fp, #-16]@ carrega n
48:e1520003cmpr2, r3
4c:dafffff4ble24 <soma_ate+0x24>@ while (i <= n)
50:e51b3008ldrr3, [fp, #-8]
54:e1a00003movr0, r3@ valor de retorno em r0
58:e28bd000addsp, fp, #0
5c:e49db004pop{fp}@ fecha o frame
60:e12fff1ebxlr

Cada variável (n, acc, i) mora num slot da pilha, referenciado por [fp, #offset]. Toda operação faz ldr → conta → str. É lento, mas é uma tradução direta do C — ideal para depurar. Repare no prólogo (push {fp} / sub sp) e no epílogo (pop {fp} / bx lr): a convenção de chamada AAPCS, assunto do artigo de sub-rotinas.

-O2: o mesmo resultado, outro caminho

00000000<soma_ate>:
0:e2502000subsr2, r0, #0@ r2 = n; testa n
4:da000007ble28 <...+0x28>@ n <= 0? retorna 0
8:e3a03001movr3, #1@ i = 1
c:e3a00000movr0, #0@ acc = 0 (já no reg de retorno)
10:e0822003addr2, r2, r3@ r2 = n + 1 (limite do laço)
14:e0800003addr0, r0, r3@ acc += i
18:e2833001addr3, r3, #1@ i++
1c:e1530002cmpr3, r2
20:1afffffbbne14 <...+0x14>@ while (i != n+1)
24:e12fff1ebxlr
28:e3a00000movr0, #0
2c:e12fff1ebxlr

Sem pilha, sem fp, tudo em registrador. Três transformações que valem notar:

  1. Nada de frame. A função não chama ninguém e cabe em registradores scratch (r0–r3), então não há prólogo/epílogo.
  2. acc nasce em r0. O compilador sabe que o retorno vai em r0 (AAPCS) e acumula direto lá.
  3. cmp i, n virou cmp i, n+1 + bne. Em vez de "i <= n" (que precisa de ble, um teste com sinal), o -O2 pré-calcula n+1 uma vez e testa i != n+1. bne é mais barato e o limite é loop-invariant.

Mesma semântica, metade das instruções. É a mesma máquina que o decoder do arm-jitter vai processar — o emulador não sabe nem se importa de onde o binário veio.

As seções do objeto

arm-none-eabi-objdump -h soma-O2.o:

Idx Name          Size      VMA       LMA       File off  Algn
  0 .text         00000030  ...  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
  1 .data         00000000  ...  CONTENTS, ALLOC, LOAD, DATA
  2 .bss          00000000  ...  ALLOC
  3 .comment      00000019  ...  CONTENTS, READONLY
SeçãoO que guardaNo arquivo?
.textcódigo executávelsim
.rodataconstantes só-leitura (strings, tabelas const)sim
.datavariáveis globais inicializadas com valor ≠ 0sim
.bssvariáveis globais zeradas (ou = 0)não — só o tamanho; o loader zera
.commentmetadados do compilador; descartávelsim, mas não carrega

soma_ate não tem dados globais, então .data e .bss estão vazios. VMA (Virtual Memory Address) ainda é 0 porque este é um objeto não ligado — o arm-none-eabi-ld atribui os endereços finais, assunto do próximo artigo.

Símbolos e endereços

arm-none-eabi-nm soma-O2.o:

00000000 T soma_ate

T = símbolo em .text, global (maiúscula). nm minúsculo (t, d, b) seria local. Um U seria um símbolo indefinido — uma função que este objeto usa mas não define, a ser resolvida na ligação.

Glossário

TermoO que é
Objeto (.o)código compilado mas ainda não ligado; endereços relativos
-O0 / -O2nível de otimização: literal / agressivo
Prólogo / epílogoas instruções que abrem/fecham o stack frame de uma função
Frame pointer (fp, r11)âncora fixa para acessar variáveis locais e argumentos na pilha
.bssdados zerados — ocupam RAM mas não espaço no arquivo
VMAendereço onde a seção vai rodar (definido na ligação)

Comandos

ComandoO que faz
arm-none-eabi-gcc -c -Ox -march=armv4t soma.ccompila para objeto sem ligar
arm-none-eabi-gcc -S soma.cpara no assembly (soma.s), sem montar
arm-none-eabi-objdump -d x.odesmonta .text
arm-none-eabi-objdump -h x.olista as seções e seus tamanhos
arm-none-eabi-nm x.olista os símbolos (T/t/D/U/...)
arm-none-eabi-readelf -a x.elftudo sobre o ELF (headers, seções, símbolos)

Próximo passo

Você tem o binário. O resto do módulo 2 decodifica ele à mão — Thumb agora, e depois abre o decoder real do arm-jitter.

Trilha: Curso de Arquitetura ARM · Módulo 2, lição 1. Ver também: Decodificando instruções ARM à mão.