Come funziona un errore di segmentazione sotto il cofano?


266

Non riesco a trovare alcuna informazione al riguardo a parte "la MMU della CPU invia un segnale" e "il kernel lo indirizza al programma offensivo, terminandolo".

Ho ipotizzato che probabilmente invia il segnale alla shell e la shell lo gestisce interrompendo il processo e la stampa offensivi "Segmentation fault". Quindi ho testato questo presupposto scrivendo una shell estremamente minimale che chiamo crsh (crap shell). Questa shell non fa altro che accettare l'input dell'utente e inviarlo al system()metodo.

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

int main(){
    char cmdbuf[1000];
    while (1){
        printf("Crap Shell> ");
        fgets(cmdbuf, 1000, stdin);
        system(cmdbuf);
    }
}

Quindi ho eseguito questa shell in un terminale nudo (senza bashcorrere sotto). Quindi ho proceduto all'esecuzione di un programma che produce un segfault. Se i miei presupposti fossero corretti, questo sarebbe a) crash crsh, chiusura di xterm, b) non stampa "Segmentation fault", o c) entrambi.

braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]

Torna al punto di partenza, immagino. Ho appena dimostrato che non è la shell a farlo, ma il sistema sottostante. Come viene persino stampato "Errore di segmentazione"? "Chi" lo sta facendo? Il nocciolo? Qualcos'altro? In che modo il segnale e tutti i suoi effetti collaterali si propagano dall'hardware all'eventuale chiusura del programma?


43
crshè un'ottima idea per questo tipo di sperimentazione. Grazie per averci fatto sapere a tutti noi e l'idea alla base.
Bruce Ediger,

30
Quando ho visto per la prima volta crsh, ho pensato che sarebbe stato pronunciato "crash". Non sono sicuro che sia un nome altrettanto appropriato.
jpmc26,

56
Questo è un bel esperimento ... ma dovresti sapere cosa system()fa sotto il cofano. Si scopre che system()genererà un processo di shell! Quindi il tuo processo shell genera un altro processo shell e quel processo shell (probabilmente /bin/sho qualcosa del genere) è quello che esegue il programma. Il modo /bin/sho bashfunziona è usando fork()e exec()(o un'altra funzione nella execve()famiglia).
Dietrich Epp,

4
@BradenBest: esattamente. Leggi la pagina del manuale man 2 wait, includerà le macro WIFSIGNALED()e WTERMSIG().
Dietrich Epp, il

4
@DietrichEpp Proprio come hai detto! Ho provato ad aggiungere un segno di spunta per (WIFSIGNALED(status) && WTERMSIG(status) == 11)farlo stampare qualcosa di sciocco ( "YOU DUN GOOFED AND TRIGGERED A SEGFAULT"). Quando ho eseguito il segfaultprogramma dall'interno crsh, è stato stampato esattamente quello. Nel frattempo, i comandi che escono normalmente non producono il messaggio di errore.
Braden Best,

Risposte:


248

Tutte le CPU moderne hanno la capacità di interrompere le istruzioni della macchina attualmente in esecuzione. Salvano abbastanza stato (di solito, ma non sempre, nello stack) per consentire di riprendere l' esecuzione in seguito, come se nulla fosse accaduto (l'istruzione interrotta verrà riavviata da zero, di solito). Quindi iniziano a eseguire un gestore di interrupt , che è solo più codice macchina, ma posizionato in una posizione speciale in modo che la CPU sappia dove si trova in anticipo. I gestori di interrupt fanno sempre parte del kernel del sistema operativo: il componente che gira con il massimo privilegio ed è responsabile della supervisione dell'esecuzione di tutti gli altri componenti. 1,2

Gli interrupt possono essere sincroni , nel senso che vengono attivati ​​dalla CPU stessa come risposta diretta a qualcosa che l'istruzione attualmente in esecuzione ha fatto, o asincroni , il che significa che si verificano in un momento imprevedibile a causa di un evento esterno, come i dati che arrivano sulla rete porta. Alcune persone riservano il termine "interruzione" per interruzioni asincrone e chiamano invece interruzioni sincrone "trappole", "guasti" o "eccezioni", ma quelle parole hanno tutti altri significati, quindi continuerò con "interruzione sincrona".

