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
- Recompile com
-g -Wall -Wextra -fsanitize=address,undefinede rode de novo. Na maior parte das vezes o relatório nomeia a linha e acabou. - 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. - Confira todos os ponteiros da linha que travou. Imprima cada um;
0x0identifica um nulo, e um valor selvagem como0x7fff5fc01000normalmente significa não inicializado ou liberado. - Pergunte de onde aquele ponteiro veio. Um
mallocoufopennão verificado? O endereço de uma local cuja função já retornou? Um ponteiro usado depois dofree? - Confira todos os limites de laço perto do travamento procurando um
<=onde se queria<. - 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
NULLse não houver nada melhor. - Verifique o retorno de
malloc,calloc,reallocefopen. - Coloque
NULLnos ponteiros imediatamente depois de liberar. - Use
snprintfefgetsem vez desprintfegets. - 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.