Wer Programme schreibt, baut ahnungslos oft auch gut versteckte Bugs ein. Die richtigen Tools und etwas Geduld vereinfachen das Debugging. Dieser Workshop erklärt die Grundlagen der Fehlersuche und vermittelt den Umgang mit dem prominentesten Vertreter der Gattung, dem GNU-Debugger GDB.
Die Existenz aller Bugs geht auf ein grundlegendes Missverständnis zurück: Etwas, was Sie für richtig gehalten haben, ist in Wirklichkeit falsch – egal ob sich der Fehler auf den Wert einer Variablen oder die Gültigkeit einer Speicheradresse oder eines Zeigers bezieht. Wie sich die Bugs auswirken, können Sie nicht vorhersagen. Vielleicht stürzt das Programm aufgrund eines Speicherzugriff-Fehlers ab, vielleicht bleibt es in einer Endlosschleife hängen oder es liefert falsche Ergebnisse. Aber eines steht fest: Alle Bugs sind wichtig und als Programmierer kommen Sie nicht darum herum, sie zu beseitigen.
Gründlich informieren
Als konventioneller Lösungsansatz dienen so genannte Trace-Meldungen. Sie geben aktuelle Zwischenergebnisse des Programms aus. In der Programmiersprache C fügt man üblicherweise Code hinzu, der auf dem Kanal zur Fehlerausgabe ausgiebig Bericht erstattet:
fprintf(stderr, "Entering CalcAverage now (num=%d, total=%d)",num,total); .... fprintf(stderr, "Leaving CalcAverage now (return av=%d)", average);
Solche Befehle dokumentieren Funktionsaufrufe und Variablenwerte. Diese Informationen helfen bei der weiteren Suche nach der Fehlerursache.
Für Faultiere
Obwohl diese Methode für kleinere Programme angebracht sein mag, wird sie mit zunehmendem Code-Umfang sehr mühsam. Zusätzlich schleichen sich dabei schnell neue Fehler ein – schließlich kann auch der hinzugefügte Debugging-Code wieder Fehler enthalten.
Faulheit gilt bei Programmierern als eine Tugend, ebenso wie das Verschieben auf morgen. Bevor Sie sich also kopfüber ins Debugging stürzen, lehnen Sie sich zurück, um in Ruhe über prophylaktische Maßnahmen nachzudenken.
Erstens: Wird das Programm sauber kompiliert? Gibt der Compiler Warnungen aus und können diese Warnungen mit dem fehlerhaften Verhalten des Programms zusammenhängen? Kompilieren Sie mit den Optionen »-Wall« und »-O3«; Letztere führt zusätzliche Checks durch, die bei der nicht optimierten Kompilierung fehlen, »-Wall« gibt sämtliche Warnungen aus. Dann sehen Sie sich die Ausgabe erneut an.
Zweitens: Überprüfen Sie den Code mit anderen Tools wie Splint[2]; es meldet Fehler, die GCC ignoriert. Auf diese Art stoßen Sie auf Programmabschnitte, die unter bestimmten Umständen den Dienst verweigern. Typische Problemfälle sind zum Beispiel:
- Eine Zahl des Typs »unsigned« nimmt einen negativen
Wert an. - Aus dem Gleichheitssymbol »==« wird durch einen
Tippfehler eine Zuweisung (»=«). - Beim Type-Casting (explizit oder implizit) gehen Informationen
verloren. - Schleifenanfänge oder -enden liegen um eins neben dem
gewünschten Wert (Off-by-One-Fehler).
Drittens: Melden andere Tools Probleme mit dem Programm? Zum Beispiel ein Memory-Debugger, der »malloc«-Probleme oder den Zugriff auf Daten außerhalb des gültigen Bereichs abfängt.
Dieses Problem tritt am häufigsten bei Zeichenketten auf, weil ein Test auf Grenzen (Bounds-Checking) hier typischerweise vernachlässigt wird. Allerdings kann das Problem jedes Array, jeden Zeiger sowie dynamisch allozierten Speicher betreffen.






