Menu

Speicherlecks in C: Wie sie entstehen und wie man sie findet

Was ein Leck wirklich ist, die drei Arten, wie C-Programme welche erzeugen, die Besitzdisziplin, die sie verhindert, und wie man den Rest mit valgrind und -fsanitize=address findet - abschließend ein leckendes Programm, Schritt für Schritt repariert.

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

Ein Speicherleck ist kein Speicher, der verschwunden ist. Es ist Speicher, der weiterhin dir gehört, weiterhin reserviert ist und den du nicht mehr zurückgeben kannst - weil der letzte Zeiger darauf fort ist. Nichts stürzt ab. Das Programm läuft weiter, jedes Mal ein wenig schwerer, bis irgendwann etwas an einer unverwandten Stelle scheitert.

Diese Seite behandelt, wie Lecks entstehen, die Disziplin, die die meisten verhindert, und die zwei Werkzeuge, die den Rest finden.

Wie ein Leck aussieht

Jede Iteration überschreibt block mit einem frischen Zeiger. Der vorherige Block ist weiterhin belegt; keine Variable hält seine Adresse; er kann nie freigegeben werden. Drei Iterationen verlieren zwölf Kilobyte. Ein Server, der das einmal pro Anfrage tut, verliert es für immer, in dem Tempo, in dem Anfragen eintreffen.

Die Lösung ist eine Zeile - free(block); am Ende des Rumpfes -, aber zu erkennen, wohin sie gehört, ist die eigentliche Fähigkeit.

Wie Lecks entstehen

1. Der verlorene Zeiger

Jede Zuweisung an einen Zeiger, der noch die einzige Referenz auf einen lebenden Block hält, verliert ihn.

char *name = malloc(32);
name = malloc(64);     /* die ersten 32 Bytes sind jetzt unerreichbar */

Die Schleife oben ist derselbe Bug im Schleifengewand. Ebenso das Neuzuweisen eines Strukturfelds und ebenso die realloc-Kurzform aus calloc und realloc:

p = realloc(p, n);     /* bei Fehlschlag: p wird NULL, alter Block verwaist */

2. Die frühe Rückkehr

Jeder Weg aus einer Funktion heraus muss freigeben, was die Funktion bereits genommen hat. Der, der vergessen wird, ist immer ein Fehlerpfad.

Der Erfolgspfad ist korrekt und der Fehlerpfad leckt, weshalb das die Tests übersteht: Der fehlschlagende Zweig läuft während der Entwicklung fast nie. Die Lösung ist ein einziger Aufräumabschnitt, zu dem jeder Pfad springt:

Das ist die eine Verwendung von goto, die erfahrene C-Programmierer aktiv empfehlen. Sie funktioniert, weil jeder Zeiger bei NULL beginnt und free(NULL) nichts tut, ein einziger Ausstiegsblock ist also korrekt, wie weit die Funktion auch gekommen ist.

3. Unklarer Besitz

Die subtilsten Lecks sind gar keine Programmierfehler - es sind zwei Funktionen, die sich uneins sind, wessen Aufgabe es war.

char *build_message(void);   /* gibt der Aufrufer das frei? */
void  store(char *text);     /* übernimmt store den Besitz? */

Wenn build_message belegten Speicher liefert und store ihn kopiert, muss der Aufrufer freigeben. Wenn store den Zeiger behält, darf der Aufrufer es nicht. Nichts im Code sagt, was davon gilt, eine der beiden Annahmen wird also doppelt getroffen - und du bekommst entweder ein Leck oder ein doppeltes Freigeben.

Das Gegenmittel ist eine Konvention, in einem Kommentar neben jeder Funktion, die Speicher belegt:

/* Liefert eine neu belegte Zeichenkette; der Aufrufer muss sie freigeben. */
char *build_message(void);

/* Übernimmt den Besitz von 'text'; es wird von store_free() freigegeben. */
void store(char *text);

Schreibe die Regel an die Funktion, nicht in ein Entwurfsdokument. Es ist die wertvollste Gewohnheit in der C-Speicherverwaltung.

Die Besitzdisziplin

Vier Regeln decken fast alles ab:

  1. Jede Belegung hat genau einen Besitzer - ein Stück Code, das für das Freigeben zuständig ist.
  2. Paare jede belegende Funktion mit einer freigebenden. vec_init / vec_free, config_load / config_free. Die Symmetrie macht einen fehlenden Aufruf sichtbar.
  3. Gib in derselben Schicht frei, die belegt hat, sofern der Kommentar der Funktion den Besitz nicht ausdrücklich überträgt.
  4. Setze einen Zeiger nach dem Freigeben auf NULL, damit eine spätere versehentliche Nutzung an der Fehlerstelle abstürzt, statt still den Heap zu beschädigen.

Lecks finden: valgrind

Unter Linux braucht valgrind keine Neuübersetzung, Debug-Symbole machen den Bericht aber lesbar:

gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

Für die leckende Schleife am Anfang dieser Seite endet der Bericht ungefähr so:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 12,000 bytes in 3 blocks
==12345==   total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 12,000 bytes in 3 blocks