Ora, i sistemi operativi più moderni hanno una nozione di processi . Nella sua forma più semplice, si tratta di un meccanismo in base al quale il computer può eseguire più di un programma contemporaneamente, ma è anche un aspetto chiave di come i sistemi operativi configurano la protezione della memoria , che è una caratteristica della maggior parte (ma, ahimè, ancora non tutte ) CPU moderne. Si accompagna alla memoria virtuale, che è la capacità di modificare la mappatura tra indirizzi di memoria e posizioni effettive nella RAM. La protezione della memoria consente al sistema operativo di assegnare a ciascun processo il proprio blocco privato di RAM, a cui solo lui può accedere. Inoltre, consente al sistema operativo (che agisce per conto di alcuni processi) di designare regioni della RAM come di sola lettura, eseguibili, condivise tra un gruppo di processi cooperanti, ecc. Ci sarà anche un pezzo di memoria accessibile solo dal kernel. 3

Finché ogni processo accede alla memoria solo nei modi in cui la CPU è configurata per consentire, la protezione della memoria è invisibile. Quando un processo infrange le regole, la CPU genererà un interrupt sincrono, chiedendo al kernel di sistemare le cose. Accade regolarmente che il processo non abbia veramente violato le regole, solo il kernel deve fare un po 'di lavoro prima che il processo possa continuare. Ad esempio, se una pagina della memoria di un processo deve essere "sfrattata" nel file di scambio per liberare spazio nella RAM per qualcos'altro, il kernel contrassegnerà quella pagina inaccessibile. La prossima volta che il processo tenta di utilizzarlo, la CPU genererà un interrupt di protezione della memoria; il kernel recupererà la pagina dallo swap, la rimetterà dove era, la renderà nuovamente accessibile e riprenderà l'esecuzione.

Ma supponiamo che il processo abbia davvero infranto le regole. Ha tentato di accedere a una pagina a cui non è mai stata mappata alcuna RAM, oppure ha tentato di eseguire una pagina contrassegnata come non contenente codice macchina o altro. La famiglia di sistemi operativi generalmente conosciuta come "Unix" usa tutti i segnali per affrontare questa situazione. 4 I segnali sono simili agli interrupt, ma sono generati dal kernel e messi in campo dai processi, piuttosto che essere generati dall'hardware e messi in campo dal kernel. I processi possono definire gestori di segnalinel proprio codice e dire al kernel dove si trovano. Quei gestori di segnale verranno quindi eseguiti, interrompendo il normale flusso di controllo, quando necessario. Tutti i segnali hanno un numero e due nomi, uno dei quali è un acronimo criptico e l'altro una frase leggermente meno enigmatica. Il segnale che viene generato quando un processo infrange le regole di protezione della memoria è (per convenzione) il numero 11, e i suoi nomi sono SIGSEGV"Errore di segmentazione". 5,6

Una differenza importante tra segnali e interruzioni è che esiste un comportamento predefinito per ogni segnale. Se il sistema operativo non riesce a definire i gestori per tutti gli interrupt, questo è un bug nel sistema operativo e l'intero computer si arresta in modo anomalo quando la CPU tenta di richiamare un gestore mancante. Ma i processi non hanno l'obbligo di definire i gestori di segnali per tutti i segnali. Se il kernel genera un segnale per un processo e quel segnale è stato lasciato al suo comportamento predefinito, il kernel andrà avanti e farà qualunque cosa sia il default e non disturberà il processo. La maggior parte dei comportamenti predefiniti dei segnali sono "non fare nulla" o "terminare questo processo e forse produrre anche un dump principale". SIGSEGVè uno di questi ultimi.

Quindi, per ricapitolare, abbiamo un processo che ha infranto le regole di protezione della memoria. La CPU ha sospeso il processo e ha generato un interrupt sincrono. Il kernel ha messo in campo quell'interruzione e ha generato un SIGSEGVsegnale per il processo. Supponiamo che il processo non abbia impostato un gestore di segnali per SIGSEGV, quindi il kernel esegue il comportamento predefinito, che è quello di terminare il processo. Ciò ha gli stessi effetti della _exitchiamata di sistema: i file aperti vengono chiusi, la memoria viene deallocata, ecc.

Fino a questo punto nulla ha stampato alcun messaggio che un essere umano può vedere e la shell (o, più in generale, il processo genitore del processo appena terminato) non è stata coinvolta affatto. SIGSEGVva al processo che ha infranto le regole, non il suo genitore. Il passaggio successivo nella sequenza, tuttavia, è notificare al processo padre che il relativo figlio è stato terminato. Questo può avvenire in molti modi diversi, di cui il più semplice è quando il genitore è già in attesa per questa notifica, utilizzando uno dei waitchiamate di sistema ( wait, waitpid, wait4, ecc). In tal caso, il kernel farà semplicemente tornare quella chiamata di sistema e fornirà al processo genitore un numero di codice chiamato stato di uscita. 7 Lo stato di uscita informa il genitore del motivo per cui il processo figlio è stato terminato; in questo caso, imparerà che il bambino è stato interrotto a causa del comportamento predefinito di un SIGSEGVsegnale.

