Menu

Falha de segmentação em C: o que causa e como corrigir

Um segfault significa que seu programa tocou em memória que não lhe pertence. Aqui estão as cinco causas que respondem por quase todos eles, cada uma com um exemplo mínimo e sua correção, além de como achar a linha exata com o gdb e o AddressSanitizer.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

Segmentation fault (core dumped)

Essa única linha é o erro mais pesquisado em C, e é menos misteriosa do que parece. Seu programa pediu ao processador um endereço de memória, o sistema operacional conferiu se o seu processo tem permissão de tocar naquele endereço, e a resposta foi não. O kernel então matou o processo com um sinal SIGSEGV.

A ideia central: o travamento é um sintoma, e o lugar dele muitas vezes não é o bug. O ponteiro ruim normalmente foi criado em outro lugar, antes, e este é apenas o primeiro ponto em que ele foi usado. Esta página cobre as cinco causas que respondem por quase todo segfault, e depois as duas ferramentas que acham a linha de verdade em segundos.

Os exemplos que travam abaixo deliberadamente não são blocos executáveis - eles travam de propósito. Leia-os, e depois leia a versão corrigida que vem em seguida.

O que significa "memória que não lhe pertence"

Quando seu programa inicia, o sistema operacional mapeia várias regiões no espaço de endereçamento dele: o código, as globais, a pilha e o quanto o heap tiver crescido. Todo o resto do espaço de endereçamento - inclusive o endereço 0 - fica não mapeado. Toque um endereço não mapeado, ou escreva num somente-leitura, e o hardware intercepta.

Então um segfault não é o compilador te pegando. É uma grade de proteção em tempo de execução, e ela só dispara quando o endereço inválido por acaso está fora das suas páginas mapeadas. É por isso que o mesmo bug pode travar numa máquina e parecer funcionar em outra: o leiaute de memória é diferente.

Causa 1: desreferenciar um ponteiro NULL

A causa mais comum, e a mais fácil de corrigir. O endereço 0 nunca é mapeado, então ler ou escrever por um ponteiro nulo sempre falha.

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

int main(void) {
    int *p = NULL;
    *p = 42;              /* CRASH: escrevendo no endereco 0 */
    printf("%d\n", *p);
    return 0;
}

A versão realista é uma alocação não verificada:

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

int main(void) {
    int *data = malloc(1000000000000UL * sizeof(int));  /* falha, devolve NULL */
    data[0] = 1;                                        /* CRASH */
    free(data);
    return 0;
}

O malloc devolve NULL quando não consegue atender ao pedido; o fopen também, quando o arquivo não existe, e o strchr quando o caractere não está lá. Verifique toda função que possa devolver NULL antes de usar o resultado dela.

Inicialize ponteiros com NULL em vez de deixá-los não inicializados. Um ponteiro nulo trava imediatamente e de forma óbvia; um ponteiro com lixo pode corromper algo e travar muito depois. Mais sobre o padrão em ponteiros NULL.

Causa 2: escrever além do fim de um array

O C não verifica limites de array. O índice 10 de um array de 10 elementos é simplesmente a memória depois do array, e o compilador vai calcular esse endereço para você sem reclamar.

#include <stdio.h>

int main(void) {
    int arr[10];

    for (int i = 0; i <= 10; i++) {   /* <= em vez de < : um a mais */
        arr[i] = i;
    }

    printf("pronto\n");
    return 0;
}

Se isso trava é sorte. Escrever quatro bytes além de um array local normalmente cai em outros dados da pilha - um registrador salvo, outra variável, o endereço de retorno - então o programa corrompe a si mesmo e trava depois, em algum lugar sem relação. Um estouro grande sai da página mapeada e dá segfault na hora.

A versão mais selvagem sempre trava:

#include <stdio.h>

int main(void) {
    int arr[10];
    arr[1000000] = 42;      /* bem fora de qualquer coisa mapeada: CRASH */
    return 0;
}

A solução é o hábito do i < n, e calcular n em vez de digitá-lo duas vezes:

As strings têm a própria versão disso: um buffer sem espaço para o '\0' terminador.

#include <string.h>

int main(void) {
    char name[5];
    strcpy(name, "Alexander");   /* 9 caracteres + terminador em 5 bytes */
    return 0;
}

O strcpy não faz ideia de quão grande é name. Use snprintf, que sabe porque você diz a ele:

Causa 3: usar um ponteiro depois do free (ponteiros pendurados)

