Segmentierungsfehler – Wikipedia

before-content-x4

Computerfehler durch Zugriff auf eingeschränkten Speicher

after-content-x4

Beim Rechnen ist a Segmentierungsfehler (oft abgekürzt zu Segfehler) oder Zugriffsverletzung ist ein Fehler oder eine Fehlerbedingung, die von Hardware mit Speicherschutz ausgelöst wird und einem Betriebssystem (OS) mitteilt, dass die Software versucht hat, auf einen eingeschränkten Speicherbereich zuzugreifen (eine Speicherzugriffsverletzung). Auf Standard-x86-Computern ist dies eine Art allgemeiner Schutzfehler. Der Betriebssystemkernel führt als Reaktion normalerweise eine Korrekturmaßnahme durch, indem er im Allgemeinen den Fehler an den störenden Prozess weiterleitet, indem er dem Prozess ein Signal sendet. Prozesse können in einigen Fällen einen benutzerdefinierten Signalhandler installieren, der es ihnen ermöglicht, sich selbst wiederherzustellen,[1] aber ansonsten wird der standardmäßige Signalhandler des Betriebssystems verwendet, was im Allgemeinen zu einer abnormalen Beendigung des Prozesses (einem Programmabsturz) und manchmal zu einem Core-Dump führt.

Segmentierungsfehler sind eine häufige Fehlerklasse in Programmen, die in Sprachen wie C geschrieben sind und einen Speicherzugriff auf niedriger Ebene und wenige bis keine Sicherheitsprüfungen bieten. Sie entstehen hauptsächlich aufgrund von Fehlern bei der Verwendung von Zeigern für die Adressierung des virtuellen Speichers, insbesondere durch illegale Zugriffe. Eine andere Art von Speicherzugriffsfehlern ist ein Busfehler, der ebenfalls verschiedene Ursachen hat, aber heute viel seltener ist; diese treten in erster Linie aufgrund falscher körperlich Speicheradressierung oder aufgrund eines falsch ausgerichteten Speicherzugriffs – dies sind Speicherreferenzen, die die Hardware kann nicht Adresse statt Referenzen, die ein Prozess nicht ist dürfen zu richten.

Viele Programmiersprachen können Mechanismen verwenden, die darauf ausgelegt sind, Segmentierungsfehler zu vermeiden und die Speichersicherheit zu verbessern. Die Programmiersprache Rust verwendet beispielsweise eine „Eigentümerschaft“[2]“-basiertes Modell, um die Speichersicherheit zu gewährleisten.[3] Andere Sprachen wie Lisp und Java verwenden Garbage Collection,[4] wodurch bestimmte Klassen von Speicherfehlern vermieden werden, die zu Segmentierungsfehlern führen könnten.[5]

Überblick[edit]

Beispiel für ein vom Menschen erzeugtes Signal

Ein Segmentierungsfehler tritt auf, wenn ein Programm versucht, auf eine Speicherstelle zuzugreifen, auf die es nicht zugreifen darf, oder auf eine unzulässige Weise auf eine Speicherstelle zuzugreifen versucht (z einen Teil des Betriebssystems zu überschreiben).

Der Begriff “Segmentierung” hat in der Informatik verschiedene Verwendungen; im Kontext von “Segmentation Fault”, ein Begriff, der seit den 1950er Jahren verwendet wird,[citation needed] es bezieht sich auf den Adressraum von a Programm.[6] Beim Speicherschutz ist nur der eigene Adressraum des Programms lesbar, und davon sind nur der Stack und der Lese-/Schreibteil des Datensegments eines Programms beschreibbar, während Nur-Lese-Daten und das Codesegment nicht beschreibbar sind. Daher führt der Versuch, außerhalb des Adressraums des Programms zu lesen oder in ein Nur-Lese-Segment des Adressraums zu schreiben, zu einem Segmentierungsfehler, daher der Name.

after-content-x4