Il processo genitore può quindi segnalare l'evento a un essere umano stampando un messaggio; i programmi di shell lo fanno quasi sempre. Il tuo crshnon include il codice per farlo, ma succede comunque, perché la routine della libreria C systemesegue una shell con tutte le funzionalità /bin/sh, "under the hood". crshè il nonno in questo scenario; la notifica del processo genitore viene messa in campo da /bin/sh, che stampa il suo solito messaggio. Quindi /bin/shsi chiude da solo, poiché non ha altro da fare e l'implementazione della libreria C systemriceve tale notifica di uscita. Puoi vedere quella notifica di uscita nel tuo codice, controllando il valore di ritorno disystem; ma non ti dirà che il processo del nipote è morto su un segfault, perché è stato consumato dal processo di shell intermedio.


Le note

  1. Alcuni sistemi operativi non implementano i driver di dispositivo come parte del kernel; tuttavia, tutti i gestori di interrupt devono comunque far parte del kernel, così come il codice che configura la protezione della memoria, poiché l'hardware non consente a nulla se non al kernel di fare queste cose.

  2. Potrebbe esserci un programma chiamato "hypervisor" o "gestore della macchina virtuale" che è ancora più privilegiato rispetto al kernel, ma ai fini di questa risposta può essere considerato parte dell'hardware .

  3. Il kernel è un programma di , ma è non è un processo; è più simile a una biblioteca. Tutti i processi eseguono parti del codice del kernel, di volta in volta, oltre al proprio codice. Potrebbero esserci un certo numero di "thread del kernel" che eseguono solo il codice del kernel, ma qui non ci riguardano.

  4. L'unico e unico sistema operativo che probabilmente dovrai affrontare più che non può essere considerato un'implementazione di Unix è, ovviamente, Windows. Non utilizza segnali in questa situazione. (In effetti, non ha segnali; su Windows l' <signal.h>interfaccia è completamente falsata dalla libreria C.) Utilizza invece qualcosa chiamato " gestione delle eccezioni strutturata ".

  5. Alcune violazioni di protezione della memoria generano SIGBUS("Errore bus") anziché SIGSEGV. La linea tra i due non è specificata e varia da sistema a sistema. Se hai scritto un programma che definisce un gestore per SIGSEGV, probabilmente è una buona idea definire lo stesso gestore SIGBUS.

  6. "Errore di segmentazione" era il nome dell'interrupt generato per le violazioni della protezione della memoria da uno dei computer che eseguivano Unix originale , probabilmente il PDP-11 . " Segmentazione " è un tipo di protezione della memoria, ma al giorno d'oggi il termine " errore di segmentazione " si riferisce genericamente a qualsiasi tipo di violazione della protezione della memoria.

  7. Tutti gli altri modi in cui il processo genitore potrebbe essere notificato di un figlio che è terminato, finire con il genitore che chiama waite riceve uno stato di uscita. È solo che qualcos'altro accade prima.


@zvol: ad 2) Non credo sia giusto dire che la CPU sappia qualcosa sui processi. Dovresti dire che invoca un gestore di interrupt, che trasferisce il controllo.
user323094

9
@ user323094 Le moderne CPU multicore conoscono davvero un po 'i processi; abbastanza in modo che, in questa situazione, possano sospendere solo il thread di esecuzione che ha innescato l'errore di protezione della memoria. Inoltre, stavo cercando di non entrare nei dettagli di basso livello. Dal punto di vista del programmatore dello spazio utente, la cosa più importante da capire sul passaggio 2 è che è l' hardware che rileva le violazioni della protezione della memoria; tanto meno la precisa divisione del lavoro tra hardware, firmware e sistema operativo quando si tratta di identificare il "processo offensivo".
zwol,

Un'altra sottigliezza che potrebbe confondere un lettore ingenuo è "Il kernel invia al processo offensivo un segnale SIGSEGV". che utilizza il solito gergo, ma in realtà significa che il kernel dice si a che fare con foo segnale sulla barra di processo (vale a dire il codice userland non essere coinvolti a meno che non ci sia un gestore di segnale installato, una domanda che si risolve dal kernel). Qualche volta preferisco "alza un segnale SIGSEGV sul processo" per questo motivo.
Dmckee,

