Einführung in die Arbeit mit Valgrind und Memcheck, Teil 1

Werkzeuge  –  9 Kommentare

Selbst guten Entwicklern unterlaufen hin und wieder Fehler, die sich nicht auf den ersten Blick entdecken lassen. Statt viel Zeit mit der Suche zu verbringen, ist es für C- und C++-Programmierer sinnvoll, auf Werkzeuge wie Memcheck zurückzugreifen und damit automatische Analysen durchzuführen.

Valgrind ist ein Framework, das die Entwicklung von Werkzeugen für die dynamische Analyse von ausführbaren Programmen erleichtert. Bei einer dynamischen Analyse wird das Verhalten des Programms zu seiner Laufzeit untersucht, ganz im Gegensatz zur statischen Analyse, bei der Programme oftmals auf Quelltextebene vor, während oder nach dem Übersetzen auf diverse Kriterien oder potenzielle Fehler überprüft werden.

Für einen Softwareentwickler wäre das Framework alleine nicht sehr interessant. Valgrind bringt jedoch einige nützliche Exemplare dieser Werkzeuge gleich mit. Sie können C- oder C++-Programmierer, egal wie viel Erfahrung sie besitzen, bei alltäglichen Schwierigkeiten unter die Arme greifen. Der Fokus dieses Artikel liegt auf Memcheck, womit sich C- oder C++-typische Fehler wie Speicherüberläufe oder Speicherlecks begegnen lässt.

Die Funktionen von Valgrind sind eng mit dem Betriebssystem und der Prozessorarchitektur verzahnt. Entwickelt wurde es ursprünglich für Linux, womit es auf diversen PPC-, MIPS-, ARM- und x86-Architekturen läuft. Verwendet man eine entsprechende Distribution, ist Valgrind als Paket schon oft enthalten. Das Debian-Paket heißt zum Beispiel schlicht valgrind. Man kann es also bequem per

apt-get install valgrind

installieren. Ansonsten gelingt auch ein klassisches

configure && make install

nach dem Entpacken des Quellcode-Archivs. Die aktuelle Version 3.10.x, auf die der Artikel basiert, lässt sich von der Webseite des Projekts beziehen.

Windows-Entwickler können Valgrind nicht direkt nutzen und auch auf aktuelleren Versionen des Desktop OS von Apple gibt es ab und an Schwierigkeiten. Nutzt man Linux nicht als sein primäres Entwicklungssystem, kann der Autor deshalb die Verwendung einer virtuellen Maschine empfehlen. Software wie Boot2Docker senkt die Hürden für ein Multiplattformen-Entwicklungsmodell weiter, denn mit einem speziellen Valgrind-Container lässt sich das Werkzeug in den lokalen Entwicklungsprozess einbinden, ganz unabhängig von der verwendeten Plattform. Wichtig bei dieser Art von Entwicklung ist jedoch, dass die Programme, oder zumindest die zu testenden Teile davon, portabel zu gestalten sind. Eine derartige Vorgehensweise erscheint generell sehr sinnvoll, beispielsweise kann man so auch im Embedded-Bereich aus Tools wie Valgrind Nutzen ziehen.

Auf Fehlersuche

Mit dem Werkzeug Memcheck lassen sich typische Fehler von in C oder C++ geschriebenen Programmen detektieren, die im Zusammenhang mit der Speicherverwaltung auftreten. Es ist das wohl bekannteste Tool der Valgrind-Suite, und oft wird der Name Valgrind als Synonym zu Memcheck gebraucht. Ein kleines Beispiel soll in die Funktion und in den Nutzen dieses Helfers einführen.

Das als Grundlage dienende Programm soll auf einfache Weise die Anzahl der Primzahlen zwischen 2 und n bestimmen. Als Algorithmus soll das Sieb des Eratosthenes zum Einsatz kommen, bei dem aufsteigend alle Vielfache von einer bereits als prim erkannten Zahl herausstreichen. Die Teilbarkeit der Zahlen soll in einem Feld festgehalten werden. Da es nur zwei Zustände gibt, lässt sich für die Implementierung ein Bitvektor verwenden, um Speicherplatz zu sparen. Als Programmiersprache ist als Vorgabe reines C zu verwenden. Der entsprechende Quelltext enthält im ersten Versuch folgenden Code:

#include <stdint.h>
#include <stdlib.h>
#include <math.h>

int prime_numbers(int n)
{
int i, num = 0;
int32_t *seen = malloc(n / 32);
if (!seen)
return -1;
memset(seen, n / 32, 0);
for (i=3; i <= sqrt(n); i++)
{
if (!(seen[i / 32] & (1 << (i % 32))))
{
int j;
num++;
for (j=i*i; j < n; j += i)
seen[j / 32] |= (1 << (j % 32));
}
}
for (; i < n; i++)
{
if (!(seen[i / 32] & (1 << (i % 32))))
num++;
}
return num;
}

Ein einfacher Unit-Test in der Datei prime_test.c sieht folgendermaßen aus:

#include <assert.h>

#include "prime.c"

void test_prime_numbers()
{
assert(prime_numbers(100) == 25);
}

int main()
{
test_prime_numbers();
return 0;
}

Er lässt sich mit folgender Zeile übersetzen:

gcc prime_test.c -oprime_test -lm

Besser ist natürlich ein Makefile:

all: prime_test

prime_test: prime_test.c prime.c
gcc prime_test.c -oprime_test -lm

tests: prime_test
./prime_test

Beim ersten Aufruf des Tests mit

make tests

schlägt er allerdings fehl. Der Grund dafür ist, dass der Variable i der Wert 3 statt 2 zugewiesen wurde. Nach der Änderung funktioniert der Test vermeintlich fehlerfrei.