Menu

Segmentierungsfehler in C: Ursachen und Behebung

Ein Segfault bedeutet, dass dein Programm Speicher angefasst hat, der ihm nicht gehört. Hier sind die fünf Ursachen, die fast alle erklären, jede mit einem minimalen Beispiel und ihrer Lösung, dazu, wie man die genaue Zeile mit gdb und AddressSanitizer findet.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Segmentation fault (core dumped)

Diese eine Zeile ist der meistgesuchte Fehler in C, und sie ist weniger rätselhaft, als sie aussieht. Dein Programm hat den Prozessor nach einer Speicheradresse gefragt, das Betriebssystem hat geprüft, ob dein Prozess diese Adresse anfassen darf, und die Antwort war nein. Der Kernel hat den Prozess dann mit einem SIGSEGV-Signal beendet.

Die wichtige Erkenntnis: Der Absturz ist ein Symptom, und sein Ort ist oft nicht der Bug. Der schlechte Zeiger entstand meist woanders, früher, und das ist bloß die erste Stelle, an der er benutzt wurde. Diese Seite behandelt die fünf Ursachen, die fast jeden Segfault erklären, und dann die zwei Werkzeuge, die die echte Zeile in Sekunden finden.

Die abstürzenden Beispiele unten sind bewusst keine lauffähigen Blöcke - sie stürzen absichtlich ab. Lies sie, und lies dann die korrigierte Fassung, die folgt.

Was „Speicher, der dir nicht gehört" bedeutet

Wenn dein Programm startet, bildet das Betriebssystem mehrere Bereiche in seinen Adressraum ab: den Code, die globalen Variablen, den Stack und das, worauf der Heap gewachsen ist. Alles andere im Adressraum - einschließlich der Adresse 0 - ist nicht abgebildet. Fass eine nicht abgebildete Adresse an oder schreibe in eine nur lesbare, und die Hardware fängt es ab.

Ein Segfault ist also nicht der Compiler, der dich erwischt. Es ist eine Leitplanke zur Laufzeit, und sie greift nur, wenn die ungültige Adresse zufällig außerhalb deiner abgebildeten Seiten liegt. Deshalb kann derselbe Bug auf einer Maschine abstürzen und auf einer anderen zu funktionieren scheinen: Das Speicherlayout unterscheidet sich.

Ursache 1: einen NULL-Zeiger dereferenzieren

Die häufigste Ursache und die am leichtesten zu behebende. Adresse 0 ist nie abgebildet, ein Lese- oder Schreibzugriff über einen Nullzeiger scheitert also immer.

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

int main(void) {
    int *p = NULL;
    *p = 42;              /* ABSTURZ: Schreiben an Adresse 0 */
    printf("%d\n", *p);
    return 0;
}

Die realistische Fassung ist eine ungeprüfte Belegung:

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

int main(void) {
    int *data = malloc(1000000000000UL * sizeof(int));  /* scheitert, liefert NULL */
    data[0] = 1;                                        /* ABSTURZ */
    free(data);
    return 0;
}

malloc liefert NULL, wenn es die Anfrage nicht erfüllen kann; ebenso fopen, wenn die Datei nicht existiert, und strchr, wenn das Zeichen fehlt. Prüfe jede Funktion, die NULL liefern kann, bevor du ihr Ergebnis benutzt.

Initialisiere Zeiger mit NULL, statt sie uninitialisiert zu lassen. Ein Nullzeiger stürzt sofort und offensichtlich ab; ein Müllzeiger kann etwas beschädigen und viel später abstürzen. Mehr zu dem Muster unter Nullzeiger.

Ursache 2: über das Ende eines Arrays hinausschreiben

C prüft keine Arraygrenzen. Index 10 eines Arrays mit 10 Elementen ist schlicht der Speicher hinter dem Array, und der Compiler berechnet diese Adresse kommentarlos für dich.

#include <stdio.h>

int main(void) {
    int arr[10];

    for (int i = 0; i <= 10; i++) {   /* <= statt < : einer zu viel */
        arr[i] = i;
    }

    printf("fertig\n");
    return 0;
}

Ob das abstürzt, ist Glückssache. Vier Bytes hinter ein lokales Array zu schreiben landet meist auf anderen Stackdaten - einem gesicherten Register, einer anderen Variablen, der Rücksprungadresse -, das Programm beschädigt sich also selbst und stürzt später an einer unverwandten Stelle ab. Eine große Überschreitung verlässt die abgebildete Seite und erzeugt sofort einen Segfault.

Die wildere Fassung stürzt immer ab:

#include <stdio.h>

int main(void) {
    int arr[10];
    arr[1000000] = 42;      /* weit außerhalb von allem Abgebildeten: ABSTURZ */
    return 0;
}

Die Lösung ist die Gewohnheit i < n und n zu berechnen, statt es zweimal zu tippen:

Zeichenketten haben ihre eigene Fassung davon: ein Puffer ohne Platz für das abschließende '\0'.

#include <string.h>

