Elenco definitivo di motivi comuni per i difetti di segmentazione

NOTA: Abbiamo un sacco di domande segfault, con in gran parte le stesse risposte, quindi sto cercando di collassarle in una domanda canonica come quella che abbiamo per riferimento non definito .

Anche se abbiamo una domanda su cosa sia un difetto di segmentazione , copre il cosa , ma non elenca molte ragioni. La risposta in alto dice “ci sono molte ragioni”, e ne elenca solo una, e la maggior parte delle altre risposte non elenca alcun motivo.

Tutto sumto, credo che abbiamo bisogno di un wiki di comunità ben organizzato su questo argomento, che elenca tutte le cause comuni (e quindi alcune) per ottenere segfaults. Lo scopo è di aiutare nel debugging, come indicato nel disclaimer della risposta.

So che cos’è un errore di segmentazione, ma può essere difficile individuare il codice senza sapere che aspetto hanno. Sebbene ci siano, senza dubbio, troppi per elencare esaustivamente, quali sono le cause più comuni di errori di segmentazione in C e C ++?

AVVERTIMENTO!

I seguenti sono potenziali motivi per un errore di segmentazione. È praticamente imansible elencare tutti i motivi . Lo scopo di questo elenco è di aiutare a diagnosticare un segfault esistente.

La relazione tra errori di segmentazione e comportamento indefinito non può essere abbastanza sottolineata! Tutte le situazioni sottostanti che possono creare un errore di segmentazione sono comportamenti tecnicamente indefiniti. Ciò significa che possono fare qualsiasi cosa , non solo segfault – come qualcuno ha detto una volta su USENET, ” è legale per il compilatore far volare via demoni dai demoni “. Non contare su un evento segfault ogni volta che si ha un comportamento indefinito. Dovresti imparare quali comportamenti non definiti esistono in C e / o C ++ ed evitare di scrivere codice che li abbia!

Ulteriori informazioni sul comportamento non definito:

  • Qual è il modo standard più semplice per produrre un Segfault in C?
  • Comportamento indefinito, non specificato e definito dall’implementazione
  • Quanto indefinito è il comportamento indefinito?

Cos’è un Segfault?

In breve, si verifica un errore di segmentazione quando il codice tenta di accedere alla memoria a cui non ha il permesso di accedere . A ogni programma viene dato un pezzo di memoria (RAM) con cui lavorare e, per ragioni di sicurezza, è consentito accedere alla memoria solo in quel blocco.

Per una spiegazione tecnica più approfondita su cos’è un errore di segmentazione, vedere Che cos’è un errore di segmentazione? .

Ecco i motivi più comuni per un errore di errore di segmentazione. Ancora una volta, questi dovrebbero essere usati per diagnosticare un segfault esistente . Per imparare come evitarli, apprendi i comportamenti non definiti della tua lingua.

Anche questo elenco non è sostitutivo per eseguire il proprio lavoro di debug . (Vedi la sezione in fondo alla risposta.) Queste sono cose che puoi cercare, ma i tuoi strumenti di debug sono l’unico modo affidabile per azzerare il problema.


Accesso a un puntatore NULL o non inizializzato

Se hai un puntatore che è NULL ( ptr=0 ) o che è completamente non inizializzato (non è ancora impostato su nulla), il tentativo di accedere o modificare usando quel puntatore ha un comportamento indefinito.

 int* ptr = 0; *ptr += 5; 

Poiché un’allocazione fallita (come con malloc o new ) restituirà un puntatore nullo, è necessario controllare sempre che il puntatore non sia NULL prima di utilizzarlo.

Si noti inoltre che anche la lettura dei valori (senza dereferenziazione) dei puntatori non inizializzati (e delle variabili in generale) è un comportamento indefinito.

A volte questo accesso di un puntatore non definito può essere piuttosto sottile, come nel tentativo di interpretare un tale puntatore come una stringa in un’istruzione di stampa C.

 char* ptr; sprintf(id, "%s", ptr); 

Guarda anche:

  • Come rilevare se variabile non inizializzata / catch segfault in C
  • La concatenazione di string e int risulta nell’errore seg C

Accedere a un puntatore penzolante