Auf Systemen, die Hardware-Speichersegmentierung verwenden, um virtuellen Speicher bereitzustellen, tritt ein Segmentierungsfehler auf, wenn die Hardware einen Versuch erkennt, auf ein nicht vorhandenes Segment oder auf eine Position außerhalb der Grenzen eines Segments oder auf eine Position in eine Art und Weise, die durch die für dieses Segment gewährten Berechtigungen nicht zulässig ist. Auf Systemen, die nur Paging verwenden, führt ein ungültiger Seitenfehler im Allgemeinen zu einem Segmentierungsfehler, und Segmentierungsfehler und Seitenfehler sind beides Fehler, die durch das virtuelle Speicherverwaltungssystem ausgelöst werden. Segmentierungsfehler können auch unabhängig von Seitenfehlern auftreten: illegaler Zugriff auf eine gültige Seite ist ein Segmentierungsfehler, aber kein ungültiger Seitenfehler, und Segmentierungsfehler können mitten in einer Seite auftreten (daher kein Seitenfehler), zum Beispiel in a Pufferüberlauf, der innerhalb einer Seite verbleibt, aber den Speicher illegal überschreibt.

Auf Hardwareebene wird der Fehler zunächst von der Speicherverwaltungseinheit (MMU) bei illegalem Zugriff (wenn der referenzierte Speicher existiert), als Teil ihrer Speicherschutzfunktion oder einem ungültigen Seitenfehler (wenn der referenzierte Speicher nicht existiert) ausgelöst ). Wenn das Problem nicht eine ungültige logische Adresse, sondern eine ungültige physikalische Adresse ist, wird stattdessen ein Busfehler ausgegeben, der jedoch nicht immer unterschieden wird.

Auf Betriebssystemebene wird dieser Fehler abgefangen und ein Signal an den verletzenden Prozess weitergegeben, der den Handler des Prozesses für dieses Signal aktiviert. Unterschiedliche Betriebssysteme haben unterschiedliche Signalnamen, um anzuzeigen, dass ein Segmentierungsfehler aufgetreten ist. Auf Unix-ähnlichen Betriebssystemen wird ein Signal namens SIGSEGV (abgekürzt von Segmentierungsverletzung) wird an den beleidigenden Prozess gesendet. Unter Microsoft Windows empfängt der problematische Prozess eine STATUS_ACCESS_VIOLATION-Ausnahme.

Die Bedingungen, unter denen Segmentierungsverletzungen auftreten und wie sie sich äußern, sind hardware- und betriebssystemspezifisch: Unterschiedliche Hardware löst für gegebene Bedingungen unterschiedliche Fehler aus, und unterschiedliche Betriebssysteme wandeln diese in unterschiedliche Signale um, die an Prozesse weitergegeben werden. Die unmittelbare Ursache ist eine Speicherzugriffsverletzung, während die zugrunde liegende Ursache im Allgemeinen ein Softwarefehler irgendeiner Art ist. Das Bestimmen der Grundursache – das Debuggen des Fehlers – kann in einigen Fällen einfach sein, wenn das Programm ständig einen Segmentierungsfehler verursacht (z. B. Dereferenzieren eines Nullzeigers), während der Fehler in anderen Fällen schwer zu reproduzieren sein kann und von der Speicherzuweisung abhängt bei jedem Durchlauf (zB Dereferenzieren eines baumelnden Zeigers).

Im Folgenden sind einige typische Ursachen für einen Segmentierungsfehler aufgeführt:

  • Versuch, auf eine nicht vorhandene Speicheradresse zuzugreifen (außerhalb des Adressraums des Prozesses)
  • Der Versuch, auf den Speicher zuzugreifen, für den das Programm keine Rechte hat (z. B. Kernelstrukturen im Prozesskontext)
  • Versuch, in Nur-Lese-Speicher zu schreiben (z. B. Codesegment)

