Aus Linux-Magazin 04/2005

Fehlersuche mit dem grafischen Hilfsmittel Data Display Debugger

Bei der Jagd nach Bugs im eigenen Programmcode holen sich Entwickler gern Hilfe. Etwa beim DDD: Er stöbert das virtuelle Ungeziefer auf und gibt den Blick frei auf alle Daten, die das Programm produziert.

Wenn ein selbst geschriebenes Programm nicht ordnungsgemäß funktioniert oder gar abstürzt, beginnt der ungeliebte Teil der Programmierarbeit: die Fehlersuche, auch Debuggen genannt. Wichtig sind dann Hilfsprogramme, die diese Arbeit erleichtern und beschleunigen. Ein weiterer Artikel in dieser Ausgabe stellt das Standardtool hierfür vor, den GNU-Debugger GDB[2].

Der GDB ist allerdings so umfangreich, dass die wenigsten Programmierer alle Optionen und Befehle kennen und daher nur einen geringen Teil seiner Funktionen nutzen. Zudem sind die Bedienung per Kommandozeile und die reine Textausgabe nicht sehr komfortabel. Das ändert der DDD (Data Display Debugger,[1]), der wie GDB ebenfalls zur GNU-Familie gehört. DDD ist nicht nur eine grafische Oberfläche für Konsolen-Debugger, er erweitert die Debugging-Möglichkeiten erheblich. Seine Stärke ist die Darstellung von Daten und komplexen Datenstrukturen.

Der DDD-Benutzer kann sogar weitere externe Programme einbinden und eigene Buttons einrichten, um seine eigene kleine IDE zusammenzubauen. Damit steuert er den kompletten Edit-Compile-Run-Zyklus aus dem DDD. Die Software unterstützt aber nicht nur den GDB, sie hilft auch beim Debuggen von Python-, PHP- oder Perl-Skripten (über Letztere berichtet der Perl-Snapshot in dieser Ausgabe).

Fehler verfolgen

Der Code in Listing 1 zeigt ein fehlerhaftes Programm. Es erhält eine Reihe von Zeichenketten als Eingabeparameter und sollte sie sortiert ausgeben, allerdings funktioniert das nicht wie gewünscht. Beim Aufspüren der Fehlerstellen hilft der Data Display Debugger.

Listing 1:
Fehlerhaftes Programm

01 #include <stdio.h>
02 #include <string.h>
03 #include <stdlib.h>
04 
05 void slow_sort(char *arrIn[], int nrElements) {
06   int ready = 0,j;
07 
08   while(!ready){
09     ready=1;
10     for (j=0; j< nrElements; j++) {
11       if (strcmp(arrIn[j], arrIn[j+1]) > 0) {
12         char *tmp;
13         ready = 0;
14         tmp = arrIn[j];
15         arrIn[j] = arrIn[j+1];
16         arrIn[j+1] = tmp;
17       }
18     }
19   }
20 }
21 
22 int main(int argc, char *argv[]) {
23   int i, nrElems;
24   char **args;
25 
26   nrElems = argc-1;
27   args = (char **) malloc(nrElems*sizeof (char *));
28   for (i=0; i< nrElems; i++)
29     args[i] = strdup(argv[i+1]);
30 
31   slow_sort(args, nrElems);
32 
33   for(i=0; i<nrElems; i++)
34     printf("%d: %sn", i, args[i]);
35 
36   free(args);
37   return 0;
38 }

Damit der Debugger später nicht nur Maschinensprache anzeigt, sondern auch den Code, aus dem das Programm übersetzt wurde, muss der Compiler beim Übersetzen Debugging-Informationen einbinden. Beim GCC ist dafür die Option »-g« zuständig, das vollständige Kommando lautet »gcc -g slow_sort.c -o slow_sort«. Als erster Test dient folgender Aufruf:

./slow_sort Otto Willi Anna Xaver Zeppelin Gustav

Statt einer sortierten Liste spuckt der Rechner nur die Fehlermeldung »Speicherzugriffsfehler« aus (oder bei englischer Locale: »Segmentation fault«). Zur Ursachenforschung ist der Debugger mit dem Namen des Programms aufzurufen: »ddd ./slow_sort«. Abbildung 1 zeigt die drei Bereiche des DDD-Hauptfensters: Oben das Datenfenster (eingeschaltet per »View | Data Window«), in der Mitte das Fenster mit dem Quelltext und darunter die GDB-Konsole. Diese zeigt die Befehle, mit denen DDD den GDB steuert. Das Data-Window bleibt zunächst leer, hier zeigt DDD auf Wunsch die Werte von Variablen und Datenstrukturen an.