Se si usa malloc o new per allocare memoria, e in seguito free o delete quella memoria tramite il puntatore, quel puntatore viene ora considerato un puntatore pendente . Il dereferenziamento (oltre alla semplice lettura del suo valore – garantito che non hai assegnato alcun nuovo valore ad esso come NULL) è un comportamento indefinito e può causare un errore di segmentazione.

 Something* ptr = new Something(123, 456); delete ptr; std::cout << ptr->foo << std::endl; 

Guarda anche:

  • Cos'è un puntatore pendente?
  • Perché il mio puntatore ciondolante non causa un errore di segmentazione?

Stack overflow

[No, non è il sito su cui ti trovi ora, per cosa è stato chiamato .] Oversimplified, lo "stack" è come quel picco su cui ti appiccica il tuo ordine in alcuni commensali. Questo problema può verificarsi quando metti troppi ordini su quel picco, per così dire. Nel computer, qualsiasi variabile che non è allocata dynamicmente e qualsiasi comando che deve ancora essere elaborato dalla CPU, va in pila.

Una causa di ciò potrebbe essere la ricorsione profonda o infinita, ad esempio quando una funzione chiama se stessa senza alcun modo di fermarsi. Poiché quella pila è stata traboccata, i documenti dell'ordine iniziano a "cadere" e occupano altro spazio non destinato a loro. Quindi, possiamo ottenere un errore di segmentazione. Un'altra causa potrebbe essere il tentativo di inizializzare un array molto grande: è solo un singolo ordine, ma già abbastanza grande da solo.

 int stupidFunction(int n) { return stupidFunction(n); } 

Un'altra causa di un overflow dello stack sarebbe avere troppe variabili (non allocate dynamicmente) contemporaneamente.

 int stupidArray[600851475143]; 

Un caso di overflow dello stack in natura derivava da una semplice omissione di un'istruzione return in un condizionale destinato a impedire la ricorsione infinita in una funzione. La morale di quella storia, assicurati sempre che il tuo errore funzioni!

Guarda anche:

  • Errore di segmentazione durante la creazione di array di grandi dimensioni in C
  • Seg Fault durante l'inizializzazione dell'array

Puntatori selvaggi

Creare un puntatore in una posizione casuale in memoria è come giocare alla roulette russa con il tuo codice: potresti facilmente perdere e creare un puntatore a una posizione a cui non hai diritti di accesso.

 int n = 123; int* ptr = (&n + 0xDEADBEEF); //This is just stupid, people. 

Come regola generale, non creare puntatori a posizioni di memoria letterali. Anche se lavorassero una volta, la prossima volta potrebbero non farlo. Non è ansible prevedere dove sarà la memoria del programma in una determinata esecuzione.

Guarda anche:

  • Qual è il significato di "wild pointer" in C?

Tentativo di leggere oltre la fine di un array

Una matrice è una regione contigua di memoria, in cui ogni elemento successivo si trova al successivo indirizzo in memoria. Tuttavia, la maggior parte degli array non ha un senso innato di quanto siano grandi o di quale sia l'ultimo elemento. Pertanto, è facile superare la fine dell'array e non saperlo mai, specialmente se si utilizza l'aritmetica del puntatore.

Se leggi oltre la fine dell'array, puoi finire andando nella memoria che non è inizializzata o appartiene a qualcos'altro. Questo è un comportamento tecnicamente indefinito . Un segfault è solo uno di quei molti potenziali comportamenti indefiniti. [Francamente, se ottieni un segfault qui, sei fortunato. Altri sono difficili da diagnosticare.]

 // like most UB, this code is a total crapshoot. int arr[3] {5, 151, 478}; int i = 0; while(arr[i] != 16) { std::cout << arr[i] << std::endl; i++; } 

O quello visto frequentemente usando for <= invece di < (legge troppo 1 byte):

 char arr[10]; for (int i = 0; i<=10; i++) { std::cout << arr[i] << std::endl; } 

O anche uno sfortunato errore di battitura che compila bene (visto qui ) e alloca solo 1 elemento inizializzato con elementi dim invece di dim .

 int* my_array = new int(dim); 