2
La differenza significativa tra SIGBUS (errore bus) e SIGSEGV (errore di segmentazione) è questa: SIGSEGV si verifica quando la CPU sa che non dovresti accedere a un indirizzo (e quindi non fa alcuna richiesta di bus di memoria esterna). SIGBUS si verifica quando la CPU rileva il problema di indirizzamento solo dopo aver inserito la richiesta sul bus di indirizzo esterno. Ad esempio, chiedere un indirizzo fisico a cui non risponde nulla sul bus o chiedere di leggere i dati su un confine erroneamente allineato (che richiederebbe due richieste fisiche per ottenere invece di uno)
Stuart Caie

2
@StuartCaie Stai descrivendo il comportamento degli interrupt ; in effetti, molte CPU fanno la distinzione che tracci (anche se alcune no, e la linea tra le due varia). I segnali SIGSEGV e SIGBUS, tuttavia, non sono mappati in modo affidabile a quelle due condizioni a livello di CPU. L'unica condizione in cui POSIX richiede SIGBUS anziché SIGSEGV è quando si inserisce mmapun file in un'area di memoria più grande del file e quindi si accede a "pagine intere" oltre la fine del file. (In caso contrario POSIX è abbastanza vago quando si verificano SIGSEGV / SIGBUS / SIGILL / ecc.)
zwol,

42

La shell ha davvero qualcosa a che fare con quel messaggio, e crshindirettamente chiama una shell, che è probabilmente bash.

Ho scritto un piccolo programma C che segna sempre i guasti:

#include <stdio.h>

int
main(int ac, char **av)
{
        int *i = NULL;

        *i = 12;

        return 0;
}

Quando lo eseguo dalla mia shell predefinita zsh, ottengo questo:

4 % ./segv
zsh: 13512 segmentation fault  ./segv

Quando lo eseguo bash, ottengo ciò che hai notato nella tua domanda:

bediger@flq123:csrc % ./segv
Segmentation fault

Stavo per scrivere un gestore di segnale nel mio codice, poi mi sono reso conto che la system()chiamata in libreria usata da crshexec è una shell, /bin/shsecondo man 3 system. Questo /bin/shsta quasi sicuramente stampando "Errore di segmentazione", dal momento che crshcertamente non lo è.

Se si riscrive crshper utilizzare la execve()chiamata di sistema per eseguire il programma, non verrà visualizzata la stringa "Errore di segmentazione". Viene dalla shell invocata da system().


5
Ne stavo solo discutendo con Dietrich Epp. Ho hackerato insieme una versione di crsh che usa execvpe ho fatto di nuovo il test per scoprire che mentre la shell non si blocca ancora (il che significa che SIGSEGV non viene mai inviato alla shell), non stampa "Segmentation Fault". Nulla è stampato affatto. Questo sembra indicare che la shell rileva quando i suoi processi figlio vengono uccisi ed è responsabile della stampa di "Errore di segmentazione" (o di una sua variante).
Braden Best,

2
@BradenBest - Ho fatto la stessa cosa, il mio codice è più sciatto del tuo codice. Non ho ricevuto nessun messaggio e la mia shell ancora più scadente non stampa nulla. Ho usato waitpid()su ogni fork / exec, e restituisce un valore diverso per i processi che hanno un errore di segmentazione, rispetto ai processi che terminano con stato 0.
Bruce Ediger,

21

Non riesco a trovare alcuna informazione al riguardo a parte "la MMU della CPU invia un segnale" e "il kernel lo indirizza al programma offensivo, terminandolo".

Questo è un po 'un sommario confuso. Il meccanismo del segnale Unix è completamente diverso dagli eventi specifici della CPU che avviano il processo.

In generale, quando si accede a un indirizzo errato (o scritto in un'area di sola lettura, si tenta di eseguire una sezione non eseguibile, ecc.), La CPU genererà alcuni eventi specifici della CPU (sulle architetture tradizionali non VM era chiamata violazione della segmentazione, poiché ogni "segmento" (tradizionalmente, il "testo" eseguibile di sola lettura, i "dati" scrivibili ea lunghezza variabile e lo stack tradizionalmente all'estremità opposta della memoria) avevano un intervallo fisso di indirizzi - su un'architettura moderna è più probabile che sia un errore di pagina [per memoria non mappata] o una violazione di accesso [per problemi di lettura, scrittura ed esecuzione dei permessi], e mi concentrerò su questo per il resto della risposta).

Ora, a questo punto, il kernel può fare diverse cose. Gli errori di pagina vengono generati anche per una memoria valida ma non caricata (ad esempio, scambiata o in un file mmapped, ecc.), E in questo caso il kernel mapperà la memoria e quindi riavvierà il programma utente dall'istruzione che ha causato il errore. Altrimenti, invia un segnale. Questo non "dirige [l'evento originale] al programma offensivo", poiché il processo di installazione di un gestore di segnale è diverso e per lo più indipendente dall'architettura, rispetto al caso in cui il programma simulasse l'installazione di un gestore di interrupt.