Depois de free(p), a memória é devolvida ao alocador. O ponteiro ainda guarda o endereço antigo, mas aquele endereço não é mais seu.

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

int main(void) {
    int *p = malloc(sizeof *p);
    *p = 42;
    free(p);

    printf("%d\n", *p);   /* uso apos liberacao: pode imprimir lixo, pode travar */
    free(p);              /* liberacao dupla: costuma abortar ou corromper o heap */
    return 0;
}

A armadilha aparentada é devolver o endereço de uma variável local. O quadro de pilha dela some no instante em que a função retorna:

#include <stdio.h>

int *make_number(void) {
    int value = 42;
    return &value;      /* o quadro morre aqui; o ponteiro fica pendurado */
}

int main(void) {
    int *p = make_number();
    printf("%d\n", *p);   /* indefinido: lixo, ou um crash */
    return 0;
}

Duas correções, dependendo do que você queria dizer. Devolva o valor em vez de um ponteiro, ou aloque no heap e deixe o chamador liberar:

Colocar NULL no ponteiro logo depois do free é o hábito defensivo barato: ele transforma um uso-após-liberação silencioso num travamento imediato e óbvio por desreferência de nulo, e torna um segundo free(p) inofensivo, já que free(NULL) é definido como não fazer nada. O lado da alocação dessa história está em memória dinâmica e vazamentos de memória.

Causa 4: estouro de pilha por recursão desgovernada

Cada chamada de função coloca um quadro na pilha, e a pilha é uma região de tamanho fixo (comumente 8 MB). Recursão sem caso base - ou com um que nunca é alcançado - sai pelo fim dela.

#include <stdio.h>

int countdown(int n) {
    printf("%d\n", n);
    return countdown(n - 1);    /* sem caso base: nunca para */
}

int main(void) {
    return countdown(5);
}

O mesmo acontece com um caso base sobre o qual a recursão passa por cima:

int f(int n) {
    if (n == 0) return 1;
    return n * f(n - 2);    /* a partir de um n impar, nunca e igual a 0 */
}

Toda função recursiva precisa de um caso base alcançável a partir de qualquer entrada:

Um array local enorme também causa isso - int buffer[10000000]; dentro de uma função pede 40 MB de pilha e falha na primeira escrita. Aloque buffers grandes no heap com malloc. Veja pilha vs heap para os tamanhos envolvidos, e recursão para o projeto do caso base.

Causa 5: escrever num literal de string

Essa surpreende as pessoas porque o código parece inofensivo.

#include <stdio.h>

int main(void) {
    char *s = "hello";
    s[0] = 'H';          /* CRASH: literais de string sao somente leitura */
    printf("%s\n", s);
    return 0;
}

Um literal de string vive numa seção somente-leitura do executável. char *s = "hello" aponta para dentro dela; escrever por esse ponteiro é uma falha de proteção, que o SO reporta como um segfault, igualzinho a um acesso não mapeado.

A correção é fazer um array, que ganha a própria cópia modificável:

Declarar ponteiros para literais como const char * transforma esse travamento em tempo de execução num erro de compilação, o que é estritamente melhor. Faça disso um hábito.

Achando a linha de verdade: gdb

Compile com -g para que o executável carregue símbolos de depuração, e então rode sob o depurador:

gcc -g programa.c -o programa
gdb ./programa

Dentro do gdb:

(gdb) run
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555151 in process_item (item=0x0) at program.c:14
14          return item->count * 2;

(gdb) backtrace
#0  process_item (item=0x0) at program.c:14
#1  0x000055555555518a in main () at program.c:23

(gdb) print item
$1 = (struct Item *) 0x0

Três comandos fazem a maior parte do trabalho. run inicia o programa e para onde ele falha. backtrace (ou bt) mostra a cadeia de chamadas que chegou até lá - o quadro #1 é normalmente onde o ponteiro ruim foi de fato produzido. print inspeciona uma variável, e item = 0x0 nomeia o problema sem rodeios.

No macOS o equivalente é lldb ./programa, depois run e bt.

Achando mais rápido: AddressSanitizer

Melhor ainda: deixe o compilador instrumentar o programa. O AddressSanitizer pega o acesso inválido no momento em que ele acontece - inclusive os que não teriam travado:

gcc -g -fsanitize=address programa.c -o programa
./programa
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
WRITE of size 4 at 0x602000000010 thread T0
    #0 0x4011f6 in main program.c:11