int main(void) {
    char name[5];
    strcpy(name, "Alexander");   /* 9 Zeichen + Terminator in 5 Bytes */
    return 0;
}

strcpy hat keine Ahnung, wie groß name ist. Nimm snprintf, das es weiß, weil du es ihm sagst:

Ursache 3: einen Zeiger nach free benutzen (baumelnde Zeiger)

Nach free(p) geht der Speicher an den Allokator zurück. Der Zeiger hält weiterhin die alte Adresse, aber diese Adresse gehört dir nicht mehr.

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

int main(void) {
    int *p = malloc(sizeof *p);
    *p = 42;
    free(p);

    printf("%d\n", *p);   /* Use-after-free: kann Müll ausgeben, kann abstürzen */
    free(p);              /* doppeltes Freigeben: bricht meist ab oder zerstört den Heap */
    return 0;
}

Die verwandte Falle ist, die Adresse einer lokalen Variablen zurückzugeben. Ihr Stackrahmen ist in dem Moment fort, in dem die Funktion zurückkehrt:

#include <stdio.h>

int *make_number(void) {
    int value = 42;
    return &value;      /* der Rahmen stirbt hier; der Zeiger baumelt */
}

int main(void) {
    int *p = make_number();
    printf("%d\n", *p);   /* undefiniert: Müll oder ein Absturz */
    return 0;
}

Zwei Lösungen, je nachdem was du meintest. Gib den Wert statt eines Zeigers zurück, oder belege auf dem Heap und lass den Aufrufer freigeben:

Den Zeiger direkt nach free auf NULL zu setzen ist die billige Schutzgewohnheit: Sie verwandelt ein stilles Use-after-free in einen sofortigen, offensichtlichen Absturz durch Null-Dereferenzierung, und sie macht ein zweites free(p) harmlos, da free(NULL) definiert nichts tut. Die Belegungsseite dieser Geschichte steht unter dynamischer Speicher und Speicherlecks.

Ursache 4: Stacküberlauf durch entgleiste Rekursion

Jeder Funktionsaufruf legt einen Rahmen auf den Stack, und der Stack ist ein Bereich fester Größe (üblicherweise 8 MB). Rekursion ohne Basisfall - oder mit einem, der nie erreicht wird - läuft über sein Ende hinaus.

#include <stdio.h>

int countdown(int n) {
    printf("%d\n", n);
    return countdown(n - 1);    /* kein Basisfall: hört nie auf */
}

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

Dasselbe passiert mit einem Basisfall, über den die Rekursion hinwegschreitet:

int f(int n) {
    if (n == 0) return 1;
    return n * f(n - 2);    /* von einem ungeraden n aus nie gleich 0 */
}

Jede rekursive Funktion braucht einen Basisfall, der von jeder Eingabe aus erreichbar ist:

Ein riesiges lokales Array schafft es auch - int buffer[10000000]; innerhalb einer Funktion fordert 40 MB Stack an und scheitert beim ersten Schreibzugriff. Belege große Puffer mit malloc auf dem Heap. Siehe Stack gegen Heap für die beteiligten Größen und Rekursion für den Entwurf von Basisfällen.

Ursache 5: in ein Zeichenkettenliteral schreiben

Das überrascht die Leute, weil der Code harmlos aussieht.

#include <stdio.h>

int main(void) {
    char *s = "hallo";
    s[0] = 'H';          /* ABSTURZ: Zeichenkettenliterale sind nur lesbar */
    printf("%s\n", s);
    return 0;
}

Ein Zeichenkettenliteral liegt in einem nur lesbaren Abschnitt der ausführbaren Datei. char *s = "hallo" zeigt hinein; über diesen Zeiger zu schreiben ist ein Schutzverstoß, den das Betriebssystem genauso als Segfault meldet wie einen Zugriff auf nicht abgebildeten Speicher.

Die Lösung ist, ein Array zu nehmen, das seine eigene änderbare Kopie bekommt:

Literalzeiger als const char * zu deklarieren verwandelt diesen Laufzeitabsturz in einen Übersetzungsfehler, was schlicht besser ist. Mach es zur Gewohnheit.

Die echte Zeile finden: gdb

Kompiliere mit -g, damit die ausführbare Datei Debug-Symbole trägt, und lass sie dann unter dem Debugger laufen:

gcc -g program.c -o program
gdb ./program

In 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

Drei Befehle erledigen den Großteil der Arbeit. run startet das Programm und hält dort an, wo es scheitert. backtrace (oder bt) zeigt die Kette der Aufrufe, die dorthin führten - Rahmen #1 ist meist der Ort, an dem der schlechte Zeiger tatsächlich entstand. print inspiziert eine Variable, und item = 0x0 benennt das Problem rundheraus.

Unter macOS ist das Gegenstück lldb ./program, dann run und bt.

Es schneller finden: AddressSanitizer

Besser noch: Lass den Compiler das Programm instrumentieren. AddressSanitizer fängt den ungültigen Zugriff in dem Moment ab, in dem er passiert - auch die, die nicht abgestürzt wären:

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