Inoltre, si dovrebbe notare che non si è nemmeno autorizzati a creare (per non parlare del dereferenziamento) un puntatore che punta all'esterno dell'array (è ansible creare tale puntatore solo se punta a un elemento all'interno dell'array oa uno oltre la fine). Altrimenti, stai triggersndo un comportamento indefinito.

Guarda anche:

  • Ho dei segfault!

Dimenticando un terminatore NUL su una stringa C.

Le stringhe C sono, a loro volta, matrici con alcuni comportamenti aggiuntivi. Devono essere terminati con null, il che significa che hanno un \0 alla fine, per essere usati in modo affidabile come stringhe. Questo viene fatto automaticamente in alcuni casi, e non in altri.

Se questo viene dimenticato, alcune funzioni che gestiscono le stringhe C non sanno mai quando fermarsi, e si possono ottenere gli stessi problemi con la lettura oltre la fine di un array.

 char str[3] = {'f', 'o', 'o'}; int i = 0; while(str[i] != '\0') { std::cout << str[i] << std::endl; i++; } 

Con le stringhe C, è davvero imperdibile se \0 farà alcuna differenza. Si dovrebbe assumere che eviterà un comportamento indefinito: meglio scrivere char str[4] = {'f', 'o', 'o', '\0'};


Tentativo di modificare una stringa letterale

Se assegni una stringa letterale a un carattere *, non può essere modificata. Per esempio...

 char* foo = "Hello, world!" foo[7] = 'W'; 

... triggers un comportamento indefinito e un errore di segmentazione è un ansible risultato.

Guarda anche:

  • Perché questo codice C di inversione delle stringhe causa un errore di segmentazione?

Mancata assegnazione dei metodi di allocazione e deallocazione

È necessario utilizzare malloc e free insieme, new ed delete insieme, e new[] ed delete[] insieme. Se li mescoli, puoi ottenere segfault e altri comportamenti strani.

Guarda anche:

  • Comportamento di malloc con delete in C ++
  • Errore di segmentazione (core dumped) quando cancello il puntatore

Errori nella toolchain.

Un bug nel backend del codice macchina di un compilatore è abbastanza capace di trasformare un codice valido in un eseguibile che segfaults. Un bug nel linker può sicuramente fare anche questo.

Particolarmente spaventoso nel fatto che questo non è UB invocato dal proprio codice.

Detto questo, dovresti sempre presumere che il problema sei tu fino a prova contraria.


Altre cause

Le possibili cause di errori di segmentazione sono tanto numerose quanto il numero di comportamenti non definiti, e ce ne sono troppi anche per la documentazione standard da elencare.

Alcune cause meno comuni da verificare:

  • UD2 generato su alcune piattaforms a causa di altri UB
  • c ++ STL map :: operator [] fatto su una voce che viene cancellata

DEBUGGING

Gli strumenti di debug sono fondamentali per diagnosticare le cause di un segfault. Compila il tuo programma con il flag di debugging ( -g ), quindi eseguilo con il tuo debugger per trovare dove è probabile che si verifichi il segfault.

I compilatori recenti supportano la compilazione con -fsanitize=address , che in genere si traduce in un programma eseguito circa 2 volte più lento ma in grado di rilevare errori di indirizzo in modo più accurato. Tuttavia, altri errori (come la lettura dalla memoria non inizializzata o la perdita di risorse non di memoria come i descrittori di file) non sono supportati da questo metodo, ed è imansible utilizzare molti strumenti di debug e ASan allo stesso tempo.

Alcuni debugger di memoria

  • GDB | Mac, Linux
  • valgrind (memcheck) | Linux
  • Dr. Memory | windows

Inoltre, si consiglia di utilizzare strumenti di analisi statici per rilevare comportamenti non definiti, ma, ancora una volta, sono uno strumento solo per aiutarti a trovare un comportamento non definito e non garantiscono di trovare tutte le occorrenze di comportamenti non definiti.

Se sei davvero sfortunato, tuttavia, l'uso di un debugger (o, più raramente, solo la ricompilazione con le informazioni di debug) può influenzare il codice e la memoria del programma in modo sufficiente che il segfault non si verifica più, un fenomeno noto come heisenbug .