Diese wiederum werden oft durch Programmierfehler verursacht, die zu ungültigen Speicherzugriffen führen:

  • Dereferenzieren eines Nullzeigers, der normalerweise auf eine Adresse zeigt, die nicht zum Adressraum des Prozesses gehört
  • Dereferenzieren oder Zuweisen zu einem nicht initialisierten Zeiger (wilder Zeiger, der auf eine zufällige Speicheradresse zeigt)
  • Dereferenzieren oder Zuweisen zu einem freigegebenen Zeiger (dangling pointer, der auf Speicher zeigt, der freigegeben/zugeordnet/gelöscht wurde)
  • Ein Pufferüberlauf
  • Ein Stapelüberlauf
  • Versuch, ein Programm auszuführen, das nicht richtig kompiliert wird. (Einige Compiler geben trotz Kompilierzeitfehlern eine ausführbare Datei aus.)

In C-Code treten Segmentierungsfehler am häufigsten aufgrund von Fehlern bei der Zeigerverwendung auf, insbesondere bei der dynamischen Speicherzuweisung in C. Das Dereferenzieren eines NULL-Zeigers führt immer zu einem Segmentierungsfehler, aber Wildzeiger und Dangling-Zeiger zeigen auf Speicher, der möglicherweise existiert oder nicht und kann oder kann nicht lesbar oder beschreibbar sein und kann daher zu vorübergehenden Fehlern führen. Zum Beispiel:

char *p1 = NULL;           // Null pointer
char *p2;                  // Wild pointer: not initialized at all.
char *p3  = malloc(10 * sizeof(char));  // Initialized pointer to allocated memory
                                        // (assuming malloc did not fail)
free(p3);                  // p3 is now a dangling pointer, as memory has been freed

Das Dereferenzieren einer dieser Variablen kann einen Segmentierungsfehler verursachen: Das Dereferenzieren des Null-Zeigers führt im Allgemeinen zu einem Segfault, während das Lesen vom Wild-Zeiger stattdessen zu zufälligen Daten, aber keinem Segfault führen kann, und das Lesen vom Dangling-Pointer kann zu gültigen Daten für a . führen while, und dann zufällige Daten, während sie überschrieben werden.

Handhabung[edit]

Die Standardaktion für einen Segmentierungsfehler oder Busfehler ist die abnormale Beendigung des Prozesses, der ihn ausgelöst hat. Eine Kerndatei kann generiert werden, um das Debuggen zu unterstützen, und andere plattformabhängige Aktionen können ebenfalls ausgeführt werden. Linux-Systeme, die den grsecurity-Patch verwenden, können beispielsweise SIGSEGV-Signale protokollieren, um mögliche Eindringversuche mithilfe von Pufferüberläufen zu überwachen.

Auf einigen Systemen wie Linux und Windows ist es möglich, dass das Programm selbst einen Segmentierungsfehler behandelt.[7] Abhängig von der Architektur und dem Betriebssystem kann das laufende Programm nicht nur das Ereignis verarbeiten, sondern auch einige Informationen über seinen Zustand extrahieren, z ungültiger Zugriff[8] und ob die Aktion ein Lesen oder Schreiben war.[9]

Obwohl ein Segmentierungsfehler im Allgemeinen bedeutet, dass das Programm einen Fehler hat, der behoben werden muss, ist es auch möglich, einen solchen Fehler absichtlich zum Testen, Debuggen und auch zum Emulieren von Plattformen zu verursachen, bei denen ein direkter Zugriff auf den Speicher erforderlich ist. Im letzteren Fall muss das System in der Lage sein, das Programm auch nach Auftreten des Fehlers laufen zu lassen. In diesem Fall ist es, wenn es das System zulässt, möglich, das Ereignis zu behandeln und den Prozessorprogrammzähler zu inkrementieren, um über den fehlgeschlagenen Befehl zu “springen”, um die Ausführung fortzusetzen.[10]

Beispiele[edit]

Segmentierungsfehler auf einer EMV-Tastatur

Schreiben in Nur-Lese-Speicher[edit]

Das Schreiben in den Nur-Lese-Speicher führt zu einem Segmentierungsfehler. Auf der Ebene von Codefehlern tritt dies auf, wenn das Programm in einen Teil seines eigenen Codesegments oder in den Nur-Lese-Teil des Datensegments schreibt, da diese vom Betriebssystem in den Nur-Lese-Speicher geladen werden.

