Segmentation fault: causas e debugging

1. O que é um Segmentation Fault?

Segmentation fault (ou segfault) é um erro fatal que ocorre quando um programa tenta acessar uma região de memória que não lhe pertence ou para a qual não tem permissão de acesso. O sistema operacional, através do gerenciador de memória virtual, detecta essa violação e envia o sinal SIGSEGV (signal 11) ao processo.

O comportamento padrão ao receber SIGSEGV é:
- Encerramento abrupto do programa
- Geração opcional de um core dump (arquivo com o estado da memória no momento do crash)
- Mensagem como "Segmentation fault (core dumped)" no terminal

É importante distinguir segmentation fault de outros erros de memória:
- Bus error (SIGBUS): ocorre quando há tentativa de acesso a memória com alinhamento incorreto (ex: ler um int de 4 bytes em um endereço ímpar)
- Abort (SIGABRT): gerado por abort() ou por detecção interna de erro (ex: assert() falho, free() em ponteiro inválido)

2. Causas Comuns em C

Desreferenciamento de ponteiro nulo ou não inicializado

#include <stdio.h>

int main() {
    int *p = NULL;
    *p = 10;  // Segmentation fault!
    return 0;
}

Estouro de buffer (stack ou heap)

#include <string.h>

int main() {
    char buffer[5];
    strcpy(buffer, "esta string é muito longa");  // Estouro de buffer na stack
    return 0;
}

Acesso a memória já liberada (use-after-free)

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    free(p);
    *p = 42;  // Use-after-free! Comportamento indefinido
    return 0;
}

Acesso fora dos limites de array

#include <stdio.h>

int main() {
    int arr[3] = {1, 2, 3};
    printf("%d\n", arr[100]);  // Acesso além do limite
    arr[-1] = 0;               // Índice negativo
    return 0;
}

3. Casos Clássicos com Ponteiros

Ponteiro sem alocação

#include <stdio.h>

int main() {
    int *p;      // Não inicializado
    *p = 10;     // Comportamento indefinido - provavelmente segfault
    printf("%d\n", *p);
    return 0;
}

Ponteiro para variável local retornada de função

#include <stdio.h>

int* get_local() {
    int x = 42;
    return &x;  // Retorna endereço de variável local (na stack)
}

int main() {
    int *p = get_local();
    printf("%d\n", *p);  // Use-after-scope! Pode causar segfault
    return 0;
}

Dupla liberação (double free)

#include <stdlib.h>

int main() {
    int *p = malloc(sizeof(int));
    free(p);
    free(p);  // Double free - corrompe o gerenciador de heap
    return 0;
}

Aritmética de ponteiro incorreta

#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello";
    char *p = str;

    // Avança além do terminador nulo
    p = p + 10;
    printf("%c\n", *p);  // Acesso a memória inválida
    return 0;
}

4. Erros com Arrays e Strings

Falta do terminador nulo

#include <stdio.h>
#include <string.h>

int main() {
    char buffer[5] = {'H', 'e', 'l', 'l', 'o'};  // Sem '\0'!
    printf("%s\n", buffer);  // Pode causar segfault ou lixo
    return 0;
}

Uso de gets() - função insegura

#include <stdio.h>

int main() {
    char buffer[10];
    gets(buffer);  // PERIGOSO: sem limite de tamanho
    return 0;
}

Confusão entre sizeof e strlen

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main() {
    char *str = "Hello";
    char *copy = malloc(sizeof(str));  // sizeof(str) = 8 (tamanho do ponteiro!)
    // char *copy = malloc(strlen(str) + 1);  // Correto
    strcpy(copy, str);  // Pode causar estouro de buffer
    free(copy);
    return 0;
}

5. Debugging com GDB: Primeiros Passos

Compilação adequada

gcc -g -O0 programa.c -o programa

Execução no GDB e captura do crash

$ gdb ./programa
(gdb) run
Starting program: ./programa

Program received signal SIGSEGV, Segmentation fault.
0x0000555555555156 in main () at programa.c:8
8           *p = 10;

Comando backtrace para ver a pilha

(gdb) backtrace
#0  0x0000555555555156 in main () at programa.c:8

Inspeção de variáveis

(gdb) print p
$1 = (int *) 0x0
(gdb) info locals
p = 0x0

6. Técnicas Avançadas de Debugging

Breakpoints condicionais

(gdb) break main.c:15 if i == 1000
(gdb) run

Análise de core dump

$ ulimit -c unlimited  # Habilitar core dump
$ ./programa
Segmentation fault (core dumped)
$ gdb ./programa core
(gdb) backtrace

Uso do Valgrind (memcheck)

$ valgrind --tool=memcheck ./programa
==12345== Invalid write of size 4
==12345==    at 0x109156: main (programa.c:8)
==12345==  Address 0x0 is not stack'd, malloc'd or (recently) free'd

Uso do AddressSanitizer

$ gcc -g -fsanitize=address programa.c -o programa
$ ./programa
=================================================================
==12345==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000

Watchpoints para monitorar variáveis

(gdb) watch *ptr
(gdb) continue

7. Prevenção e Boas Práticas

Inicialização explícita de ponteiros

int *p = NULL;  // Sempre inicializar
if (p != NULL) {
    *p = 10;
}

Verificação do retorno de malloc/calloc

int *arr = malloc(100 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "Falha na alocação\n");
    exit(1);
}

Limitação de acesso a arrays

#define TAMANHO 10

int arr[TAMANHO];
for (int i = 0; i < TAMANHO; i++) {  // Usar constante, não valor fixo
    arr[i] = i;
}

Adoção de funções seguras

// Em vez de gets(buffer)
fgets(buffer, sizeof(buffer), stdin);

// Em vez de strcpy(dest, src)
strncpy(dest, src, tamanho_dest - 1);
dest[tamanho_dest - 1] = '\0';

// Em vez de sprintf
snprintf(buffer, sizeof(buffer), "%s %d", str, num);

Referências