Segmentation fault (core dumped)
Quella riga è l'errore più cercato del C, ed è meno misteriosa di quanto sembri. Il tuo programma ha chiesto al processore un indirizzo di memoria, il sistema operativo ha controllato se il tuo processo può toccare quell'indirizzo e la risposta è stata no. Il kernel ha quindi terminato il processo con un segnale SIGSEGV.
Il punto chiave: il crash è un sintomo, e il punto in cui avviene spesso non è il bug. Il puntatore sbagliato di solito è stato creato altrove, prima, e questo è solo il primo posto in cui è stato usato. Questa pagina copre le cinque cause che spiegano quasi ogni segfault, e poi i due strumenti che trovano la riga vera in pochi secondi.
Gli esempi che vanno in crash qui sotto non sono blocchi eseguibili, di proposito: vanno in crash per costruzione. Leggili, poi leggi la versione corretta che segue.
Cosa significa "memoria che non ti appartiene"
Quando il programma parte, il sistema operativo mappa diverse regioni nel suo spazio di indirizzi: il codice, le globali, lo stack e quanto è cresciuto l'heap. Tutto il resto dello spazio di indirizzi, compreso l'indirizzo 0, non è mappato. Tocca un indirizzo non mappato, o scrivi su uno di sola lettura, e l'hardware lo intercetta.
Quindi un segfault non è il compilatore che ti becca. È un guardrail a runtime, e scatta solo quando l'indirizzo non valido cade per caso fuori dalle tue pagine mappate. Per questo lo stesso bug può andare in crash su una macchina e sembrare funzionare su un'altra: la disposizione della memoria è diversa.
Causa 1: dereferenziare un puntatore NULL
La causa più comune e la più facile da correggere. L'indirizzo 0 non è mai mappato, quindi leggere o scrivere tramite un puntatore nullo provoca sempre un errore.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = NULL;
*p = 42; /* CRASH: scrittura all'indirizzo 0 */
printf("%d\n", *p);
return 0;
}
La versione realistica è un'allocazione non controllata:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *data = malloc(1000000000000UL * sizeof(int)); /* fallisce, restituisce NULL */
data[0] = 1; /* CRASH */
free(data);
return 0;
}
malloc restituisce NULL quando non riesce a soddisfare la richiesta; lo stesso fa fopen quando il file non esiste, e strchr quando il carattere manca. Controlla ogni funzione che può restituire NULL prima di usarne il risultato.
Inizializza i puntatori a NULL invece di lasciarli non inizializzati. Un puntatore nullo va in crash subito e in modo evidente; un puntatore spazzatura può corrompere qualcosa e andare in crash molto più tardi. Trovi di più su questo schema nella pagina sui puntatori nulli.
Causa 2: scrivere oltre la fine di un array
Il C non controlla i limiti degli array. L'indice 10 di un array di 10 elementi è semplicemente la memoria dopo l'array, e il compilatore calcolerà quell'indirizzo per te senza lamentarsi.
#include <stdio.h>
int main(void) {
int arr[10];
for (int i = 0; i <= 10; i++) { /* <= invece di < : uno di troppo */
arr[i] = i;
}
printf("fatto\n");
return 0;
}
Che vada in crash o meno è questione di fortuna. Scrivere quattro byte oltre un array locale di solito finisce su altri dati dello stack, come un registro salvato, un'altra variabile o l'indirizzo di ritorno, quindi il programma si corrompe da solo e va in crash più tardi in un punto che non c'entra nulla. Uno sconfinamento grande esce dalla pagina mappata e va subito in segfault.
La versione più estrema va sempre in crash:
#include <stdio.h>
int main(void) {
int arr[10];
arr[1000000] = 42; /* molto lontano da qualsiasi cosa mappata: CRASH */
return 0;
}
La soluzione è l'abitudine di scrivere i < n, e di calcolare n invece di digitarlo due volte:
Le stringhe hanno una loro versione del problema: un buffer senza spazio per il terminatore '\0'.
#include <string.h>
int main(void) {
char name[5];
strcpy(name, "Alexander"); /* 9 caratteri + terminatore in 5 byte */
return 0;
}
strcpy non ha idea di quanto sia grande name. Usa snprintf, che lo sa perché glielo dici tu:
Causa 3: usare un puntatore dopo free (puntatori pendenti)
Dopo free(p), la memoria torna all'allocatore. Il puntatore contiene ancora il vecchio indirizzo, ma quell'indirizzo non è più tuo.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof *p);
*p = 42;
free(p);
printf("%d\n", *p); /* uso dopo free: puo stampare spazzatura o andare in crash */
free(p); /* doppio free: di solito termina il programma o corrompe l'heap */
return 0;
}
La trappola collegata è restituire l'indirizzo di una variabile locale. Il suo stack frame sparisce nel momento in cui la funzione restituisce:
#include <stdio.h>
int *make_number(void) {
int value = 42;
return &value; /* il frame muore qui; il puntatore resta pendente */
}
int main(void) {
int *p = make_number();
printf("%d\n", *p); /* indefinito: spazzatura, oppure un crash */
return 0;
}
Due soluzioni, a seconda di cosa intendevi. Restituisci il valore invece di un puntatore, oppure alloca sull'heap e lascia che sia il chiamante a liberare:
Impostare il puntatore a NULL subito dopo free è l'abitudine difensiva a basso costo: trasforma un uso silenzioso dopo il free in un crash immediato ed evidente da puntatore nullo, e rende innocuo un secondo free(p), perché free(NULL) per definizione non fa nulla. Il lato dell'allocazione di questa storia è nella memoria dinamica e nei memory leak.
Causa 4: stack overflow da ricorsione senza freni
Ogni chiamata di funzione mette un frame sullo stack, e lo stack è una regione di dimensione fissa (di solito 8 MB). Una ricorsione senza caso base, o con un caso base mai raggiunto, ne supera la fine.
#include <stdio.h>
int countdown(int n) {
printf("%d\n", n);
return countdown(n - 1); /* nessun caso base: non si ferma mai */
}
int main(void) {
return countdown(5);
}
Lo stesso succede con un caso base che la ricorsione scavalca:
int f(int n) {
if (n == 0) return 1;
return n * f(n - 2); /* partendo da un n dispari, non vale mai 0 */
}
Ogni funzione ricorsiva ha bisogno di un caso base raggiungibile da ogni input:
Anche un enorme array locale fa lo stesso: int buffer[10000000]; dentro una funzione chiede 40 MB di stack e va in errore alla prima scrittura. Alloca i buffer grandi sull'heap con malloc. Consulta stack e heap per le dimensioni in gioco e la ricorsione per progettare il caso base.
Causa 5: scrivere su un letterale stringa
Questa sorprende perché il codice sembra innocuo.
#include <stdio.h>
int main(void) {
char *s = "hello";
s[0] = 'H'; /* CRASH: i letterali stringa sono di sola lettura */
printf("%s\n", s);
return 0;
}
Un letterale stringa si trova in una sezione di sola lettura dell'eseguibile. char *s = "hello" punta lì dentro; scrivere tramite quel puntatore è una violazione di protezione, che il sistema operativo segnala come segfault proprio come un accesso non mappato.
La soluzione è usare un array, che riceve una propria copia modificabile:
Dichiarare i puntatori a letterali come const char * trasforma questo crash a runtime in un errore di compilazione, il che è decisamente meglio. Fanne un'abitudine.
Trovare la riga vera: gdb
Compila con -g così che l'eseguibile contenga i simboli di debug, poi eseguilo nel debugger:
gcc -g program.c -o program
gdb ./program
Dentro 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
Tre comandi fanno quasi tutto il lavoro. run avvia il programma e si ferma dove avviene l'errore. backtrace (o bt) mostra la catena di chiamate che ha portato lì: il frame #1 di solito è il punto in cui il puntatore sbagliato è stato davvero prodotto. print ispeziona una variabile, e item = 0x0 indica il problema senza giri di parole.
Su macOS l'equivalente è lldb ./program, poi run e bt.
Trovarlo più in fretta: AddressSanitizer
Ancora meglio, lascia che il compilatore strumenti il programma. AddressSanitizer intercetta l'accesso non valido nel momento in cui avviene, compresi quelli che non avrebbero causato un crash:
gcc -g -fsanitize=address program.c -o program
./program
==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
Quel rapporto indica la categoria del bug, la riga che l'ha causato, la riga che ha liberato la memoria e la riga che l'ha allocata. È in assoluto lo strumento di debug più efficace per i bug di memoria in C, funziona con GCC e clang su Linux e macOS e costa circa il doppio del tempo di esecuzione, cosa irrilevante durante lo sviluppo.
Abbinalo a -fsanitize=undefined per intercettare anche l'overflow con segno e altri comportamenti indefiniti:
gcc -g -Wall -Wextra -fsanitize=address,undefined program.c -o program
valgrind ./program è l'alternativa che non richiede di ricompilare e segnala le stesse categorie di errori, più i leak.
Una checklist quando ne incontri uno
- Ricompila con
-g -Wall -Wextra -fsanitize=address,undefineded eseguilo di nuovo. Nella maggior parte dei casi il rapporto indica la riga e hai finito. - Se il sanitizer non è disponibile, eseguilo in gdb e prendi un
backtrace. Guarda il frame 1, non solo il frame 0. - Controlla ogni puntatore sulla riga del crash. Stampali uno per uno;
0x0identifica un nullo, e un valore strano come0x7fff5fc01000di solito indica un puntatore non inizializzato o già liberato. - Chiediti da dove viene quel puntatore. Un
malloco unfopennon controllato? L'indirizzo di una locale la cui funzione ha già restituito? Un puntatore usato dopofree? - Controlla ogni limite di ciclo vicino al crash alla ricerca di
<=dove intendevi<. - Se lo stack trace è profondo migliaia di frame, si tratta di ricorsione senza freni, non di un bug di puntatori.
Prevenirli
Le abitudini che rendono i segfault rari:
- Compila sempre con
-Wall -Wextrae tratta gli avvisi come bug. - Inizializza ogni puntatore, a
NULLse non hai di meglio. - Controlla il valore restituito da
malloc,calloc,reallocefopen. - Imposta i puntatori a
NULLsubito dopo averli liberati. - Usa
snprintfefgetsinvece disprintfegets. - Dichiara i puntatori a letterali stringa come
const char *. - Preferisci
sizeof arr / sizeof arr[0]a una lunghezza scritta a mano. - Esegui la suite di test con AddressSanitizer nella CI.
Un segfault è il modo di fallire più amichevole: ti dice che qualcosa non va. La stessa categoria di bug che corrompe in silenzio una variabile vicina e produce risultati sbagliati tre funzioni più avanti è molto peggio, e gli strumenti qui sopra intercettano entrambi i casi.
Domande frequenti
Cos'è un segmentation fault in C?
Un crash che il sistema operativo provoca quando il tuo programma accede a memoria che non ha il permesso di toccare: leggere o scrivere tramite un puntatore non valido, andare oltre la fine di un array fino a una pagina non mappata o esaurire lo stack. Il kernel invia al processo un segnale SIGSEGV, che lo termina e stampa "Segmentation fault (core dumped)".
Come trovo il punto in cui avviene un segmentation fault?
Compila con i simboli di debug ed esegui il programma in un debugger: gcc -g program.c -o program, poi gdb ./program, run e, quando va in crash, backtrace. Così ottieni il file e la riga esatti. Ancora più rapido per i bug di memoria è gcc -g -fsanitize=address program.c -o program: basta eseguire il programma per ottenere un rapporto completo su cosa è andato storto e dove.
Perché il mio programma C va in segfault solo a volte?
Perché l'accesso non valido è comportamento indefinito, non un crash garantito. Scrivere un elemento oltre la fine di un array spesso finisce in memoria che il tuo processo possiede davvero, quindi niente ti ferma: al suo posto corrompi una variabile vicina. Il segfault arriva solo quando l'indirizzo sbagliato cade per caso fuori da una pagina mappata, e questo dipende dalla disposizione di quella specifica compilazione ed esecuzione.
Un segmentation fault significa che ho un memory leak?
No: sono problemi opposti. Un leak è memoria che hai allocato e mai liberato: il programma continua a girare e cresce lentamente. Un segfault è toccare memoria che non ti appartiene. Liberare la memoria due volte, o usare un puntatore dopo averlo liberato, causa segfault; dimenticare di liberare causa leak.