Abgesehen von "Die MMU der CPU sendet ein Signal" und "Der Kernel leitet es an das fehlerhafte Programm weiter und beendet es", kann ich anscheinend keine Informationen dazu finden.
Ich nahm an, dass es wahrscheinlich das Signal an die Shell sendet und die Shell es durch Beenden des fehlerhaften Prozesses und Drucken verarbeitet "Segmentation fault"
. Also habe ich diese Annahme getestet, indem ich eine extrem minimale Shell geschrieben habe, die ich crsh (crap shell) nenne . Diese Shell nimmt nur Benutzereingaben entgegen und leitet sie an die system()
Methode weiter.
#include <stdio.h>
#include <stdlib.h>
int main(){
char cmdbuf[1000];
while (1){
printf("Crap Shell> ");
fgets(cmdbuf, 1000, stdin);
system(cmdbuf);
}
}
Also habe ich diese Shell in einem leeren Terminal ausgeführt (ohne bash
darunter zu laufen). Dann fuhr ich fort, ein Programm auszuführen, das einen Segfault erzeugt. Wenn meine Annahmen korrekt wären, würde dies entweder a) abstürzen crsh
, xterm schließen, b) nicht drucken "Segmentation fault"
oder c) beides.
braden@system ~/code/crsh/ $ xterm -e ./crsh
Crap Shell> ./segfault
Segmentation fault
Crap Shell> [still running]
Zurück zu Punkt eins, denke ich. Ich habe gerade gezeigt, dass dies nicht von der Shell, sondern vom darunterliegenden System ausgeführt wird. Wie wird "Segmentierungsfehler" überhaupt gedruckt? "Wer" macht das? Der Kernel? Etwas anderes? Wie breitet sich das Signal und alle seine Nebenwirkungen von der Hardware aus, bis das Programm schließlich beendet wird?
quelle
crsh
ist eine großartige Idee für diese Art des Experimentierens. Vielen Dank, dass Sie uns alle darüber und über die dahinter stehende Idee informiert haben.crsh
, dachte ich, es würde "Absturz" ausgesprochen. Ich bin mir nicht sicher, ob das ein ebenso passender Name ist.system()
unter der Haube tut. Es stellt sich heraus, dasssystem()
ein Shell-Prozess ausgelöst wird! Ihr Shell-Prozess erzeugt also einen anderen Shell-Prozess und dieser Shell-Prozess (wahrscheinlich/bin/sh
oder so ähnlich) ist derjenige, der das Programm ausführt. Die Art/bin/sh
und Weise oder Arbeitsweisebash
ist durch die Verwendung vonfork()
undexec()
(oder einer anderen Funktion in derexecve()
Familie).man 2 wait
, sie enthält die MakrosWIFSIGNALED()
undWTERMSIG()
.(WIFSIGNALED(status) && WTERMSIG(status) == 11)
, damit er etwas doofes druckt ("YOU DUN GOOFED AND TRIGGERED A SEGFAULT"
). Als ich dassegfault
Programm von innen heraus ausführtecrsh
, druckte es genau das. Währenddessen wird bei Befehlen, die normalerweise beendet werden, keine Fehlermeldung ausgegeben.Antworten:
Alle modernen CPUs haben die Fähigkeit, den gerade ausgeführten Maschinenbefehl zu unterbrechen . Sie speichern genügend Status (normalerweise, aber nicht immer, auf dem Stapel), um die Ausführung später wieder aufzunehmen , als wäre nichts geschehen (normalerweise wird der unterbrochene Befehl von Grund auf neu gestartet). Dann starten sie die Ausführung eines Interrupt-Handlers , der nur aus Maschinencode besteht, aber an einer bestimmten Stelle platziert ist, damit die CPU weiß, wo er sich im Voraus befindet. Interrupt-Handler sind immer Teil des Kernels des Betriebssystems: Die Komponente, die mit den größten Berechtigungen ausgeführt wird und für die Überwachung der Ausführung aller anderen Komponenten verantwortlich ist. 1,2
Interrupts können synchron sein , was bedeutet, dass sie von der CPU selbst als direkte Antwort auf etwas ausgelöst werden, das der gerade ausgeführte Befehl ausgeführt hat, oder asynchron , was bedeutet, dass sie zu einem unvorhersehbaren Zeitpunkt aufgrund eines externen Ereignisses auftreten, wie z. B. Daten, die im Netzwerk ankommen Hafen. Einige Leute reservieren den Begriff "Interrupt" für asynchrone Interrupts und nennen synchrone Interrupts stattdessen "Traps", "Fehler" oder "Ausnahmen", aber diese Wörter haben alle andere Bedeutungen, also bleibe ich bei "synchronem Interrupt".
Heutzutage kennen die meisten modernen Betriebssysteme Prozesse . Im Grunde ist dies ein Mechanismus, mit dem der Computer mehr als ein Programm gleichzeitig ausführen kann, aber es ist auch ein wesentlicher Aspekt der Konfiguration des Speicherschutzes durch Betriebssysteme , der ein Merkmal der meisten (aber leider noch nicht alle ) modernen CPUs. Es geht zusammen mit dem virtuellen SpeicherDies ist die Möglichkeit, die Zuordnung zwischen Speicheradressen und tatsächlichen Speicherorten im RAM zu ändern. Der Speicherschutz ermöglicht es dem Betriebssystem, jedem Prozess einen eigenen privaten RAM-Block zuzuweisen, auf den nur er zugreifen kann. Außerdem kann das Betriebssystem (das im Auftrag eines bestimmten Prozesses handelt) RAM-Bereiche als schreibgeschützt, ausführbar, für eine Gruppe kooperierender Prozesse freigegeben usw. kennzeichnen. Außerdem wird ein Teil des Arbeitsspeichers vorhanden sein, auf den nur der Zugriff möglich ist Kernel. 3
Solange jeder Prozess nur so auf den Speicher zugreift, wie es die CPU zulässt, ist der Speicherschutz unsichtbar. Wenn ein Prozess gegen die Regeln verstößt, generiert die CPU einen synchronen Interrupt und fordert den Kernel auf, die Dinge zu klären. Es kommt regelmäßig vor, dass der Prozess nicht wirklich gegen die Regeln verstößt. Nur der Kernel muss etwas arbeiten, bevor der Prozess fortgesetzt werden kann. Wenn beispielsweise eine Seite des Arbeitsspeichers eines Prozesses in die Auslagerungsdatei "entfernt" werden muss, um Speicherplatz im RAM für etwas anderes freizugeben, markiert der Kernel diese Seite als unzugänglich. Wenn der Prozess das nächste Mal versucht, ihn zu verwenden, generiert die CPU einen Speicherschutz-Interrupt. Der Kernel ruft die Seite aus dem Auslagerungsmodus ab, legt sie wieder an ihrem ursprünglichen Ort ab, markiert sie als wieder zugänglich und setzt die Ausführung fort.
Angenommen, der Prozess hat wirklich gegen die Regeln verstoßen. Es wurde versucht, auf eine Seite zuzugreifen, der noch kein RAM zugeordnet war, oder es wurde versucht, eine Seite auszuführen, die als nicht mit Maschinencode gekennzeichnet ist, oder was auch immer. Die Betriebssystemfamilie, die allgemein als "Unix" bekannt ist, verwendet alle Signale , um mit dieser Situation umzugehen. 4 Signale ähneln Interrupts, werden jedoch vom Kernel generiert und von Prozessen abgefangen, anstatt von der Hardware generiert und vom Kernel abgefangen zu werden. Prozesse können Signalhandler definierenin ihrem eigenen Code, und teilen Sie dem Kernel mit, wo sie sich befinden. Diese Signalhandler werden dann ausgeführt und unterbrechen bei Bedarf den normalen Steuerungsfluss. Alle Signale haben eine Nummer und zwei Namen, von denen einer ein kryptisches Akronym und der andere eine etwas weniger kryptische Phrase ist. Das Signal, das generiert wird, wenn ein Prozess die Speicherschutzregeln verletzt, ist (gemäß Konvention) Nummer 11, und seine Namen sind
SIGSEGV
und "Segmentierungsfehler". 5,6Ein wichtiger Unterschied zwischen Signalen und Interrupts besteht darin, dass es für jedes Signal ein Standardverhalten gibt . Wenn das Betriebssystem keine Handler für alle Interrupts definiert, ist dies ein Fehler im Betriebssystem, und der gesamte Computer stürzt ab, wenn die CPU versucht, einen fehlenden Handler aufzurufen. Prozesse sind jedoch nicht verpflichtet, Signalhandler für alle Signale zu definieren. Wenn der Kernel ein Signal für einen Prozess generiert und dieses Signal auf seinem Standardverhalten belassen wurde, wird der Kernel einfach weitermachen und alles tun, was der Standard ist, und den Prozess nicht stören. Das Standardverhalten der meisten Signale ist entweder "nichts tun" oder "diesen Prozess beenden und möglicherweise auch einen Core-Dump erzeugen".
SIGSEGV
ist einer der letzteren.Um es noch einmal zusammenzufassen, wir haben einen Prozess, der die Speicherschutzregeln gebrochen hat. Die CPU hat den Prozess angehalten und einen synchronen Interrupt generiert. Der Kernel hat das unterbrochen und ein
SIGSEGV
Signal für den Prozess generiert . Angenommen, der Prozess hat keinen Signal-Handler für eingerichtetSIGSEGV
, sodass der Kernel das Standardverhalten ausführt, das darin besteht, den Prozess zu beenden. Dies hat die gleichen Auswirkungen wie der_exit
Systemaufruf: Geöffnete Dateien werden geschlossen, Speicher wird freigegeben usw.Bis zu diesem Zeitpunkt wurden keine Nachrichten ausgedruckt, die ein Mensch sehen kann, und die Shell (oder allgemein der übergeordnete Prozess des gerade abgebrochenen Prozesses) war überhaupt nicht beteiligt.
SIGSEGV
Geht zu dem Prozess, der die Regeln verletzt hat, nicht zu seinem übergeordneten Element. Der nächste Schritt in der Sequenz besteht jedoch darin, dem übergeordneten Prozess mitzuteilen, dass sein untergeordnetes Element beendet wurde. Dies kann auf verschiedene Weise geschehen, von denen die einfachste ist , wenn die Eltern bereits für diese Meldung warten, eines der Verwendung vonwait
Systemaufrufen (wait
,waitpid
,wait4
, usw.). In diesem Fall veranlasst der Kernel lediglich die Rückgabe dieses Systemaufrufs und versieht den übergeordneten Prozess mit einer Codenummer, die als Exit-Status bezeichnet wird. 7 Der Beendigungsstatus informiert den Elternteil darüber, warum der Kindprozess beendet wurde. In diesem Fall wird festgestellt, dass das Kind aufgrund des Standardverhaltens einesSIGSEGV
Signals beendet wurde.Der übergeordnete Prozess kann dann das Ereignis einem Menschen melden, indem er eine Nachricht druckt; Shell-Programme tun dies fast immer. Sie enthalten
crsh
keinen Code, um das zu tun, aber es passiert trotzdem, weil die C-Bibliotheksroutinesystem
eine voll funktionsfähige Shell/bin/sh
"unter der Haube" ausführt.crsh
ist der Großelternteil in diesem Szenario; Die Benachrichtigung über den übergeordneten Prozess wird durch gekennzeichnet/bin/sh
, wodurch die übliche Nachricht gedruckt wird. Dann wird es/bin/sh
selbst beendet, da es nichts mehr zu tun hat, und die Implementierung der C-Bibliothek vonsystem
empfängt diese Beendigungsbenachrichtigung. Sie können diese Beendigungsbenachrichtigung in Ihrem Code sehen, indem Sie den Rückgabewert von überprüfensystem
; Es wird Ihnen jedoch nicht mitgeteilt, dass der Enkelprozess aufgrund eines Segfault-Vorgangs gestorben ist, da dieser durch den Zwischen-Shell-Prozess verbraucht wurde.Fußnoten
Einige Betriebssysteme implementieren keine Gerätetreiber als Teil des Kernels. Alle Interrupt-Handler müssen jedoch weiterhin Teil des Kernels sein, ebenso wie der Code, der den Speicherschutz konfiguriert, da die Hardware nur dem Kernel gestattet, diese Aufgaben auszuführen .
Es kann ein Programm geben, das als "Hypervisor" oder "Virtual Machine Manager" bezeichnet wird und noch privilegierter ist als der Kernel. Für diese Antwort kann es jedoch als Teil der Hardware betrachtet werden .
Der Kernel ist ein Programm , aber kein Prozess. es ist eher wie eine Bibliothek. Alle Prozesse führen von Zeit zu Zeit zusätzlich zu ihrem eigenen Code Teile des Kernel-Codes aus. Es kann eine Reihe von "Kernel-Threads" geben, die nur Kernel-Code ausführen, die uns hier jedoch nicht betreffen.
Das einzige Betriebssystem, mit dem Sie wahrscheinlich mehr zu tun haben, das nicht als Implementierung von Unix angesehen werden kann, ist natürlich Windows. In dieser Situation werden keine Signale verwendet. ( In der Tat ist es nicht haben Signale, unter Windows die
<signal.h>
Schnittstelle vollständig durch die C - Bibliothek gefälscht ist.) Es nutzt etwas „genannt strukturierte Ausnahmebehandlung “ statt.Einige Speicherschutzverletzungen erzeugen
SIGBUS
("Busfehler") stattSIGSEGV
. Die Linie zwischen den beiden ist unterbestimmt und variiert von System zu System. Wenn Sie ein Programm geschrieben haben, das einen Handler für definiertSIGSEGV
, ist es wahrscheinlich eine gute Idee, denselben Handler für zu definierenSIGBUS
."Segmentierungsfehler" war der Name des Interrupts, der bei Verstößen gegen den Speicherschutz von einem der Computer generiert wurde, auf denen das ursprüngliche Unix ausgeführt wurde , wahrscheinlich der PDP-11 . „ Segmentierung “ ist ein Typ von Speicherschutz, aber heutzutage der Begriff „Segmentierung Fehler “ bezieht sich allgemein auf jede Art von Speicherschutzverletzung.
Alle anderen Möglichkeiten, wie der übergeordnete Prozess benachrichtigt werden kann, wenn ein Kind beendet wurde, führen dazu, dass der übergeordnete Prozess anruft
wait
und einen Beendigungsstatus erhält. Es ist nur so, dass zuerst etwas anderes passiert.quelle
mmap
eine Datei in einen Speicherbereich verschieben, der größer als die Datei ist, und dann über das Dateiende hinaus auf "ganze Seiten" zugreifen. (POSIX ist ansonsten ziemlich vage, wann SIGSEGV / SIGBUS / SIGILL / etc passieren.)Die Shell hat in der Tat etwas mit dieser Nachricht zu tun und
crsh
ruft indirekt eine Shell auf, was wahrscheinlich auch so istbash
.Ich habe ein kleines C-Programm geschrieben, das immer Fehler ausgibt:
Wenn ich es von meiner Standard-Shell aus starte
zsh
, erhalte ich Folgendes:Wenn ich es starte
bash
, bekomme ich das, was Sie in Ihrer Frage notiert haben:Ich wollte einen Signal-Handler in meinen Code schreiben, dann wurde mir klar, dass der
system()
voncrsh
execs verwendete Bibliotheksaufruf/bin/sh
laut eine Shell istman 3 system
. Das/bin/sh
ist mit ziemlicher Sicherheit der Ausdruck "Segmentierungsfehler", da dies mit ziemlicher Sicherheit nicht der Fallcrsh
ist.Wenn Sie erneut schreiben
crsh
, um denexecve()
Systemaufruf zum Ausführen des Programms zu verwenden, wird die Zeichenfolge "Segmentierungsfehler" nicht angezeigt. Es kommt von der Shell, die von aufgerufen wirdsystem()
.quelle
execvp
und den Test erneut durchgeführt, um festzustellen, dass die Shell zwar immer noch nicht abstürzt (was bedeutet, dass SIGSEGV niemals an die Shell gesendet wird), aber keinen "Segmentierungsfehler" ausgibt. Es wird überhaupt nichts gedruckt. Dies scheint darauf hinzudeuten, dass die Shell erkennt, wann ihre untergeordneten Prozesse beendet werden, und für das Drucken von "Segmentierungsfehler" (oder einer Variante davon) verantwortlich ist.waitpid()
auf jedem Fork / Exec einen anderen Wert für Prozesse mit einem Segmentierungsfehler zurückgegeben als für Prozesse, die mit dem Status 0 beendet werden.Dies ist eine verstümmelte Zusammenfassung. Der Unix-Signalmechanismus unterscheidet sich grundlegend von den CPU-spezifischen Ereignissen, die den Prozess starten.
Wenn auf eine ungültige Adresse zugegriffen wird (oder in einen schreibgeschützten Bereich geschrieben wird, versucht wird, einen nicht ausführbaren Abschnitt auszuführen usw.), generiert die CPU im Allgemeinen ein CPU-spezifisches Ereignis (auf herkömmlichen Nicht-VM-Architekturen war dies der Fall) Dies wird als Segmentierungsverletzung bezeichnet, da jedes "Segment" (normalerweise der schreibgeschützte ausführbare "Text", die beschreibbaren Daten mit variabler Länge und der Stapel, der sich normalerweise am anderen Ende des Speichers befindet) einen festen Adressbereich hat. In einer modernen Architektur ist es eher ein Seitenfehler (für nicht zugeordneten Speicher) oder eine Zugriffsverletzung (für Lese-, Schreib- und Ausführungsberechtigungsprobleme), und ich werde mich für den Rest der Antwort darauf konzentrieren.
Jetzt kann der Kernel verschiedene Dinge tun. Seitenfehler werden auch für den Speicher generiert, der gültig, aber nicht geladen ist (z. B. ausgelagert oder in einer Mmap-Datei usw.). In diesem Fall ordnet der Kernel den Speicher zu und startet das Benutzerprogramm von der Anweisung aus, die den Fehler verursacht hat Error. Andernfalls wird ein Signal gesendet. Dies "leitet [das ursprüngliche Ereignis] nicht direkt an das fehlerhafte Programm", da der Prozess zum Installieren eines Signal-Handlers anders und größtenteils architekturunabhängig ist, als wenn das Programm die Installation eines Interrupt-Handlers simulieren würde.
Wenn auf dem Benutzerprogramm ein Signalhandler installiert ist, bedeutet dies, dass ein Stapelrahmen erstellt und die Ausführungsposition des Benutzerprogramms auf den Signalhandler festgelegt wird. Das Gleiche gilt für alle Signale, aber im Fall einer Segmentierungsverletzung werden die Dinge im Allgemeinen so angeordnet, dass der Befehl, der den Fehler verursacht hat, neu gestartet wird, wenn der Signalhandler zurückkehrt. Möglicherweise hat das Anwenderprogramm den Fehler behoben, z. B. indem der Speicher der betreffenden Adresse zugeordnet wurde (es hängt von der Architektur ab, ob dies möglich ist). Der Signalhandler kann auch zu einer anderen Stelle im Programm springen (normalerweise über longjmp oder durch Auslösen einer Ausnahme), um die Operation abzubrechen, die den fehlerhaften Speicherzugriff verursacht hat.
Wenn im Anwenderprogramm kein Signalhandler installiert ist, wird es einfach beendet. Bei einigen Architekturen wird der Befehl möglicherweise immer wieder neu gestartet, wenn das Signal ignoriert wird, was zu einer Endlosschleife führt.
quelle
#PF(fault-code)
(Seitenfehler) oder#GP(0)
("Wenn sich eine effektive Adresse eines Speicheroperanden außerhalb der CS befindet, DS-, ES-, FS- oder GS-Segmentlimit. "). Im 64-Bit-Modus werden Segmentlimitprüfungen gestrichen, da die Betriebssysteme stattdessen nur Paging und ein flaches Speichermodell für den Benutzerbereich verwendet haben.Ein Segmentierungsfehler ist ein Zugriff auf eine Speicheradresse, die nicht zulässig ist (nicht Teil des Prozesses oder der Versuch, schreibgeschützte Daten zu schreiben oder nicht ausführbare Daten auszuführen, ...). Dies wird von der MMU (Memory Management Unit, heute Teil der CPU) abgefangen und verursacht einen Interrupt. Der Interrupt wird vom Kernel behandelt, der ein
SIGSEGFAULT
Signal (siehesignal(2)
zum Beispiel) an den fehlerhaften Prozess sendet . Der Standardhandler für dieses Signal gibt einen Speicherauszug aus (siehecore(5)
) und beendet den Prozess.Die Muschel hat absolut keine Hand dabei.
quelle