17.6 Debuggen mit gdb und ddd
Das Thema nach der Fehlersuche in Programmen ist so alt wie der Computer selbst. Meines Erachtens gibt es auch keinen Programmierer, der davon befreit ist. Auf einmal gibt die Anwendung irgendwelchen Murks aus oder stürzt aus nicht ersichtlichen Gründen ab. Treten Fragen zu diesen oder ähnlichen Problemen auf, müsste eine Antwort hierzu lauten: Was sagt der Debugger dazu?
Hierzu möchte ich noch ein wenig ausholen. Sicherlich, das Thema »Debuggen« ist Bücher füllend – aber häufig muss man nicht gleich bei jeder Kleinigkeit zum Debugger greifen. Als allererstes sollte man sich (logischerweise) nochmals den Quellcode ansehen, wo man vermutet, dass das Programm abstürzt oder eben nicht wie erwartet ausgeführt wird. Jetzt könnte man an der vermuteten Codestelle etwas ausgeben lassen, z. B. den Programmzustand oder eventuell auch einen einfachen String. Wissen Sie in etwa, in welcher Funktion Ihr Programm abstürzt, können Sie hier z.B eine einfache Textausgabe wie
printf("DEBUG\n");
machen. Rutschen Sie mit der Anweisung immer eine Zeile tiefer, bis das Programm abstürzt und der Text DEBUG nicht mehr auf dem Bildschirm ausgegeben wird. Natürlich können Sie hierfür auch den Zustand gewisser Variablen ausgeben lassen, sofern dies an der Stelle sinnvoll ist, oder ein einfaches getchar() einbauen, welches das Programm anhält und auf ein (ENTER) wartet. Meistens findet man so recht schnell den Fehler, der das Programm abstürzen lässt. Des Weiteren hat es sich bewährt, häufiger eine assert()-Anweisung in den Code einzubauen.
Hat man hiermit keinen Erfolg gehabt, kann man ja den Programmzustand verändern. Häufig bewährt hat es sich, das Programm mit anderen Daten zu füttern, als dies der Fall ist, wenn das Programm abstürzt. Das Verändern des Quellcodes an einer vermutlich kritischen Stelle kann auch recht sinnvoll sein. Eventuell können Sie hierbei auch die eine oder andere Codezeile auskommentieren. Bitte beachten Sie beim Verändern der Codezeile, dass Sie den alten Zustand wiederherstellen können. Hierfür empfiehlt es sich, CVS oder RCS zu verwenden. Und um gleich beim Thema »Versionsverwaltung« zu bleiben: Gibt es von der unstabilen Programmversion mehrere Versionen, sollten Sie die letzte stabile Version aus dem Archiv auschecken bzw. die letzte stabile Version mit der unstabilen Version vergleichen (siehe CVS und RCS).
Zwar gibt es hier und da noch weitere Möglichkeiten, ein Programm mit einfachen Mitteln zu debuggen. Sie können aber auch zum Debugger GDB greifen, einem Werkzeug, womit Sie Programme ausführen und diese während der Ausführung anhalten und genauer untersuchen können. Hierbei ist es möglich, einzelne Variablen unter die Lupe zu nehmen und deren Werte zu verändern. Ebenso lässt sich hiermit der Aufruf einzelner Funktionen Schritt für Schritt verfolgen. All dies und noch einiges mehr lässt sich wohlgemerkt während der Ausführung des Programms bewerkstelligen.
In diesem Buch wird zwar der Konsolen-Debugger GDB beschrieben, doch wenn Sie dessen Ausführung und Einsatz verstanden haben, ist es nicht mehr schwer, auf die mittlerweile zahlreichen zur Verfügung stehenden grafischen Frontends zurückzugreifen. Eines der komfortableren, das ich Ihnen diesbezüglich empfehlen kann, ist DDD, entwickelt von Dorothea Lütkehaus und Andreas Zeller.
Programm übersetzen für gdb
Als ersten Schritt müssen Sie die Anwendung für den Debugger übersetzen. Dies geschieht mit der Option -g. Durch Verwendung dieser Option generiert der Compiler eine größere Symboltabelle. Einfach ausgedrückt, die ausführbare Datei benötigt auch mehr Plattenplatz, da diese jetzt Informationen für den Debugger beinhaltet.
Häufig wurde im Buch der Debugger mit dem Schalter –ggdb3 verwendet, womit noch viel mehr Informationen hinzugefügt werden, was auch bedeutet, dass die ausführbare Datei wesentlich größer wird. Sollte aber ein Feature in GDB nicht funktionieren, kann es sein, dass zu wenige Infos vorhanden sind. Die Größe der Debug-Version einer Anwendung hängt wesentlich ungefähr geschätzt von der Anzahl der Objektdateien ab. Ein normales Heimprogramm mag »so« max. 100 KB ergeben, mit -ggdb3 ist es geringfügig mehr. Mittelständische Anwendungen (»Serverzubehöranwendungen«), die mit 300 KB anfangen (10+ Dateien), belegen bis zu 1,5 MB mit dem Schalter -ggdb3 (nur ein Beispiel), Serveranwendungen wie Samba hingegen sind im -O2 -g0-Modus gerade einmal 3,03 MB groß (/usr/sbin/smbd), während es mit -ggdb3 -O0 schon gut 170 MB werden können.
Debugger gdb starten
Starten können Sie den GDB mit folgender Eingabe in der Kommandozeile:
gdb programm [coredatei]
Das programm ist das Beispiel, das Sie debuggen wollen. Optional kann auch als zusätzlicher Parameter eine Core-Dump-Datei verwendet werden, falls eine, bei einem früheren Versuch, das Beispiel zum Laufen zu bringen, angelegt wurde. Der Vorteil einer Core-Dump-Datei ist, dass man sofort findet, wo das Programm misslungen ist (das trifft nicht zu, wenn man einen Stack Smash hat, also die Rücksprungadressen zerbombt sind) und vor allem auch warum. Wollen Sie den GDB wieder beenden, reicht ein einfaches quit (oder 'q') in der GDB-Shell.
$ gdb array1
GNU gdb 5.3
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions. There is absolutely no warranty for
GDB. Type "show warranty" for details.
This GDB was configured as "i586-suse-linux"...
(gdb) quit
Inhalt der Datei anzeigen
Wenn Sie die ausführbare Datei mit dem GDB gestartet haben, werden Sie sich wohl zuerst einen Überblick über den Quellcode der ausführbaren Datei verschaffen wollen. Hierzu können Sie das Kommando list (oder 'l') verwenden:
(gdb) list
11
12 int main(int argc, char **argv)
13 {
14 int ein_array[10];
15 int i;
16
17 fill_Array(ein_array);
18 for(i = 0; i < NUM; i++)
19 printf("Wert %d: %d\n",i, ein_array[i]);
20
(gdb) list 1,5
1 #include <stdio.h>
2 #define NUM 100
3
4 void fill_Array( int *arr )
5 {
(gdb) list fill_Array
1 #include <stdio.h>
2 #define NUM 100
3
4 void fill_Array( int *arr )
5 {
6 int i;
7 for( i = 0; i < NUM; i++)
8 arr[i] = i;
9 }
10
(gdb) list 4, 9
4 void fill_Array( int *arr )
5 {
6 int i;
7 for( i = 0; i < NUM; i++)
8 arr[i] = i;
9 }
(gdb)
Geben Sie list ohne irgendwelche Argumente an, zeigt der GDB die Zeilen um den Quellcode, der gerade ausgeführt wurde (wurde der Quellcode noch nicht ausgeführt, wird der Code um die main()-Funktion aufgelistet). Die Zeilen auf der linken Seite entsprechen den Zeilennummern im Quellcode. Wiederholen Sie den Aufruf von list ohne Argumente, listet GDB immer die nächsten Zeilen so lange auf, bis Sie am Ende des Quellcodes angekommen sind. Standardmäßig werden bei keiner Angabe von Argumenten immer zehn Zeilen eingelesen und ausgegeben. Wollen Sie bestimmte Zeilen ausgeben, die Sie debuggen oder lesen wollen, können Sie auch, wie im Beispiel gesehen, Folgendes angeben:
(gdb) list 1,5
Hiermit werden die Zeilen 1 bis 5 aufgelistet. Gibt man nur eine Zeile an (list 37), so wird 37 +/– 5 angezeigt. Natürlich lassen sich hiermit auch bestimmte Funktionen auflisten, wie im Beispiel demonstriert wurde:
(gdb) list fill_Array
Programm mit dem gdb ausführen
Bisher haben Sie zwar das Programm in GDB geladen, aber ausführen müssen Sie dies explizit. Hierzu genügt ein einfaches run (oder 'r') in der GDB-Shell:
(gdb) run
Starting program: /home/tot/debuggen/array1
Program received signal SIGSEGV, Segmentation fault.
0x0000000f in ?? ()
(gdb)
Natürlich lässt sich hier bei unnötig langer Ausgabe des Programms die Standardausgabe auch umleiten. Ebenso ist es möglich, die Eingabe umzuleiten. Im Beispiel wurde die Anwendung komplett ausgeführt und wie schon außerhalb der Welt von GDB mit einem Segmentation Fault (unerlaubter Speicherzugriff) beendet.
Müssen der Anwendung noch Argumente aus der Kommandozeile übergeben werden, können Sie dies mit set args folgendermaßen machen:
(gdb) set args argument1 argument2
(gdb) show args
Argument list to give program being debugged when it is started is
"argument1 argument2".
(gdb)
Mit show args können Sie sich die Argumente, mit denen die Anwendung aus der Kommandozeile gestartet wird, anzeigen lassen. Alternativ können die Argumente auch bei run mitgegeben werden:
(gdb) run arg1 arg2 ...etc
Haltepunkte setzen
Bisher brachte Ihnen der Debugger nicht mehr ein, als wenn Sie das Programm ohne ihn gestartet hätten. Um ein Programm sinnvoll zu debuggen, wird während der Ausführung des Programms an einer oder mehreren (von dem Programmierer vermuteten) kritischen Stellen ein Haltepunkt (break-Point) gesetzt. An dieser Stelle wird die Ausführung des Programms dann angehalten. Dabei können Sie dann z. B. Variablen überprüfen, ändern oder weitere GDB-Kommandos ausführen. Haltepunkte werden mit dem Kommando break (oder 'b') gesetzt. Diese gibt es in den folgenden Varianten:
Kommando
|
Bedeutung
|
break zeilennummer
|
Ausführung vor der angegebenen Zeilennummer stoppen
|
break dateiname:zeilennummer
|
Ausführung vor der angegebenen Zeilennummer in der Datei dateiname stoppen
|
break zeilennumer if bedingung
|
Ausführung vor der angegebenen Zeilennummer nur dann stoppen, wenn bedingung zutrifft
|
break Funktion
|
Ausführung vor dem Eintreten in die Funktion stoppen. Der Hardware-Meister verrät: GDB hält erst an, wenn ein neues Stack-Frame angefangen wurde. Das passiert bei der »mov ebp, esp«-Instruktion.
|
break dateiname:Funktion
|
Ausführung vor dem Eintreten in die Funktion in der Datei dateiname stoppen
|
break Funktion if bedingung
|
Ausführung vor dem Eintreten in die Funktion nur dann stoppen, wenn bedingung zutrifft
|
Ein Beispiel:
(gdb) list 4, 9
4 void fill_Array( int *arr )
5 {
6 int i;
7 for( i = 0; i < NUM; i++)
8 arr[i] = i;
9 }
(gdb) break array1.c:7
Note: breakpoint 1 also set at pc 0x804831a.
Breakpoint 2 at 0x804831a: file array1.c, line 7.
(gdb) run
Starting program: /home/tot/debuggen/array1
Breakpoint 2, fill_Array (arr=0xbffff3b0) at array1.c:7
7 for( i = 0; i < NUM; i++)
Hier wurde bei der Ausführung der Quelldatei array.c in der Zeile 7 ein Haltepunkt gesetzt, der sofort aktiv wird.
Ausführung nach einem Haltepunkt fortsetzen
Um die Ausführung der Anwendung nach einem break-Punkt wieder aufzunehmen, müssen Sie das continue-Kommando (oder kurz: 'c') verwenden. Nach einem continue-Aufruf wird die Ausführung bis zum Programmende, dem nächsten break-Punkt oder bei Auftreten eines Fehlers fortgesetzt.
(gdb) continue
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x0000000f in ?? ()
Verwaltung von Haltepunkten
Da mit der Zeit eine Menge Haltpunkte in einer Sitzung mit dem Debugger zusammenkommen können, finden Sie mit info breakpoints ein Kommando, durch das Sie den Überblick zu den Haltepunkten bekommen. Um einen nicht benötigten break-Punkt wieder zu entfernen, können Sie das Kommando delete verwenden.
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804831a in fill_Array at array1.c:7
stop only if i == 5
2 breakpoint keep y 0x0804831a in fill_Array at array1.c:7
breakpoint already hit 1 time
(gdb) delete 2
(gdb) info breakpoints
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804831a in fill_Array at array1.c:7
stop only if i == 5
Mit dem Kommando clear (oder kurz 'cl') können Sie alle Haltepunkte auf einmal entfernen. Vorübergehend ausschalten können Sie einen Haltepunkt mit dem Kommando disable und der Nummer als Argument. Aktivieren können Sie diesen Haltepunkt wieder mit enable und der entsprechenden Nummer.
Einzelschritte
Gewöhnlich werden Sie, wenn Sie einen Haltepunkt gesetzt haben, auch einzelne Anweisungen des Quellcodes ausführen wollen. Hierzu stehen Ihnen zwei Kommandos zur Verfügung; einmal das Kommando step (kurz s), das eine Anweisung ausführt und, falls es sich um einen Funktionsaufruf handelt, in diese Funktion eintritt und immer nur einen Schritt nach dem anderen ausführt. Mit dem zweiten Kommando next (kurz n) hingegen wird eine Zeile ausgeführt oder eine Anweisung, sollte sie auf mehreren Zeilen liegen. Um den Sachverhalt besser zu verstehen, hier ein Beispiel:
(gdb) list
13 {
14 int ein_array[10];
15 int i;
16
17 fill_Array(ein_array);
18 //for(i = 0; i < NUM; i++)
19 //printf("Wert %d: %d\n",i, ein_array[i]);
20
21 return 0;
22 }
(gdb) break 16
Breakpoint 4 at 0x8048355: file array1.c, line 16.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/tot/debuggen/array1
Breakpoint 5, main (argc=1, argv=0xbffff434) at array1.c:17
17 fill_Array(ein_array);
(gdb) next
21 return 0;
(gdb) next
22 }
(gdb) continue
...
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/tot/debuggen/array1
Breakpoint 5, main (argc=1, argv=0xbffff434) at array1.c:17
17 fill_Array(ein_array);
(gdb) step
fill_Array (arr=0xbffff3b0) at array1.c:7
7 for( i = 0; i < NUM; i++)
(gdb) step
8 arr[i] = i;
(gdb) step
7 for( i = 0; i < NUM; i++)
(gdb) step
8 arr[i] = i;
(gdb) step
7 for( i = 0; i < NUM; i++)
(gdb) continue
Continuing.
...
Hier wurde vor dem Funktionsaufruf fill_Array in der main-Funktion ein break-Point gesetzt. Wenn Sie das Programm jetzt laufen lassen und mit next den nächsten Schritt ausführen, wird die komplette Funktion fill_Array ausgeführt und mit der Ausführung in der main-Funktion hinter fill_Array fortgesetzt. Verwenden Sie hingegeben beim selben break-Point das step-Kommando, werden die Anweisungen in der Funktion fill_Array Schritt für Schritt ausgeführt.
Datenausgabe
Natürlich müssen Sie zum Debuggen auch auf die Werte der Variablen zugreifen können. Hierzu können Sie das print-Kommando (kurz p) von GDB verwenden. Mit dem print-Kommando lässt sich mehr machen, als man diesem einfachen Kommando ansehen würde:
|
Funktionsaufrufe innerhalb von Programmen mit Übergabe von Parametern. Z. B. könnten Sie eine Funktion aufrufen mit |
(gdb) print summe_von(4, 9, 3)
$1 = 16
|
Damit haben Sie eine globale Variable definiert, die zur weiteren Ausführung des Programms sichtbar bleibt. |
|
|
|
Auflisten von komplexen Datenstrukturen. Wenn Sie z. B. folgende Datenstruktur in einem zu debuggenden Programm haben: |
struct komplex {
int i;
char a;
char j[6];
};
|
dann können Sie aus dieser Struktur sämtliche Informationen herauslesen: |
|
|
(gdb) print komplex
$1 = (struct komplex *) 0x40014814
(gdb) print *komplex
$2 = {i = 1073825736, a = 0 '\0', j = "\0\0\0\0\0"}
|
Werteverlauf-Elemente (History). Sicherlich sind Ihnen bei den print-Ausgaben $1, $2, $3 und so weiter aufgefallen. Dabei handelt es sich um so genannte Werteverlauf-Elemente, die Sie jederzeit wieder verwenden können. Z. B. folgender Werteverlauf: |
(gdb) print summe_von(4, 9, 3)
$1 = 16
|
Jetzt können Sie auf das Elemente $1 folgendermaßen zugreifen: |
|
|
(gdb) print $1 + 5
$2 = 21
(gdb) print $1 + $2
$3 = 37
|
Jedes dieser Werteverlauf-Elemente gilt bis zum Ende der Programmausführung von GDB, und darauf kann jederzeit zurückgegriffen werden. |
|
|
|
Künstliche Felder. Mit künstlichen Feldern können Sie Speicherbereiche wie Arrays oder dynamisch allokierten Speicher ausgeben. Hierzu einige Beispiele: |
(gdb) list
8 };
9
10 static void fill_Array( int *arr )
11 {
12 int i;
13 for( i = 0; i < NUM; i++)
14 arr[i] = i;
15 }
16
17
(gdb) break 15
Breakpoint 5 at 0x8048343: file array1.c, line 15.
(gdb) run
Starting program: /home/tot/debuggen/array1
Breakpoint 5, fill_Array (arr=0xbffff3b0) at array1.c:15
15 }
(gdb) print arr@10
$6 = {0xbffff3b0, 0x400144a0, 0x1a, 0x40014678, 0xbffff3f0,
0x4000ae8f,0x40014814, 0x40014e10, 0x0, 0x1}
(gdb) print arr@15
$7 = {0xbffff3b0, 0x400144a0, 0x1a, 0x40014678, 0xbffff3f0,
0x4000ae8f, 0x40014814, 0x40014e10, 0x0, 0x1, 0x2, 0x3,
0x4, 0x5, 0x6}
(gdb) print arr[0]@10
$8 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb) print arr[0]@15
$9 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}
(gdb) print arr[20]@5
$10 = {20, 21, 22, 23, 24}
|
Mit arr@10 haben Sie sich die Adressen der ersten zehn Arrays ausgeben lassen. Mit arr[0]@10 hingegen habe Sie zehn Werte ausgegeben, beginnend bei 0, die sich im Array arr befinden. arr[20]@5 dagegen gibt fünf Array-Werte aus, allerdings beginnend bei dem Element 20 des Arrays. |
|
|
Variablen überprüfen und verändern
Außer die Variablen mit print auszugeben, können Sie auch ermitteln, von welchem Typ eine Variable ist. Des Weiteren ist es möglich, einer Variablen einen Wert zuzuweisen. Das Ermitteln des Datentyps wird mit dem Kommando whatis und für komplexere Datenstrukturen mit ptype erledigt:
(gdb) list
10 void fill_Array( int *arr )
11 {
12 int i;
13 for( i = 0; i < NUM; i++)
14 arr[i] = i;
15 }
16
17
18 int main(int argc, char **argv)
19 {
(gdb) whatis i
type = int
(gdb) whatis *arr
type = int
(gdb) whatis arr[0]
type = int
(gdb) next
main (argc=16, argv=0x11) at array1.c:28
28 return 0;
(gdb) whatis komp
type = struct komplex *
(gdb) ptype komp
type = struct komplex {
int i;
char a;
char j[6];
} *
(gdb) usw.
Werte können Sie den Variablen mit dem Kommando set variable oder aber auch mit print übergeben.
(gdb) print arr[5]
$15 = 5
(gdb) set variable arr[5] = 100
(gdb) print arr[5]
$16 = 100
(gdb) print arr[5] = $16 + $15
$17 = 105
Weiteres
Zum Schluss der Einführung zum Debugger GDB noch einige Quickies, mit denen Sie noch etwas komfortabler arbeiten können.
|
Wollen Sie ein zuvor geschriebenes Kommando wiederholen, reicht ein wiederholtes Drücken der (ENTER)-Taste, was sich gerade beim schrittweisen Debuggen als sehr hilfreich erweist. |
|
Die Kommandos in GDB müssen nicht ganz ausgeschrieben werden, wie schon des Öfteren angedeutet. Einige Kommandos beim GDB können mit dem Anfangsbuchstaben abgekürzt werden. Z. B. r für run, c für continue, n für next, s für step oder q für quit, damit lässt sich der Tippaufwand enorm verringern. Mehr hierzu finden Sie in den Manual Pages von GDB. |
Tipp Versuchen Sie doch einmal, das eben durchgeführte Kapitel mit dem grafischen Frontend DDD auszuführen. Im Prinzip müssen Sie hierbei nur die Maus statt der Tastatur verwenden.
|
|