Hier ist ein Beispiel für ANSI C-Code, der auf Plattformen mit Speicherschutz im Allgemeinen einen Segmentierungsfehler verursacht. Es versucht, ein Zeichenfolgenliteral zu ändern, was gemäß dem ANSI-C-Standard ein undefiniertes Verhalten darstellt. Die meisten Compiler werden dies zur Kompilierzeit nicht abfangen und stattdessen in ausführbaren Code kompilieren, der abstürzt:

int main(void)
{
    char *s = "hello world";
    *s = 'H';
}

Wenn das Programm, das diesen Code enthält, kompiliert wird, wird die Zeichenfolge “hello world” im Rodata-Abschnitt der ausführbaren Programmdatei platziert: dem schreibgeschützten Abschnitt des Datensegments. Beim Laden legt das Betriebssystem sie zusammen mit anderen Zeichenfolgen und konstanten Daten in einem schreibgeschützten Speichersegment ab. Bei der Ausführung wird eine Variable, S, wird so gesetzt, dass er auf die Position des Strings zeigt, und es wird versucht, an . zu schreiben h Zeichen durch die Variable in den Speicher, was zu einem Segmentierungsfehler führt. Das Kompilieren eines solchen Programms mit einem Compiler, der zur Kompilierzeit nicht auf die Zuweisung von Nur-Lese-Speicherorten prüft, und das Ausführen auf einem Unix-ähnlichen Betriebssystem führt zu folgendem Laufzeitfehler:

$ gcc segfault.c -g -o segfault
$ ./segfault
Segmentation fault

Rückverfolgung der Core-Datei von GDB:

Program received signal SIGSEGV, Segmentation fault.
0x1c0005c2 in main () at segfault.c:6
6               *s = 'H';

Dieser Code kann korrigiert werden, indem ein Array anstelle eines Zeichenzeigers verwendet wird, da dieser Speicher auf dem Stack alloziert und mit dem Wert des String-Literals initialisiert:

char s[] = "hello world";
s[0] = 'H';  // equivalently, *s = 'H';

Obwohl String-Literale nicht geändert werden sollten (dies hat ein undefiniertes Verhalten im C-Standard), sind sie in C von static char [] Typ,[11][12][13] Es gibt also keine implizite Konvertierung im Originalcode (was auf a char * in diesem Array), während sie in C++ von static const char [] type, und daher gibt es eine implizite Konvertierung, sodass Compiler diesen speziellen Fehler im Allgemeinen abfangen.

Nullzeiger-Dereferenzierung[edit]

In C und C-ähnlichen Sprachen werden Nullzeiger verwendet, um “Zeiger auf kein Objekt” und als Fehleranzeige zu bedeuten, und das Dereferenzieren eines Nullzeigers (ein Lesen oder Schreiben durch einen Nullzeiger) ist ein sehr häufiger Programmfehler. Der C-Standard sagt nicht, dass der Nullzeiger derselbe ist wie der Zeiger auf die Speicheradresse 0, obwohl dies in der Praxis der Fall sein kann. Die meisten Betriebssysteme bilden die Adresse des Nullzeigers so ab, dass der Zugriff darauf einen Segmentierungsfehler verursacht. Dieses Verhalten wird vom C-Standard nicht garantiert. Das Dereferenzieren eines Nullzeigers ist in C undefiniertes Verhalten, und eine konforme Implementierung darf annehmen, dass jeder dereferenzierte Zeiger nicht null ist.

int *ptr = NULL;
printf("%d", *ptr);

Dieser Beispielcode erstellt einen Nullzeiger und versucht dann, auf seinen Wert zuzugreifen (den Wert zu lesen). Dies führt bei vielen Betriebssystemen zur Laufzeit zu einem Segmentierungsfehler.

Das Dereferenzieren eines Nullzeigers und das anschließende Zuweisen (Schreiben eines Werts in ein nicht vorhandenes Ziel) verursacht normalerweise auch einen Segmentierungsfehler:

int *ptr = NULL;
*ptr = 1;