0x602000000010 is located 0 bytes inside of 4-byte region
freed by thread T0 here:
    #1 0x4011c9 in main program.c:10
previously allocated by thread T0 here:
    #2 0x4011a6 in main program.c:8

Esse relatório nomeia a classe do bug, a linha que o cometeu, a linha que liberou a memória e a linha que a alocou. É a ferramenta de depuração mais eficaz para bugs de memória em C, funciona com GCC e clang no Linux e no macOS, e custa aproximadamente 2x em tempo de execução - o que é irrelevante durante o desenvolvimento.

Combine-o com -fsanitize=undefined para pegar overflow com sinal e outros comportamentos indefinidos ao mesmo tempo:

gcc -g -Wall -Wextra -fsanitize=address,undefined programa.c -o programa

valgrind ./programa é a alternativa que não exige recompilação, e reporta as mesmas classes de erro mais os vazamentos.

Uma lista de conferência quando você toma um

  1. Recompile com -g -Wall -Wextra -fsanitize=address,undefined e rode de novo. Na maior parte das vezes o relatório nomeia a linha e acabou.
  2. Se o sanitizador não estiver disponível, rode sob o gdb e tire um backtrace. Olhe o quadro 1, não só o quadro 0.
  3. Confira todos os ponteiros da linha que travou. Imprima cada um; 0x0 identifica um nulo, e um valor selvagem como 0x7fff5fc01000 normalmente significa não inicializado ou liberado.
  4. Pergunte de onde aquele ponteiro veio. Um malloc ou fopen não verificado? O endereço de uma local cuja função já retornou? Um ponteiro usado depois do free?
  5. Confira todos os limites de laço perto do travamento procurando um <= onde se queria <.
  6. Se o rastro de pilha tiver milhares de quadros de profundidade, é recursão desgovernada, e não um bug de ponteiro.

Prevenindo

Os hábitos que tornam os segfaults raros:

  • Compile sempre com -Wall -Wextra, e trate avisos como bugs.
  • Inicialize todos os ponteiros, com NULL se não houver nada melhor.
  • Verifique o retorno de malloc, calloc, realloc e fopen.
  • Coloque NULL nos ponteiros imediatamente depois de liberar.
  • Use snprintf e fgets em vez de sprintf e gets.
  • Declare ponteiros para literais de string como const char *.
  • Prefira sizeof arr / sizeof arr[0] a um comprimento fixo no código.
  • Rode a suíte de testes sob o AddressSanitizer na CI.

Um segfault é o modo de falha amigável - ele te avisa que algo está errado. A mesma classe de bug que corrompe silenciosamente uma variável vizinha e produz respostas erradas três funções depois é bem pior, e as ferramentas acima pegam os dois.

Perguntas frequentes

O que é uma falha de segmentação em C?

É um travamento que o sistema operacional dispara quando o seu programa acessa memória que ele não tem permissão de tocar - lendo ou escrevendo por um ponteiro inválido, passando do fim de um array para uma página não mapeada, ou estourando a pilha. O kernel envia ao processo o sinal SIGSEGV, que o encerra e imprime "Segmentation fault (core dumped)".

Como descubro onde acontece a falha de segmentação?

Compile com símbolos de depuração e rode sob um depurador: gcc -g programa.c -o programa, depois gdb ./programa, run e, quando travar, backtrace. Isso imprime o arquivo e a linha exatos. Mais rápido ainda para bugs de memória é gcc -g -fsanitize=address programa.c -o programa - basta rodar o programa e ele imprime um relatório completo do que deu errado e onde.

Por que meu programa em C só dá segfault às vezes?

Porque o acesso inválido é comportamento indefinido, não um travamento garantido. Escrever um elemento além do array muitas vezes cai em memória que o seu processo de fato possui, então nada o impede - em vez disso ele corrompe uma variável vizinha. Você só toma um segfault quando o endereço ruim por acaso cai fora de uma página mapeada, o que depende do leiaute daquela compilação e execução específicas.

Uma falha de segmentação significa que eu tenho um vazamento de memória?

Não - são problemas opostos. Um vazamento é memória que você alocou e nunca liberou: o programa continua rodando e cresce aos poucos. Um segfault é tocar em memória que não lhe pertence. Liberar memória duas vezes, ou usar um ponteiro depois de liberá-lo, causa segfaults; esquecer de liberar causa vazamentos.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR