O mapa da memória
Todo processo tem um espaço de endereçamento próprio dividido em segmentos com funções distintas. Entender esse mapa é entender por que stack overflow acontece, por que malloc existe e por que ponteiros podem ser perigosos.
Quatro zonas com regras diferentes
Quando o sistema operacional carrega um programa, ele não joga tudo numa área contínua desordenada. Ele divide o espaço de endereçamento em segmentos, cada um com um propósito e um comportamento de crescimento próprios.
O segmento de código (texto) guarda as instruções compiladas — é somente leitura para evitar que o programa modifique a si mesmo em runtime. O segmento de dados guarda variáveis globais e estáticas, inicializadas antes de main() ser chamado. O heap serve para memória alocada dinamicamente (malloc/new): cresce sob demanda, o tamanho só é conhecido em runtime, e você gerencia o ciclo de vida. A stack empilha automaticamente os frames de cada chamada de função e os desempilha ao retornar.
Acompanhe um programa passo a passo
Avance pelo ciclo de vida de um processo e veja como cada instrução afeta os segmentos: variáveis locais na stack, malloc no heap, free devolvendo memória e o dangling pointer que fica para trás.
Os quatro segmentos
// Espaço de endereçamento de um processo (Linux/64-bit, simplificado) // Endereço alto ───────────────────────────────────────── // Kernel (mapeado mas inacessível ao programa) // Stack ← cresce para baixo (frames de função) // (espaço livre — stack e heap podem crescer aqui) // Heap → cresce para cima (malloc / new) // BSS variáveis globais não-inicializadas (= 0) // Data variáveis globais inicializadas // Código (text) instruções — somente leitura // Endereço baixo ───────────────────────────────────────── // Em C: int global = 10; // segmento Data int nao_init; // segmento BSS (= 0) int main() { int local = 20; // stack — liberado ao retornar int* p = malloc(16); // heap — persiste até free() free(p); return 0; }
Stack vs. Heap: quando usar cada um
// Stack: tamanho determinado em tempo de compilação int arr[100]; // ✓ tamanho constante → stack OK int arr[n]; // ✗ VLA — evitar; comportamento perigoso em C // Heap: tamanho determinado em tempo de execução int* arr = malloc(n * sizeof(int)); // ✓ n só conhecido em runtime // Limite da stack: tipicamente 1–8 MB (configurável com ulimit) // Limite do heap : memória disponível no sistema (GB) // Stack overflow: ocorre quando a recursão é muito profunda // ou quando uma variável local é muito grande para caber na stack char buf[10000000]; // 10 MB na stack → provável SIGSEGV
Explorando a memória do processo
Mini projeto: em C, declare variáveis em cada segmento (global inicializada, global não-inicializada, local em main, malloc) e imprima o endereço de cada uma com %p. Confirme que os endereços seguem o layout esperado: código menor, dados, heap crescendo para cima, stack com endereço bem mais alto.
Projeto principal: escreva um programa em C que aloca e libera memória em loop, e use o Valgrind ou o htop para observar o consumo de memória. Compare: (1) free() em cada iteração, (2) sem free() — memory leak. Meça RSS (Resident Set Size) ao longo do tempo e plote um gráfico.
Desafio extra: leia o arquivo /proc/<pid>/maps no Linux com o seu programa rodando em background. Identifique cada segmento na saída e relacione com o layout estudado. Quanto de memória virtual vs. física (RSS) o processo usa? Por que podem ser diferentes?
Teste sua intuição
int x = 5;?
Onde você encontra isso
Motores de jogos
Engines como Unreal e Unity gerenciam manualmente pools de memória no heap para evitar a fragmentação e os picos de latência que o GC genérico causaria em meio a um frame de jogo. Eles pré-alocam blocos grandes e sub-alocam objetos dentro deles, controlando o layout exato do heap.
Segurança — buffer overflow
A maioria dos exploits históricos (Morris Worm, Heartbleed, Stagefright) explorava confusão entre segmentos: escrever além do fim de um buffer na stack para sobrescrever o endereço de retorno e redirecionar a execução. Stack canaries, NX bit e ASLR são defesas diretas contra isso.
Rust e ownership
O sistema de ownership do Rust garante em tempo de compilação que cada valor na heap tem exatamente um dono, e que ele é liberado quando o dono sai de escopo — sem GC, sem dangling pointer. É a resposta moderna à pergunta "como ter heap segura sem coletor de lixo?".