Der folgende Code enthält eine Nullzeiger-Dereferenzierung, führt jedoch beim Kompilieren oft nicht zu einem Segmentierungsfehler, da der Wert nicht verwendet wird und die Dereferenz daher oft durch Eliminierung von totem Code wegoptimiert wird:

Pufferüberlauf[edit]

Der folgende Code greift auf das Zeichenarray zu s über seine obere Grenze hinaus. Je nach Compiler und Prozessor kann dies zu einem Segmentierungsfehler führen.

char s[] = "hello world";
char c = s[20];

Paketüberfluss[edit]

Ein weiteres Beispiel ist die Rekursion ohne Basisfall:

int main(void)
{
    return main();
}

Dies führt zu einem Überlauf des Stapels, was zu einem Segmentierungsfehler führt.[14] Eine unendliche Rekursion muss nicht unbedingt zu einem Stapelüberlauf führen, abhängig von der Sprache, den vom Compiler durchgeführten Optimierungen und der genauen Struktur eines Codes. In diesem Fall ist das Verhalten von nicht erreichbarem Code (der return-Anweisung) undefiniert, sodass der Compiler es eliminieren und eine Tail-Call-Optimierung verwenden kann, die möglicherweise zu keiner Stapelverwendung führt. Andere Optimierungen könnten darin bestehen, die Rekursion in eine Iteration zu übersetzen, was angesichts der Struktur der Beispielfunktion dazu führen würde, dass das Programm ewig läuft, während der Stapel wahrscheinlich nicht überläuft.

Siehe auch[edit]

Verweise[edit]

  1. ^ Experten-C-Programmierung: tiefe C-Geheimnisse Von Peter Van der Linden, Seite 188
  2. ^ Die Programmiersprache Rust – Eigentum
  3. ^ Furchtlose Parallelität mit Rust – The Rust Programming Language Blog
  4. ^ McCarthy, John (April 1960). “Rekursive Funktionen symbolischer Ausdrücke und deren maschinelle Berechnung, Teil I”. Mitteilungen des ACM. 4 (3): 184-195. mach:10.1145/367177.367199. S2CID 1489409. Abgerufen 2018-09-22.
  5. ^ Dhurjati, Dinakar; Kowshik, Sumant; Adve, Vikram; Lattner, Chris (1. Januar 2003). „Speichersicherheit ohne Laufzeitprüfungen oder Garbage Collection“ (PDF). Proceedings of the 2003 ACM SIGPLAN Conference on Language, Compiler, and Tool for Embedded Systems. ACM. 38 (7): 69–80. mach:10.1145/780732.780743. ISBN 1581136471. S2CID 1459540. Abgerufen 2018-09-22.
  6. ^ “Debuggen von Segmentierungsfehlern und Zeigerproblemen – Cprogramming.com”. www.cprogramming.com. Abgerufen 2021-02-03.
  7. ^ “Saubere Wiederherstellung von Segfaults unter Windows und Linux (32-Bit, x86)”. Abgerufen 2020-08-23.
  8. ^ “Implementierung des SIGSEGV/SIGABRT-Handlers, der den Debug-Stack-Trace ausgibt”. Abgerufen 2020-08-23.
  9. ^ “Wie identifiziere ich Lese- oder Schreiboperationen von Seitenfehlern bei Verwendung des Sigaction-Handlers auf SIGSEGV? (LINUX)”. Abgerufen 2020-08-23.
  10. ^ „LINUX – SCHREIBEN VON FEHLERBEHANDLUNGEN“. Abgerufen 2020-08-23.
  11. ^ “6.1.4 Zeichenfolgenliterale”. ISO/IEC 9899:1990 – Programmiersprachen – C.
  12. ^ “6.4.5 Zeichenfolgenliterale”. ISO/IEC 9899:1999 – Programmiersprachen – C.
  13. ^ “6.4.5 Zeichenfolgenliterale”. ISO/IEC 9899: 2011 – Programmiersprachen – C.
  14. ^ Was ist der Unterschied zwischen einem Segmentierungsfehler und einem Stapelüberlauf? bei Stapelüberlauf

Externe Links[edit]


after-content-x4