Um ponteiro é um valor que contém um endereço de memória — nada mais, nada menos. Esta simplicidade é ao mesmo tempo o que torna C poderoso e o que torna C perigoso: com acesso directo a qualquer endereço de memória, um erro de programação pode corromper dados arbitrários, executar código injectado, ou expor informação sensível. Perceber ponteiros é perceber porque buffer overflows existem, porque use-after-free é explorável, e o que a JVM sacrifica em troca de eliminar estas classes de bugs.
Toda a variável em C ocupa um endereço de memória. Um ponteiro é uma variável que guarda esse endereço — em vez de guardar um valor directamente, guarda a localização onde o valor está. Em sistemas 64-bit, todos os ponteiros têm 8 bytes, independentemente do tipo apontado.
// Declaração e uso básico de ponteiros:
int x = 42; // variável inteira — valor 42, algures na memória
int* p = &x; // ponteiro para int — guarda o endereço de x
// &x = "endereço de x" = ex: 0x00007FFE_A3B2C1D0
// Os três operadores fundamentais:
// & — address-of: obtém o endereço de uma variável
// * — dereference: acede ao valor no endereço guardado pelo ponteiro
// [] — indexação: açúcar sintáctico para aritmética de ponteiros
printf("%p\n", p); // imprime o endereço: 0x7ffe a3b2c1d0
printf("%d\n", *p); // dereferencia: imprime o valor em p → 42
printf("%d\n", x); // acesso directo → 42 (mesmo valor)
*p = 99; // escreve 99 no endereço guardado por p
printf("%d\n", x); // → 99 — x foi modificado através do ponteiro
// Tamanho de um ponteiro:
printf("%zu\n", sizeof(int*)); // → 8 bytes (sistema 64-bit)
printf("%zu\n", sizeof(char*)); // → 8 bytes (todos os ponteiros têm 8 bytes em 64-bit)
printf("%zu\n", sizeof(void*)); // → 8 bytes
// Declaração — a posição do * é irrelevante para o compilador mas convencional:
int* p1; // estilo C++ — o * "pertence ao tipo"
int *p2; // estilo C — o * "pertence à variável"
// int* a, b; — ATENÇÃO: apenas a é ponteiro, b é int normal
// int *a, *b; — ambos são ponteiros
Em C, é possível fazer aritmética sobre ponteiros — somar ou subtrair inteiros para avançar ou recuar na memória. A aritmética é automaticamente escalada pelo tamanho do tipo apontado. Esta funcionalidade é a base de como arrays funcionam em C — e a fonte de uma classe inteira de vulnerabilidades quando os limites não são verificados.
// Arrays em C são ponteiros para o primeiro elemento:
int arr[5] = {10, 20, 30, 40, 50};
// arr[0]=10 arr[1]=20 arr[2]=30 arr[3]=40 arr[4]=50
// endereço: +0 +4 +8 +12 +16 (bytes, sizeof(int)=4)
int* p = arr; // p aponta para arr[0]
// Indexação e aritmética são equivalentes:
arr[2] == *(arr + 2) // → 30 — notação de array = aritmética de ponteiro
p[2] == *(p + 2) // → 30 — mesma coisa
// p + 1 não avança 1 byte — avança sizeof(int) = 4 bytes
// p + 2 avança 8 bytes — aponta para arr[2]
// Percorrer um array com aritmética de ponteiros:
int* end = arr + 5; // ponteiro "past the end" — endereço após o último elemento
for (int* it = arr; it != end; it++) {
printf("%d ", *it); // dereferencia cada posição
}
// → 10 20 30 40 50
// O que acontece sem verificação de limites — out-of-bounds:
printf("%d\n", arr[5]); // ❌ undefined behaviour — acesso fora do array
printf("%d\n", arr[10]); // ❌ undefined behaviour — pode ler memória de outra variável,
// retornar lixo, ou causar segmentation fault
// Em Java: ArrayIndexOutOfBoundsException — determinístico
// Em C: comportamento indefinido — silencioso e perigoso
Em C, o programador controla explicitamente onde cada dado é alocado.
A stack é gerida automaticamente pelo compilador —
rápida, tamanho fixo por frame. O heap é gerido
manualmente — malloc para alocar, free
para libertar. Esquecer o free é um memory leak;
usar memória depois do free é use-after-free.
// ── Stack — alocação automática ──────────────────────────────────────────────
void funcao_stack() {
int x = 42; // alocado na stack — libertado quando a função retorna
char buf[256]; // 256 bytes na stack — libertados automaticamente
int arr[10]; // 40 bytes na stack
// x, buf, arr são válidos enquanto estamos dentro de funcao_stack()
} // aqui: x, buf, arr são automaticamente libertados (stack pointer recua)
// Tamanho da stack: tipicamente 1-8 MB por thread
// Stack overflow: exceder o tamanho da stack — recursão infinita, arrays grandes
void recursao_infinita() {
int arr[100000]; // 400 KB na stack a cada chamada
recursao_infinita(); // → stack overflow em poucos níveis de recursão
}
// ── Heap — alocação dinâmica ──────────────────────────────────────────────────
#include <stdlib.h>
#include <string.h>
void funcao_heap() {
// malloc — aloca n bytes no heap, retorna void* (ponteiro genérico)
// retorna NULL se a alocação falhar — SEMPRE verificar
int* arr = malloc(10 * sizeof(int));
if (arr == NULL) {
// tratamento de erro — malloc falhou (memória esgotada)
return;
}
// calloc — aloca E inicializa a zero (malloc não inicializa — contém lixo)
int* zeros = calloc(10, sizeof(int));
// realloc — redimensionar uma alocação existente
arr = realloc(arr, 20 * sizeof(int));
if (arr == NULL) { /* erro */ }
// usar o array...
arr[0] = 42;
memset(arr, 0, 20 * sizeof(int)); // inicializar a zero manualmente
// free — libertar a memória alocada
free(arr); // obrigatório — não libertar = memory leak
free(zeros);
arr = NULL; // boa prática: NULL após free — evita dangling pointer acidental
} // a função retorna — arr e zeros são variáveis da stack (libertadas automaticamente)
// mas a memória que elas apontavam estava no heap — por isso o free é necessário
// Visualização da memória durante funcao_heap():
// Stack (endereços altos → baixos):
// ┌─────────────────────────────────────┐ ← endereço alto
// │ arr = 0x0000561A_B3C4D5E0 (8B) │ ← ponteiro — guarda endereço do heap
// │ zeros = 0x0000561A_B3C4F600 (8B) │ ← outro ponteiro para o heap
// └─────────────────────────────────────┘
//
// Heap (endereços baixos → altos):
// ┌─────────────────────────────────────┐ ← endereço baixo
// │ [lixo][lixo][lixo]...[lixo] (40B)│ ← malloc(10 * sizeof(int)) — não inicializado
// │ addr: 0x0000561A_B3C4D5E0 │
// ├─────────────────────────────────────┤
// │ [0][0][0][0][0][0][0][0][0][0](40B)│ ← calloc(10, sizeof(int)) — inicializado a 0
// │ addr: 0x0000561A_B3C4F600 │
// └─────────────────────────────────────┘
Bugs de gestão de memória em C são a fonte de décadas de CVEs críticos. Ao contrário dos erros lógicos, estes bugs têm consequências de segurança directas — um atacante que controla os dados de input pode frequentemente transformar um bug de memória em execução de código arbitrário.
// ── 1. Memory Leak — esquecer o free ─────────────────────────────────────────
void processar_pedido(Request* req) {
char* buffer = malloc(req->size);
if (req->tipo == TIPO_INVALIDO) {
return; // ❌ memory leak — buffer nunca é libertado neste caminho
}
processar(buffer);
free(buffer); // só chega aqui se req->tipo != TIPO_INVALIDO
}
// Consequência: cada pedido inválido vaza req->size bytes
// Em servidores de longa duração: uso de memória cresce indefinidamente → crash
// Detecção: Valgrind, AddressSanitizer
// ── 2. Use-After-Free — usar memória depois de libertar ───────────────────────
int* criar() {
int* p = malloc(sizeof(int));
*p = 42;
return p;
}
void bug_use_after_free() {
int* p = criar();
free(p); // memória libertada — devolvida ao allocator
printf("%d\n", *p); // ❌ use-after-free — undefined behaviour
// O allocator pode ter reutilizado esta memória para outra alocação
// Ler: retorna lixo ou dados de outra estrutura
// Escrever: corrompe dados de outra estrutura — potencialmente explorável
*p = 99; // ❌ use-after-free com escrita — ainda mais perigoso
}
// Exploit clássico:
// 1. Atacante faz free de um objecto com callback function pointer
// 2. Aloca novo objecto do mesmo tamanho com dados controlados pelo atacante
// (o allocator reutiliza o mesmo bloco de memória)
// 3. Escreve um endereço malicioso na posição do function pointer
// 4. O código legítimo chama o callback → executa código do atacante
// ── 3. Dangling Pointer — ponteiro para memória inválida ─────────────────────
int* ponteiro_dangling() {
int x = 42; // variável na stack
return &x; // ❌ retorna endereço de variável local
} // x é destruído aqui — o endereço retornado é inválido
void usar_dangling() {
int* p = ponteiro_dangling();
printf("%d\n", *p); // ❌ undefined behaviour — lê memória da stack que foi reutilizada
// pode retornar 42 "por acidente" ou lixo total — não determinístico
}
// O compilador moderno avisa: warning: address of local variable 'x' returned
// Com -Wall este warning é imediato — mais uma razão para -Wall ser obrigatório
// ── 4. Double Free — libertar o mesmo bloco duas vezes ───────────────────────
void double_free_bug() {
int* p = malloc(sizeof(int));
free(p);
free(p); // ❌ double free — corrupção das estruturas internas do allocator
// O allocator mantém uma lista de blocos livres
// Double free insere o mesmo bloco duas vezes na lista
// A alocação seguinte pode retornar o mesmo bloco para dois chamadores diferentes
// → dois ponteiros para a mesma memória → race condition / corrupção
}
// CVE-2019-11477 (Linux kernel), CVE-2022-0185 — double free em código kernel
// ── 5. Buffer Overflow — escrever além dos limites ───────────────────────────
void buffer_overflow() {
char buf[8]; // 8 bytes na stack
strcpy(buf, "AAAAAAAAAAAAA"); // copia 13 bytes (+ null) para 8 bytes
// os 6 bytes extra sobrescrevem:
// - variáveis locais adjacentes na stack
// - o saved RBP (base pointer do frame anterior)
// - o endereço de retorno da função
// Quando a função retorna: CPU salta para o endereço sobrescrito pelo atacante
}
// gets() e strcpy() são as funções mais perigosas da libc — nunca usar
// Substituições seguras:
fgets(buf, sizeof(buf), stdin); // em vez de gets()
strncpy(buf, src, sizeof(buf) - 1); // em vez de strcpy()
buf[sizeof(buf) - 1] = '\0'; // garantir null-termination
snprintf(buf, sizeof(buf), "%s", src); // em vez de sprintf()
Em C é possível ter ponteiros para funções — variáveis que guardam o endereço de uma função e permitem chamá-la indirectamente. São a base de callbacks, polimorfismo em C, e tabelas de dispatch — e um vector de ataque crítico em exploits de use-after-free.
// Ponteiros para funções — declaração e uso:
// Declaração: tipo_retorno (*nome)(tipos_parametros)
int (*operacao)(int, int); // ponteiro para função que recebe dois int e retorna int
int somar(int a, int b) { return a + b; }
int multiplicar(int a, int b) { return a * b; }
operacao = somar; // atribuir endereço da função
printf("%d\n", operacao(3, 4)); // → 7 — chamada indirecta
operacao = multiplicar;
printf("%d\n", operacao(3, 4)); // → 12
// Uso prático — callback:
void processar_array(int* arr, int n, int (*transformar)(int)) {
for (int i = 0; i < n; i++) {
arr[i] = transformar(arr[i]);
}
}
int dobrar(int x) { return x * 2; }
int quadrado(int x) { return x * x; }
processar_array(arr, 10, dobrar); // passa dobrar como callback
processar_array(arr, 10, quadrado); // passa quadrado como callback
// Virtual dispatch em C — simular polimorfismo com tabela de funções:
typedef struct {
void (*render)(void* self); // função virtual "render"
void (*update)(void* self); // função virtual "update"
void (*destroy)(void* self); // função virtual "destroy"
} VTable;
typedef struct {
VTable* vtable; // ponteiro para a tabela de funções
// dados do objecto...
} Componente;
// Em C++, o compilador gera esta VTable automaticamente para classes com virtual
// Em Java, a JVM gere a dispatch table — o programador não tem acesso directo
// Relevância para segurança:
// Use-after-free num objecto com VTable → atacante sobrescreve vtable pointer
// → próxima chamada virtual executa endereço controlado pelo atacante
A JVM foi desenhada explicitamente para eliminar as classes de bugs de gestão de memória que assolam C. Mas esta eliminação tem um custo — e não cobre tudo.
// O que a JVM elimina comparado com C:
// ✅ Buffer overflow de array:
// C: arr[10] em array de 5 → undefined behaviour silencioso
// Java: arr[10] em array de 5 → ArrayIndexOutOfBoundsException determinística
// ✅ Use-after-free:
// C: free(p); *p = 42; → corrupção de memória
// Java: não existe free manual — o GC liberta quando não há mais referências
// enquanto existe uma referência para o objecto, o GC não o liberta
// ✅ Dangling pointer:
// C: retornar endereço de variável local → acesso a memória inválida
// Java: referências para objectos são sempre válidas enquanto existem
// a JVM garante que o objecto não é recolhido enquanto é referenciado
// ✅ Double free:
// C: free(p); free(p); → corrupção do heap allocator
// Java: não existe free manual — o GC gere a libertação automaticamente
// ✅ Aritmética de ponteiros arbitrária:
// C: int* p = (int*)0xDEADBEEF; *p = 42; → escreve em endereço arbitrário
// Java: não existem ponteiros raw — referências são verificadas pelo runtime
// ❌ O que a JVM NÃO elimina:
// Null pointer dereference → NullPointerException (melhor que UB, mas ainda é erro)
// Integer overflow → silencioso em Java, como em C
// Logic errors → a JVM não verifica a correcção do algoritmo
// Memory leaks lógicos → guardar referências desnecessariamente impede o GC
// ex: adicionar a uma List estática sem nunca remover
// O custo da segurança da JVM:
// - Overhead de verificação de limites em cada acesso a array (~1-5% em JIT optimizado)
// - Overhead do Garbage Collector — pausas GC, uso de memória 2-5× maior
// - Sem acesso a endereços de memória raw — impossível implementar certas optimizações
// - Startup lento — JVM precisa de inicializar antes de executar código
Em C, bugs de memória são frequentemente silenciosos em desenvolvimento e exploráveis em produção. Um conjunto de ferramentas padrão da indústria detecta estes bugs antes que cheguem a produção.
// Valgrind — detector de erros de memória em runtime
valgrind --leak-check=full --show-leak-kinds=all ./programa
// Detecta: memory leaks, use-after-free, leituras de memória não inicializada
// Overhead: ~20-50× mais lento — usar apenas em testes, nunca em produção
// AddressSanitizer (ASan) — mais rápido que Valgrind, integrado no compilador
gcc -fsanitize=address -g -o programa programa.c
./programa
// Detecta: buffer overflow (stack e heap), use-after-free, double free
// Overhead: ~2× mais lento, ~3× mais memória — usar em CI e testes
// UBSanitizer (UBSan) — detecta undefined behaviour
gcc -fsanitize=undefined -g -o programa programa.c
// Detecta: integer overflow signed, null dereference, desalinhamento, ...
// Pode combinar com ASan: -fsanitize=address,undefined
// Static analysis — sem executar o programa
clang --analyze programa.c // Clang Static Analyzer
cppcheck programa.c // CPPCheck — open source
// Detecta padrões de bug sem executar — útil para code review automatizado
// Exemplo de saída do ASan para use-after-free:
// ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
// READ of size 4 at 0x602000000010 thread T0
// #0 0x401234 in usar_dangling programa.c:42
// #1 0x401456 in main programa.c:60
// 0x602000000010 was freed in:
// #0 0x7f... in free (...)
// #1 0x401123 in funcao programa.c:38
p + 1 avança sizeof(*p) bytes.malloc/free — esquecer é memory leak.malloc — retorna NULL se a alocação falhar.ptr = NULL após free — evita uso acidental de dangling pointer.gets() ou strcpy() sem limite — usar fgets() e strncpy().-Wall não apanha em compile time.