Trilha 06 · Memória e execução

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.

① Intuição

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.

Por que isso importa? Stack overflow, memory leak, segmentation fault e dangling pointer são todos consequências diretas de violar as regras de um desses segmentos. Com esse mapa na cabeça, os erros deixam de ser mágica e passam a ter localização exata.
② Visualização interativa

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.

ESPAÇO DE ENDEREÇAMENTO
Código
instruções (somente leitura)
Dados
globais e estáticos
Heap ↓
↕ espaço livre
Stack ↑
Passo 1: Programa carregado. Os segmentos de código e dados já estão na memória. Stack e heap ainda vazios.
Código
Instruções. Tamanho fixo. Somente leitura em runtime.
Dados
Globais e estáticos. Inicializados antes de main().
Heap ↓
malloc/new. Cresce para baixo. Gerenciado pelo programador.
Stack ↑
Frames de função. Cresce para cima. Automático.
1/7
③ Explicação técnica

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
ASLR — Address Space Layout Randomization: sistemas operacionais modernos randomizam os endereços base de cada segmento a cada execução. Isso dificulta ataques de exploração (como buffer overflow com endereço fixo) mas não muda a estrutura lógica dos segmentos — eles ainda existem nas mesmas relações uns com os outros.
④ Projeto para programar

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?

⑤ Exercícios rápidos

Teste sua intuição

Onde fica uma variável local declarada dentro de main() como int x = 5;?
Em arquiteturas x86-64 com o layout convencional, qual é a direção de crescimento de stack e heap?
Por que um memory leak é um problema mesmo que o sistema operacional limpe a memória ao fim do processo?
⑥ Aplicações no mundo real

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?".

← Trilha: Memória e execução Próxima: A pilha de chamadas →