EinfĂĽhrung in die Arbeit mit Valgrind und Memcheck, Teil 2
Valgrind ist ein Framework, das die Entwicklung von Werkzeugen fĂĽr die dynamische Analyse ausfĂĽhrbarer Programme erleichtert. Und C/C++-Entwickler sollten darauf nicht verzichten, wenn sie ihre Software auf lange Zeit stabil halten wollen.
- Sebastian Bauer
Valgrind ist ein Framework, das die Entwicklung von Werkzeugen fĂĽr die dynamische Analyse ausfĂĽhrbarer Programme erleichtert. Wie bei weiteren Verfahren der Software-Technik, etwa Unit-Testing und Continuous Integration, sollten C/C++-Entwickler nicht darauf verzichten, wenn sie ihre Software auf lange Zeit stabil halten wollen.
Ein erster Artikel fĂĽhrte anhand eines Beispiels in die Verwendung von Valgrind beziehungsweise Memcheck ein. Allerdings bietet die Valgrind-Suite noch viel mehr.
Die Probleme im Primzahlbeispiel aus dem letzten Artikel waren leicht zu finden, da der Speicherblock lokal in einer einzelnen Funktion zum Einsatz kam. Generell ist das allerdings nur selten der Fall. Als Beispiel könnte man eine Funktion, die ein höheres "Objekt" und ein passendes Gegenstück anlegt (zum Beispiel account_new() und account_free()) betrachten. Vergisst man account_free(), befindet sich das Leck nicht auf malloc()/free()-Ebene, sondern auf Objektebene. In der Regel sind Fehler auf höherer Ebene schwieriger zu finden, da die Interaktion mit dem Objekt steigt. Es ist in solchen Fällen oft effektiv, den Stacktrace tatsächlich rückwärts zu gehen und zu schauen, ob der korrespondierende Freigabeaufruf vorhanden ist.
Memcheck kann noch mehr
Neben Speicherzugriffsfehlern und Speicherlecks unterzieht Memcheck die Parameter bei Systemaufrufen einer ĂśberprĂĽfung. Der Code
char *buf;
/* buf wird nie gesetzt */
write(STDOUT_FILENO, buf, sizeof(buf));
wird auf folgende Weise bemängelt:
==30554== Syscall param write(buf) contains uninitialised byte(s)
==30554== at 0x5217200: __write_nocancel (syscall-template.S:81)
==30554== by 0x40073F: main (bug.cpp:24)
Sollte der Speicher, auf den buf zeigt, nicht alloziert oder bereits freigegeben worden sein, wird eine ähnliche Meldung ausgegeben. Des Weiteren erkennt Memcheck, ob der korrekte Freigabemechanismus im Einsatz ist. Mit malloc() angelegten Speicher darf man nicht mit delete freigeben. Ebenso beanstandet das Tool, wenn das falsche delete benutzt wurde:
char *buf = new char[10];
/* ... */
delete buf;
Valgrind gibt daraufhin folgende Meldung aus:
==30699== Mismatched free() / delete / delete []
==30699== at 0x4C29C00: operator delete(void*) (vg_replace_malloc.c:502)
==30699== by 0x400781: main (bug.cpp:28)
==30699== Address 0x59fd090 is 0 bytes inside a block of size 10 alloc'd
==30699== at 0x4C29140: operator new[](unsigned long)
(vg_replace_malloc.c:384)
==30699== by 0x400749: main (bug.cpp:25)
Nach aktuellen C++-Standards ist einzig und allein der delete []-Operator korrekt. Zu guter Letzt kann Memcheck speicherĂĽberlappende Quellen und Ziele bei Funktionen wie memcpy() detektieren und den Entwickler darĂĽber in Kenntnis setzen.
Erwähnenswert an dieser Stelle ist noch, dass sich Memcheck fast ausnahmslos auf den Heap des Programms bezieht. Pufferüberläufe, die auf dem Stack oder in globalen Daten des Programms stattfinden, kann Memcheck nicht detektieren.
Speicherlecks ergrĂĽnden
In der Ausgabe der Speicherlecks findet man Valgrinds Versuche, Speicherbereiche zu kategorisieren. Dahinter verbirgt sich eine Heuristik, die in der Regel eine gute Trefferquote hat, aber nicht immer korrekt sein muss.
Beispielsweise wurde der fehlerhafte Bereich aus der als Beispiel herangezogenen Primzahlfunktion als "definitely lost" bezeichnet. Das bedeutet, dass das Programm keine Möglichkeit mehr hat, auf den Zeiger zuzugreifen, weil sich die Startadresse weder in einem Register noch im Daten-, Heap- oder Stacksegment des Programms wiederfindet. Es handelt sich hierbei tatsächlich um ein Speicherleck, da der Speicher weder freigegeben wurde noch freigegeben werden kann, und man tut gut daran, sich auf die Suche danach zu begeben. Wäre die Adresse des Blocks aber zufälligerweise bei Programmende noch in einem Register, würde er als "still reachable" klassifiziert.
Befindet sich innerhalb eines eindeutig verlorenen Blocks eine Referenz auf einen weiteren Speicherblock, der nicht freigegeben wird, wird letzterer als "indirectly lost" klassifiziert. Daraus folgt, dass es ohne einen "definitely lost"- keinen "indirectly lost"-Block geben kann. Es handelt sich dabei also um ein sehr sicher detektiertes Speicherleck, das sich eventuell durch das Beheben des korrespondierenden "definitely lost"-Speicherblocks entfernen lässt. Sollte das nicht der Fall sein, wird er höchstwahrscheinlich das nächste Mal als "definitely lost" klassifiziert.
Speicherblöcke, die Memcheck als "possibly lost" deklariert, wurden üblicherweise auch nicht freigegeben. Anders als bei "still reachable" befinden sich jedoch nicht die Anfangsadressen der Blöcke in den Datenbereichen des Programms, sondern andere gültige Adressen innerhalb des Blocks. Das kommt beispielsweise vor, wenn man eigene Speicherverwaltung implementiert, etwa durch die folgende Funktion:
static void *mymalloc(size_t t)
{
uint32_t *mem = malloc(t + 4);
if (!mem) return NULL;
mem[0] = t;
return mem + 1;
}
Der Aufrufer von mymalloc() speichert also nicht die Anfangsadresse eines von malloc() reservierten Speicherblocks, sondern eine um vier Bytes verschobene. Mit der Option --show-possibly-lost=no lassen sich derartige Speicherlecks unterdrĂĽcken.