Se nel programma utente è installato un gestore di segnale, ciò significa creare un frame stack e impostare la posizione di esecuzione del programma utente sul gestore di segnale. Lo stesso vale per tutti i segnali, ma nel caso di una violazione di segmentazione le cose sono generalmente disposte in modo tale che se il gestore del segnale ritorna, riavvierà l'istruzione che ha causato l'errore. Il programma utente potrebbe aver corretto l'errore, ad esempio mappando la memoria sull'indirizzo incriminato - dipende dall'architettura se ciò è possibile). Il gestore del segnale può anche saltare in una posizione diversa nel programma (in genere tramite longjmp o lanciando un'eccezione), per interrompere qualsiasi operazione abbia causato l'accesso alla memoria errato .

Se nel programma utente non è installato un gestore dei segnali, viene semplicemente terminato. Su alcune architetture, se il segnale viene ignorato, è possibile che le istruzioni vengano riavviate ripetutamente, causando un loop infinito.


+1, unica risposta che aggiunge qualcosa a quello accettato. Bella descrizione della storia della "segmentazione". Curiosità: x86 in realtà ha ancora limiti di segmento in modalità protetta a 32 bit (con o senza paging (memoria virtuale) abilitata), quindi le istruzioni che possono essere generate dalla memoria di accesso #PF(fault-code)(errore di pagina) o #GP(0)("Se un indirizzo effettivo di operando di memoria è esterno al CS, Limite di segmento DS, ES, FS o GS. "). La modalità a 64 bit elimina i controlli del limite di segmento, poiché i sistemi operativi utilizzavano semplicemente il paging e un modello di memoria piatta per lo spazio utente.
Peter Cordes,

In realtà, credo che la maggior parte dei sistemi operativi su x86 utilizzino l'impaginazione segmentata: un gruppo di grandi segmenti all'interno di uno spazio di indirizzi piatto e impaginato. Ecco come proteggere e mappare la memoria del kernel in ogni spazio degli indirizzi: gli anelli (livelli di protezione) sono collegati ai segmenti, non alle pagine
Lorenzo Dematté,

Inoltre, su NT (ma mi piacerebbe sapere se sulla maggior parte degli Unix è lo stesso!) "L'errore di segmentazione" potrebbe accadere abbastanza spesso: c'è un segmento protetto da 64k all'inizio dello spazio utente, quindi la dereferenziazione di un puntatore NULL genera un (corretto?) errore di segmentazione
Lorenzo Dematté,

1
@ LorenzoDematté Sì, tutti gli Unix moderni o quasi lasceranno un pezzo di indirizzi permanentemente non mappati all'inizio dello spazio degli indirizzi per catturare le dereferenze NULL. Può essere piuttosto grande: sui sistemi a 64 bit, infatti, potrebbe essere di quattro gigabyte , quindi il troncamento accidentale dei puntatori a 32 bit verrà catturato prontamente. Tuttavia, la segmentazione in senso stretto x86 è a malapena utilizzata; c'è un segmento piatto per lo spazio utente e uno per il kernel, e forse un paio per trucchi speciali come ottenere un certo uso da FS e GS.
zwol,

1
@ LorenzoDematté NT utilizza le eccezioni anziché i segnali; in questo caso STATUS_ACCESS_VIOLATION.
Casuale 832,

18

Un errore di segmentazione è un accesso a un indirizzo di memoria non consentito (non parte del processo o tentativo di scrivere dati di sola lettura o eseguire dati non eseguibili, ...). Ciò viene catturato dalla MMU (Memory Management Unit, oggi parte della CPU), causando un interrupt. L'interruzione è gestita dal kernel, che invia un SIGSEGFAULTsegnale (vedi signal(2)ad esempio) al processo offensivo. Il gestore predefinito per questo segnale scarica il core (vedi core(5)) e termina il processo.

La shell non ha assolutamente alcuna mano in questo.


3
Quindi la tua libreria C, come glibc su un desktop, definisce la stringa?
Drewbenn,

7
Vale anche la pena notare che SIGSEGV può essere gestito / ignorato. Quindi è possibile scrivere un programma che non viene chiuso da esso. La Java Virtual Machine è un esempio notevole che utilizza SIGSEGV internamente per scopi diversi, come menzionato qui: stackoverflow.com/questions/3731784/…
Karol Nowak,

2
Allo stesso modo, su Windows, .NET non si preoccupa di aggiungere controlli puntatore null nella maggior parte dei casi, ma rileva solo le violazioni dell'accesso (equivalenti a segfaults).
immibis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.