Dieser Bericht benennt die Fehlerklasse, die Zeile, die es tat, die Zeile, die den Speicher freigab, und die Zeile, die ihn belegte. Es ist das wirksamste Debugging-Werkzeug für Speicherfehler in C, es funktioniert mit GCC und clang unter Linux und macOS, und es kostet grob die doppelte Laufzeit - was während der Entwicklung belanglos ist.

Paare es mit -fsanitize=undefined, um gleichzeitig vorzeichenbehafteten Überlauf und anderes undefiniertes Verhalten abzufangen:

gcc -g -Wall -Wextra -fsanitize=address,undefined program.c -o program

valgrind ./program ist die Alternative, die keine Neuübersetzung braucht und dieselben Fehlerklassen plus Lecks meldet.

Eine Checkliste, wenn du auf einen triffst

  1. Baue neu mit -g -Wall -Wextra -fsanitize=address,undefined und lass es erneut laufen. Meist benennt der Bericht die Zeile und du bist fertig.
  2. Ist der Sanitizer nicht verfügbar, lass es unter gdb laufen und nimm ein backtrace. Sieh dir Rahmen 1 an, nicht nur Rahmen 0.
  3. Prüfe jeden Zeiger in der abstürzenden Zeile. Gib jeden aus; 0x0 kennzeichnet einen Nullzeiger, und ein wilder Wert wie 0x7fff5fc01000 bedeutet meist uninitialisiert oder freigegeben.
  4. Frage, woher dieser Zeiger kam. Ein ungeprüftes malloc oder fopen? Die Adresse einer Lokalen, deren Funktion längst zurückgekehrt ist? Ein Zeiger, der nach free benutzt wurde?
  5. Prüfe jede Schleifengrenze nahe dem Absturz auf ein <=, wo < gemeint war.
  6. Ist die Aufrufkette Tausende Rahmen tief, ist es entgleiste Rekursion und überhaupt kein Zeigerfehler.

Sie verhindern

Die Gewohnheiten, die Segfaults selten machen:

  • Kompiliere immer mit -Wall -Wextra und behandle Warnungen als Bugs.
  • Initialisiere jeden Zeiger, mit NULL, wenn nichts Besseres da ist.
  • Prüfe den Rückgabewert von malloc, calloc, realloc und fopen.
  • Setze Zeiger unmittelbar nach dem Freigeben auf NULL.
  • Nimm snprintf und fgets statt sprintf und gets.
  • Deklariere Zeiger auf Zeichenkettenliterale als const char *.
  • Bevorzuge sizeof arr / sizeof arr[0] gegenüber einer fest eingetragenen Länge.
  • Lass die Testsuite in der CI unter AddressSanitizer laufen.

Ein Segfault ist die freundliche Fehlerart - er sagt dir, dass etwas nicht stimmt. Dieselbe Fehlerklasse, die still eine benachbarte Variable beschädigt und drei Funktionen später falsche Antworten erzeugt, ist weit schlimmer, und die obigen Werkzeuge fangen beides ab.

Häufig gestellte Fragen

Was ist ein Segmentierungsfehler in C?

Ein Absturz, den das Betriebssystem auslöst, wenn dein Programm auf Speicher zugreift, den es nicht anfassen darf - Lesen oder Schreiben über einen ungültigen Zeiger, über das Ende eines Arrays in eine nicht abgebildete Seite laufen oder den Stack überlaufen lassen. Der Kernel schickt dem Prozess ein SIGSEGV-Signal, das ihn beendet und „Segmentation fault (core dumped)" ausgibt.

Wie finde ich heraus, wo ein Segmentierungsfehler passiert?

Kompiliere mit Debug-Symbolen und lass es unter einem Debugger laufen: gcc -g program.c -o program, dann gdb ./program, run, und beim Absturz backtrace. Das gibt die genaue Datei und Zeile aus. Noch schneller für Speicherfehler ist gcc -g -fsanitize=address program.c -o program - das Programm einfach laufen zu lassen gibt dann einen vollständigen Bericht aus, was schiefging und wo.

Warum stürzt mein C-Programm nur manchmal mit einem Segfault ab?

Weil der ungültige Zugriff undefiniertes Verhalten ist und kein garantierter Absturz. Ein Element hinter ein Array zu schreiben landet oft in Speicher, der deinem Prozess gehört, nichts hält dich also auf - es beschädigt stattdessen eine benachbarte Variable. Ein Segfault entsteht nur, wenn die schlechte Adresse zufällig außerhalb einer abgebildeten Seite liegt, und das hängt vom Layout dieses konkreten Builds und Laufs ab.

Bedeutet ein Segmentierungsfehler, dass ich ein Speicherleck habe?

Nein - das sind gegensätzliche Probleme. Ein Leck ist Speicher, den du belegt und nie freigegeben hast: Das Programm läuft weiter und wächst langsam. Ein Segfault ist das Anfassen von Speicher, der dir nicht gehört. Speicher doppelt freizugeben oder einen Zeiger nach dem Freigeben zu benutzen verursacht Segfaults; das Freigeben zu vergessen verursacht Lecks.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S