Um wie in Abbildung 1 die Werkzeug-Buttons ins Hauptfenster einzubetten statt sie als eigenständiges Fenster zu benutzen, wählt man im Menü »Edit | Preferences | Source« hinter »Tool Buttons Location« die Option »Source Window«. Hier versteckt sich auch die Option, die Zeilennummern im Quelltext einblendet: »Display Source Line Numbers«. Wer beim Start keinen Splash Screen und keinen Tip of the Day mag, schaltet beides unter »Startup« aus. Nach einem »Edit | Save Options« merkt sich DDD diese Einstellungen dauerhaft.

Abbildung 1: Das DDD-Hauptfenster teilt sich in Datenbereich (oben, noch leer), Quellcode (Mitte) und GDB-Konsole (unten). Beim Aufruf mit einer Namensliste bricht das Programm mit einem Segmentation Fault ab. Der Backtrace zeigt, wo im Quelltext der Fehler aufgetreten ist.

Abbildung 1: Das DDD-Hauptfenster teilt sich in Datenbereich (oben, noch leer), Quellcode (Mitte) und GDB-Konsole (unten). Beim Aufruf mit einer Namensliste bricht das Programm mit einem Segmentation Fault ab. Der Backtrace zeigt, wo im Quelltext der Fehler aufgetreten ist.

Programmstart

DDD hat das Programm noch nicht gestartet. Das erledigt entweder ein »run Parameter« in der GDB-Konsole oder der Menüpunkt »Program | Run…«. Bei Letzterem öffnet DDD ein Fenster, das die Aufrufparameter entgegennimmt (unter »Run with Arguments«). Beim Start mit einer Namensliste stürzt die Sortiersoftware dank eines Segmentation Fault prompt ab. Der GDB (siehe GDB-Konsole in Abbildung 1) informiert zusätzlich darüber, in welcher Funktion an welcher Adresse der Fehler aufgetreten ist.

Der Programmablauf ist innerhalb der Systemroutine »strcmp()« gescheitert. Diese Funktion ist Teil der Libc und vermutlich fehlerfrei – wahrscheinlich hat das Programm die Funktion falsch benutzt. Um das näher einzugrenzen, hilft die Aufrufreihenfolge, der so genannte Backtrace. Er ist über »Status | Backtrace« zu erreichen (Abbildung 1).

Das Ergebnis: Die »strcmp()«-Funktion wurde laut Eintrag »#1« im Backtrace vom Code in Zeile 11 (Listing 1) in der Sortierfunktion »slow_sort()« aufgerufen. Nach einem Klick auf den Eintrag wechselt die Darstellung im Quellcode-Fenster an die entsprechende Stelle.

Der grüne Pfeil vor dem »strcmp()«-Aufruf bestätigt, dass das Programm gerade an dieser Zeile war, bevor es abstürzte – der Pfeil markiert die gerade ausgeführte Funktion. Die beiden Tool-Buttons »Up« und »Down« bewegen den Cursor zur aufrufenden oder aufgerufenen Funktion, also nach oben oder unten im Call Stack.

Den nächsten Hinweis bei der Spurensuche geben die Parameter der »strcmp()«-Funktion selbst: Den ersten Parameter »arrIn[j]« im Quelltext-Fenster markieren und auf den Toolbar-Button »Print« klicken, dann wertet GDB den Ausdruck aus und präsentiert das Ergebnis in seiner Konsole. Es erscheint der unverdächtige Wert »$1 = 0x8049878 “Zeppelin”«. Beim zweiten »strcmp()«-Parameter »arrIn[j+1]« gibt GDB allerdings »$2 = 0x0« aus, der Inhalt dieses Array-Elements ist ein Null-Zeiger.

Spurensuche

Nächstes Indiz ist die Variable »j«. Ihr aktueller Wert lautet 5, »j+1« ist demnach 6. Das scheint zu passen, da das Programm mit sechs Namen gestartet wurde. Nächster Schritt: einen Haltepunkt (Breakpoint) an einer verdächtigen Stelle setzen, am besten beim While-Statement. Das geht mit einem Doppelklick am linken Rand dieser Zeile oder über den Button »Break«. In der ersten Spalte erscheint ein formschönes Stop-Symbol.

Beim erneuten Programmstart mit dem »Run«-Button verwendet DDD die gleichen Parameter wie beim Aufruf zuvor. Die Ausführung stoppt nun automatisch am Breakpoint. Nach einem Klick auf den Button »Next« führt das Programm die aktuelle Anweisung aus und springt zur nächsten. Der grüne Pfeil markiert wieder die aktuelle Zeile. Nach drei Klicks ist die For-Schleife erreicht. E