Lies ihn von unten. „definitely lost" bedeutet, dass beim Beenden kein Zeiger auf den Block existierte - ein echtes Leck. Die Aufrufkette nennt die Zeile des malloc, das ihn erzeugte, nicht die Zeile, an der er verloren ging, was meist genügt, um das fehlende free zu finden.

Zwei weitere Kategorien tauchen auf:

  • indirectly lost - Blöcke, die nur über einen selbst verlorenen Block erreichbar sind, etwa die Elemente einer verlorenen verketteten Liste. Behebe den „definitely lost"-Fall, und diese verschwinden.
  • still reachable - beim Beenden belegt, aber mit einem lebenden Zeiger, typischerweise ein globaler Zwischenspeicher. Kein Leck im gefährlichen Sinn, aber es lohnt sich freizugeben, damit der Bericht leer bleibt.

Valgrind fängt auch Lesezugriffe auf uninitialisierten Speicher und Schreibzugriffe hinter das Ende eines Blocks ab, und so entdeckt man oft den Bug hinter dem Leck.

Lecks finden: AddressSanitizer

AddressSanitizer ist in GCC und Clang eingebaut, läuft weit schneller als valgrind und funktioniert dort, wo valgrind es nicht tut (einschließlich aktueller macOS-Versionen):

gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program

Der Leckbericht wird beim Beenden automatisch ausgegeben:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x1086... in main program.c:6

SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

ASan verwandelt außerdem Use-after-free und Heap-Pufferüberläufe in sofortige, klar beschriftete Abbrüche statt in rätselhafte Korruption später. Baue deine Testläufe mit ihm ein; baue Releases ohne ihn, da er Speicher und Tempo kostet.

Falls die Leckerkennung auf deiner Plattform nicht anspringt, setze vor dem Start ASAN_OPTIONS=detect_leaks=1 in der Umgebung.

Ein leckendes Programm Schritt für Schritt reparieren

Hier ist ein kleines Programm mit drei getrennten Lecks:

Valgrind meldet drei „definitely lost"-Einträge mit drei verschiedenen Zeilennummern. Einer nach dem anderen behoben:

Fix 1 ist der Besitzkommentar in die Tat umgesetzt: shout belegt, main gibt frei. Fix 2 entfernt die doppelte Belegung ganz, statt die erste freizugeben - der einfachere Code ist auch der korrekte. Fix 3 fügt das fehlende free bei der frühen Rückkehr hinzu; mit mehr Belegungen im Spiel skaliert das früher gezeigte einzelne cleanup:-Label besser als wiederholte Freigaben.

Gewohnheiten, die Lecks verhindern

  • Schreibe das free sofort nach dem malloc und fülle den Code dazwischen anschließend.
  • Gib jeder belegenden Funktion eine passende freigebende Funktion.
  • Nenne den Besitz in einem Kommentar an jeder Funktion, die einen von ihr belegten Zeiger liefert oder entgegennimmt.
  • Benutze einen einzigen cleanup:-Ausstiegsblock in Funktionen, die mehrere Belegungen halten.
  • Lass deine Tests standardmäßig unter -fsanitize=address laufen, nicht erst, wenn etwas verdächtig aussieht.
  • Behandle „definitely lost: 0 bytes" als Teil eines bestandenen Testlaufs.

Häufig gestellte Fragen

Was ist ein Speicherleck in C?

Speicher, den du mit malloc belegt hast und den du nicht mehr freigeben kannst, weil nichts im Programm noch darauf zeigt. Der Block bleibt für die Lebensdauer des Prozesses reserviert. Es ist kein Absturz - das Programm arbeitet weiter und benutzt bei jedem Durchlauf nur mehr Speicher, bis er irgendwann ausgeht.

Wie finde ich Speicherlecks in C?

Lass das Programm unter valgrind laufen: valgrind --leak-check=full ./program. Es meldet jeden beim Beenden noch belegten Block samt der Aufrufkette des malloc, das ihn erzeugte. Unter macOS oder wo valgrind nicht verfügbar ist, kompiliere mit -fsanitize=address, und derselbe Bericht erscheint beim Beenden.

Was verursacht Speicherlecks in C?

Drei Muster decken fast alle ab: den einzigen Zeiger auf einen Block überschreiben (einschließlich p = realloc(p, n) bei einem Fehlschlag), früh aus einer Funktion zurückkehren, die bereits Speicher belegt hat, und unklarer Besitz - zwei Funktionen nehmen jeweils an, die andere gebe frei, also tut es keine.

Spielen Speicherlecks eine Rolle, wenn das Programm ohnehin endet?

Bei einem Programm, das einmal läuft und endet, holt das Betriebssystem alles zurück, die praktische Wirkung ist also null. Es zählt bei allem Langlaufenden - einem Server, einer Spielschleife, einem Dienst -, wo ein Leck pro Anfrage unbegrenzt wächst. Gib trotzdem konsequent frei: Ein Leckbericht ist Rauschen, das die verdeckt, auf die es ankommt.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S