Anatomia de um executável: ELF e segmentos de memória
1. Introdução ao formato ELF
O formato ELF (Executable and Linkable Format) é o padrão para executáveis, bibliotecas compartilhadas e arquivos objeto no Linux e na maioria dos sistemas Unix modernos. Quando você compila um programa em C com gcc, o compilador gera código objeto (.o), o linker combina esses objetos e produz um arquivo ELF executável. O ELF substituiu formatos mais antigos como a.out e COFF, oferecendo maior flexibilidade e suporte a arquiteturas diversas.
A estrutura do ELF é composta por três partes principais: o cabeçalho ELF (ELF header), que descreve o arquivo; as seções (sections), que organizam o conteúdo gerado pelo compilador; e os segmentos (segments), que definem como o carregador do sistema operacional deve mapear o executável na memória.
2. Cabeçalho ELF: o mapa do executável
O cabeçalho ELF é o ponto de partida para interpretar qualquer arquivo ELF. Ele contém informações essenciais:
- Magic number: os primeiros 4 bytes
7f 45 4c 46(\x7fELF) - Classe: 32 bits (
1) ou 64 bits (2) - Endianness: little-endian (
1) ou big-endian (2) - Tipo: relocatable (
ET_REL), executable (ET_EXEC), shared (ET_DYN) - Arquitetura: ISA alvo (x86, ARM, RISC-V)
- Entry point: endereço da primeira instrução a executar
Para inspecionar o cabeçalho, use readelf -h:
$ readelf -h programa
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Entry point address: 0x401060
Em C, podemos interpretar esse cabeçalho usando a estrutura Elf64_Ehdr:
#include <elf.h>
#include <stdio.h>
int main() {
FILE *f = fopen("programa", "rb");
Elf64_Ehdr ehdr;
fread(&ehdr, sizeof(ehdr), 1, f);
printf("Entry point: 0x%lx\n", ehdr.e_entry);
printf("Type: %d\n", ehdr.e_type); // 2 = ET_EXEC
fclose(f);
return 0;
}
3. Seções do ELF: o que o compilador gera
O compilador C organiza o código e os dados em seções dentro do arquivo objeto. As principais seções são:
.text: código executável da máquina (instruções).data: variáveis globais e estáticas inicializadas.bss: variáveis globais e estáticas não inicializadas (ocupa espaço apenas na memória, não no arquivo).rodata: dados somente leitura (strings literais, constantesconst).symtab: tabela de símbolos (nomes de funções e variáveis).debug: informações de depuração (gerado com-g)
Para listar todas as seções:
$ readelf -S programa
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[13] .text PROGBITS 0000000000401060 00001060
0000000000000190 0000000000000000 AX 0 0 16
[24] .data PROGBITS 0000000000404018 00003018
0000000000000010 0000000000000000 WA 0 0 8
[25] .bss NOBITS 0000000000404028 00003028
0000000000000008 0000000000000000 WA 0 0 8
As flags indicam permissões: A (alocável), X (executável), W (gravável).
4. Segmentos de memória: a visão em tempo de execução
Enquanto as seções são uma visão do linker, os segmentos (program headers) são a visão do carregador do kernel. O linker mapeia seções em segmentos para que o sistema operacional possa carregar o programa de forma eficiente.
Segmentos típicos:
- LOAD: segmento carregável na memória (código ou dados)
- INTERP: caminho do interpretador (para bibliotecas dinâmicas)
- DYNAMIC: informações de ligação dinâmica
- NOTE: metadados diversos
$ readelf -l programa
Elf file type is EXEC (Executable file)
Entry point 0x401060
There are 2 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x0000000000000600 0x0000000000000600 R E 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000200 0x0000000000000300 RW 0x1000
O primeiro LOAD (R E) contém o cabeçalho e o código (.text). O segundo LOAD (RW) contém dados inicializados e não inicializados (.data e .bss).
5. Layout da memória de um processo C
Quando o kernel carrega um executável, ele organiza a memória do processo da seguinte forma (endereços baixos para altos):
- Segmento de texto (
.text): código executável, permissão RX - Segmento de dados (
.data+.bss): variáveis globais, permissão RW - Heap: alocações dinâmicas (
malloc,calloc), cresce para cima - Bibliotecas compartilhadas: mapeadas em endereços aleatórios (ASLR)
- Pilha (stack): variáveis locais, cresce para baixo
Para visualizar o layout de um processo em execução:
$ pmap $(pgrep meu_programa)
Address Kbytes RSS Dirty Mode Mapping
0000555555554000 4 4 0 r-x-- meu_programa
0000555555555000 4 4 4 r---- meu_programa
0000555555556000 4 4 4 rw--- meu_programa
00007ffff7dce000 2040 1024 0 r-x-- libc.so.6
00007ffff7fce000 2040 0 0 ----- libc.so.6
00007ffff81ce000 8 8 8 rw--- libc.so.6
00007ffffffde000 132 12 12 rw--- [stack]
6. Variáveis em C e sua localização nos segmentos
Cada tipo de variável em C é alocada em um segmento específico:
#include <stdio.h>
#include <stdlib.h>
int global_inic = 42; // .data
int global_nao_inic; // .bss
static int estatica = 10; // .data
static int estatica_nao_inic; // .bss
const int constante = 100; // .rodata (às vezes .data se modificável)
int main() {
int local = 5; // pilha (stack)
static int local_estatica; // .bss
int *heap = malloc(10); // heap
printf("global_inic: %p\n", &global_inic);
printf("global_nao_inic: %p\n", &global_nao_inic);
printf("local: %p\n", &local);
printf("heap: %p\n", heap);
free(heap);
return 0;
}
Compilando e inspecionando:
$ gcc -o programa programa.c
$ size programa
text data bss dec hex filename
1120 544 16 1680 690 programa
$ nm programa | grep -E "global|estatica|constante"
0000000000404020 D global_inic
0000000000404028 B global_nao_inic
0000000000404010 D estatica
0000000000404030 b estatica_nao_inic
0000000000404008 R constante
As letras indicam: D (dados inicializados), B (BSS), R (read-only), b (BSS local).
7. Ferramentas para inspecionar o executável
Além de readelf, outras ferramentas são indispensáveis:
objdump: desmonta o código e mostra seções detalhadas
$ objdump -d programa | head -20
programa: file format elf64-x86-64
Disassembly of section .text:
0000000000401060 <main>:
401060: 55 push %rbp
401061: 48 89 e5 mov %rsp,%rbp
401064: 48 83 ec 10 sub $0x10,%rsp
nm: lista símbolos (funções e variáveis) com seus endereços
$ nm programa | grep -E "T|U"
0000000000401060 T main
U printf@@GLIBC_2.2.5
size: exibe o tamanho das seções texto, dados e BSS
$ size programa
text data bss dec hex filename
1120 544 16 1680 690 programa
Exemplo prático completo:
$ cat teste.c
#include <stdio.h>
int x = 10;
int y;
int main() { return 0; }
$ gcc -o teste teste.c
$ readelf -S teste | grep -E "\.text|\.data|\.bss"
[13] .text PROGBITS 0000000000401060 00001060
[24] .data PROGBITS 0000000000404018 00003018
[25] .bss NOBITS 0000000000404028 00003028
$ nm teste | grep -E "x|y|main"
0000000000401060 T main
0000000000404018 D x
0000000000404028 B y
8. Considerações finais e implicações práticas
Entender a anatomia do ELF e os segmentos de memória tem implicações diretas no desenvolvimento em C:
- Desempenho: o alinhamento de segmentos (múltiplos de 0x1000, páginas de 4KB) afeta o uso de cache e TLB. Dados muito próximos em segmentos diferentes podem causar cache misses.
- Segurança: o bit NX (No-Execute) impede que segmentos de dados sejam executados, mitigando ataques de buffer overflow. O ASLR (Address Space Layout Randomization) randomiza endereços de segmentos para dificultar exploração de vulnerabilidades.
- Tamanho do binário: entender a diferença entre
.datae.bssajuda a reduzir o tamanho do executável. Variáveis não inicializadas ocupam espaço apenas na memória, não no disco. - Linker scripts: personalizar o layout de memória com scripts de linker permite otimizações avançadas para sistemas embarcados.
Ao compilar com gcc -O2 -fstack-protector-strong, você ativa proteções que interagem diretamente com esses segmentos. Dominar esses conceitos é fundamental para programação de sistemas, depuração de baixo nível e segurança de software.
Referências
- ELF - OSDev Wiki — Documentação técnica completa sobre o formato ELF, incluindo estruturas de dados e exemplos de parsing
- readelf(1) - Linux manual page — Página manual oficial do comando readelf com todas as opções de análise
- Executable and Linkable Format (ELF) Specification — Especificação oficial do formato ELF publicada pela Linux Foundation
- Memory Layout of C Programs - GeeksforGeeks — Tutorial prático sobre o layout de memória de programas C com exemplos de código
- Anatomy of a Program in Memory — Artigo técnico aprofundado sobre segmentos de memória, heap, stack e mapeamento ELF
- Understanding the ELF Format - Linux Journal — Série clássica de artigos sobre o formato ELF, com exemplos de inspeção manual
- Linker Scripts - GNU LD Documentation — Documentação oficial sobre scripts de linker para controle fino do layout de seções e segmentos