Es wäre unnötig mühsam, nach jedem »Next« wieder per »Print« auf die interessanten Variablen zuzugreifen, denn dafür gibt es ein weiteres Feature des DDD: statt »Print« den »Display«-Button verwenden, dann zeigt das Data-Fenster permanent den Inhalt des markierten Ausdrucks. Im Beispiel interessieren die drei Variablen »j«, »arrIn[j]« und »arrIn[j+1]«. Es genügt, sie nacheinander zu markieren und mit »Display« in das Datenfenster einzufügen (Abbildung 2). Damit DDD die Werte so kompakt zusammenfasst, muss jedoch unter »Edit | Preferences | Data« bei »Placement« die Variante »Clustered« gewählt sein.

Abbildung 2: Zwei Ausschnitte aus dem Data-Fenster des DDD: Oben die Variablen vor einer Wertänderung und unten danach.

Abbildung 2: Zwei Ausschnitte aus dem Data-Fenster des DDD: Oben die Variablen vor einer Wertänderung und unten danach.

Zielfahnder

Führt der Entwickler das Programm jetzt schrittweise weiter aus, dann sieht er sofort jede Veränderung der Variablen. Es könnte aber recht lange dauern, bis sich der Fehler manifestiert. Wenn eine Fehlerbedingung bekannt ist, geht es deutlich schneller. Der Zugriff auf das Array schlägt fehl, wenn »j« den Wert »5« annimmt, also soll das Programm genau in diesem Moment anhalten.

Das geht recht einfach: einen Haltepunkt in Zeile 11 setzen, also die erste Zeile der For-Schleife. Mit der rechten Maustaste das Kontextmenü der Stop-Marke öffnen und »Properties« wählen, es öffnet sich ein Fenster mit diversen Eigenschaften des Haltepunkts (Abbildung 3). Die Bedingung »j==5« im Feld »Condition« sorgt dafür, dass DDD das Programm an diesem Breakpoint nur anhält, wenn »j« den Wert »5« hat.

Abbildung 3: Ein Haltepunkt kann im DDD an Bedingungen geknüpft sein. Hier stoppt der Debugger nur, wenn die Variable »j« den Wert »5« hat.

Abbildung 3: Ein Haltepunkt kann im DDD an Bedingungen geknüpft sein. Hier stoppt der Debugger nur, wenn die Variable »j« den Wert »5« hat.

Da der Haltepunkt an eine Bedingung geknüpft ist, verändert sich das Stop-Zeichen im Quelltext, es enthält nun zusätzlich ein kleines Fragezeichen. Der andere Haltepunkt ist nun nicht mehr wichtig, er lässt sich über den Kontextmenü-Eintrag »Disable Breakpoint« entfernen. Ein Klick auf »Cont« (für Continue) setzt das Programm so lange fort, bis »j« den Wert »5« erreicht.

Bug gefunden

Erfahrene C-Programmierer werden es längst ahnen und sehen sich nun bestätigt: Grund für den Absturz ist ein Fehler beim Indizieren des Array. Ein Array mit sechs Elementen läuft von 0 bis 5. Im Falle von »j==5« erfolgt ein Zugriff auf »arrIn[j+1]« und damit auf »arrIn[6]«. Das Feld liegt aber jenseits der Array-Grenzen.

Der Fehler steckt in der Abbruchbedingung der For-Schleife: »j« dürfte nur bis »nrElements-<$>1« laufen. Statt sofort den Quelltext zu korrigieren und das Programm neu zu übersetzen ist es sinnvoller, die Vermutung zunächst im laufenden Programm zu überprüfen. Das geht mit DDD-Bordmitteln: Der Debugger kann die Variable »nrElements« um 1 vermindern und die Routine noch einmal von Anfang an laufen lassen.

Für den bequemen Zugriff auf den Wert einer Variablen empfiehlt es sich, sie ins Data-Fenster aufzunehmen. Also: »nrElements« im Quelltext markieren und »Display« drücken (oder doppelt auf die Variable klicken). Danach den Eintrag im Data-Window markieren, sein Kontextmenü öffnen, den Punkt »Set value…« wählen und »5« eintragen.

Anschließend den Cursor im Quelltext an den Anfang der Zeile 8 setzen (vor die While-Anweisung) und im Kontextmenü die Option »Set Execution Position« wählen. Damit wird Zeile 8 zur aktuellen Position und ein »Cont« führt das Programm korrekt zu Ende.

Wie sich der Debugger verhält, wenn er einen Breakpoint erreicht, ist im DDD frei konfigurierbar, er kann weit mehr als nur den Prozesses anhalten. Neben den bedingten Haltepunkten findet sich im Properties-Fenster (Abbildung 3) die Option »Ignore Count«. Mit ihr hält der Debugger erst dann an, wenn das Programm den Breakpoint mehrmals überschritten hat.

Breakpoint-Magie

Besonders mächtig ist der Punkt »Commands«: Damit setzt der Debugger automatisch DDD-Kommandos ab, sobald das Benutzerprogramm einen Haltepunkt erreicht. Sinnvoll ist das zum Beispiel, um den Wert einer Variablen auszugeben und dann mit »Cont« die Ausführung fortzusetzen. Das ersetzt »printf()«-Kommandos im Quelltext. Der Benutzer könnte beim Erreichen eines Haltepunkts auch automatisch andere Breakpoints aktivieren.

Wer beim Debugging seines Programms viele Breakpoints braucht, den Inhalt mehrerer Variablen verfolgt und einige Bedingungen eingefügt hat, will das nicht bei jedem DDD-Start erneut einstellen. Muss er auch nicht, da DDD seine Sitzungen speichert: Per »File | Save Session as…« landen alle Einstellungen auf der Festplatte, nach einem Neustart holt »File | Open Session« die alte Konfiguration zurück.

In den Menüs des DDD finden sich noch viele weitere praktische Perlen, besonders das grafische Darstellen von Daten ist eine Stärke des DDD. Das hilft dabei, komplizierte Datenstrukturen besser zu verstehen. Abbildung 4 zeigt einen Ausschnitt einer dynamischen Baumstruktur. Per Doppelklick auf die Zeiger navigiert der Benutzer bequem durch die Strukturen. Damit sieht er beispielsweise Zyklen in verketteten Listen.

Abbildung 4: Datenstrukturen übersichtlich darstellen ist die große Stärke des DDD. Hier zeigt er eine Baumstruktur. Der Benutzer öffnet neue Zweige, indem er doppelt auf die Zeiger in den Kästchen klickt.

Abbildung 4: Datenstrukturen übersichtlich darstellen ist die große Stärke des DDD. Hier zeigt er eine Baumstruktur. Der Benutzer öffnet neue Zweige, indem er doppelt auf die Zeiger in den Kästchen klickt.

Plots mit Gnuplot

Der Data Display Debugger bindet Gnuplot ein[3], um aus dem Debugger heraus Werte als Diagramm darzustellen. Das funktioniert auf ein- und zweidimensionalen Arrays, die Gnuplot in 2D- oder 3D-Kurven darstellt. Abbildung 5 zeigt den Plot eines C-Array, das die Werte einer trigonometrischen Funktion enthält. Diese Grafiken behandelt DDD genauso wie den Inhalt des Datenfensters, bei jeder Aktualisierung bringt er den Plot auf den neuesten Stand.

Abbildung 5: DDD zeichnet auch Diagramme. Die Daten stammen aus dem Programm, das der Debugger durchsucht, für die Darstellung ist Gnuplot verantwortlich.

Abbildung 5: DDD zeichnet auch Diagramme. Die Daten stammen aus dem Programm, das der Debugger durchsucht, für die Darstellung ist Gnuplot verantwortlich.

Um zum Beispiel den Sortierablauf zu verfolgen, soll DDD ein numerisches Feld als Plot darstellen. Zusätzlich sind Breakpoints mit »cont«-Kommando nötig, sie stoppen das Programm, DDD aktualisiert die Display- und Plot-Fenster und setzt wegen des Cont-Kommandos die Ausführung fort.

Multitalent

DDD unterstützt auch Anwendungen mit mehreren Threads, sofern der verwendete Debugger diese Funktionalität bereitstellt. Der Zugriff erfolgt über das Menü »Status | Threads«. Hier springt man von Thread zu Thread, wobei DDD das Backtrace-Fenster aktualisiert und immer die Aufrufkette des gewählten Thread anzeigt.

Die mächtige Datenanzeige des Data Display Debugger stellt die meisten einfachen grafischen GDB-Frontends bei weitem in den Schatten. Auch wenn diese besonderen Fähigkeiten in eigenen Projekten nicht gefragt sein sollten: DDD enthält alle wichtigen Funktionen, um die Fehlerjagd kräftig zu unterstützen. Weidmannsheil! (fjl)

Infos

[1] DDD: [http://www.gnu.org/software/ddd/]

[2] GDB: [http://www.gnu.org/software/gdb/]

[3] Gnuplot: [http://gnuplot.sourceforge.net]

Der
Autor

Ralf Neubersch arbeitet seit dem Abschluss seines Informatikstudiums freiberuflich als Entwickler unter Solaris, Linux und Windows.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben