Copyright © 1995 by O'Reilly/International Thomson Verlag

Bitte denken Sie daran: Sie dürfen zwar die Online-Version ausdrucken, aber diesen Druck nicht fotokopieren oder verkaufen.

Wünschen Sie mehr Informationen zu der gedruckten Version des Buches "Linux - Wegweiser zur Installation & Konfiguration", dann klicken Sie hier.


Kapitel 6

Debuggen mit gdb

Gehören Sie zu den Programmierern, die Verachtung empfinden angesichts der Idee, einen Debugger zu benutzen, um ein Programm schrittweise ablaufen zu lassen? Sind Sie der Meinung, daß der Programmierer selbst schuld ist, wenn er seinen eigenen Code nicht mehr versteht und deshalb Fehler macht? Überprüfen Sie Ihren Code vor dem geistigen Auge, bewaffnet mit Vergrößerungsglas und Zahnstocher? Entstehen die meisten Ihrer Fehler, weil Sie ein Zeichen ausgelassen haben, etwa weil Sie = benutzt haben statt +=?

Vielleicht sollten Sie gdb kennenlernen -- den GNU-Debugger. Ob Sie es wissen oder nicht: GNU ist Ihr Freund. Der Debugger kann Fehler entdecken, die selten auftreten und schwierig zu finden sind, und in deren Folge Core-Dumps, Speicherprobleme und unvorhersehbares Verhalten auftreten können (sowohl im Programm als auch beim Programmierer). Manchmal können schon kleinste Fehler im Code große Verwirrung stiften; ohne die Hilfe eines Debuggers wie gdb kann es fast unmöglich sein, solche Fehler zu finden. Dies gilt besonders dann, wenn ein Programm mehr als nur ein paar hundert Zeilen lang ist. In diesem Abschnitt werden wir die nützlichsten Eigenschaften von gdb anhand von Beispielen besprechen.

Es gibt auch ein Buch zum Thema gdb -- Debugging with GDB von der Free Software Foundation.

gdb ist in der Lage, Programme zur Laufzeit zu debuggen oder die Ursache eines Programmabsturzes mit Hilfe eines Core-Dumps (Speicherauszug) zu ermitteln. Programme, die mit gdb zur Laufzeit untersucht werden, können entweder aus gdb heraus gestartet werden oder selbständig ablaufen -- d.h., gdb kann sich an einen laufenden Prozeß anhängen, um ihn zu überwachen. Wir werden erst zeigen, wie Programme von Fehlern befreit werden, die aus dem gdb heraus gestartet werden, und dann erklären, wie Sie gdb an einen laufenden Prozeß anhängen und Core-Dumps auswerten.

Ein Programm schrittweise ablaufen lassen

Unser erstes Beispiel ist ein Programm namens trymh , das Kanten in einem Schwarz-Weißbild findet. trymh benutzt eine Bilddatei als Eingabe, führt einige Berechnungen durch und gibt eine andere Bilddatei aus. Leider stürzt es bei jedem Aufruf mit dieser Meldung ab:

papaya$ trymh < image00.pgm > image00.pbm 
Segmentation fault (core dumped)

Wir könnten jetzt den gdb benutzen, um den Core-Dump zu analysieren, aber in diesem Beispiel wollen wir statt dessen zeigen, wie Sie das laufende Programm schrittweise durchgehen (trace). (3)

Bevor wir mit gdb das Programm trymh schrittweise laufen lassen, muß sichergestellt sein, daß es mit Debugging-Code kompiliert wurde (siehe den Abschnitt » Den Code debuggen « weiter oben in diesem Kapitel). trymh sollte also mit dem Schalter -g zum gcc kompiliert werden.

Es ist nicht verboten, gleichzeitig mit dem Debugging-Code ( -g ) auch die Optimierung ( -O ) einzuschalten -- es kann aber auch nicht empfohlen werden. Das Problem besteht darin, daß der gcc besser ist, als ihm guttut. Wenn Sie z.B. innerhalb einer Funktion an zwei verschiedenen Stellen zwei identische Codezeilen schreiben, kann es passieren, daß gdb unerwarteterweise die zweite Zeile statt der ersten anspringt. Das liegt daran, daß gcc aus den beiden Zeilen eine einzige Zeile Maschinencode gemacht hat, die für beide Fälle benutzt wird.

Einige der Optimierungen, die gcc automatisch durchführt, können bei der Arbeit mit einem Debugger ziemlich verwirrend wirken. Benutzen Sie den Schalter -O0 (also Strich-OOH-NULL), wenn Sie alle Optimierungen ausschalten möchten (auch die, die ohne den Schalter -O durchgeführt werden).

Jetzt sind wir bereit, den gdb zu starten und das Problem zu untersuchen:

papaya$ gdb trymh 
GDB is free software and you are welcome to 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.
GDB 4.8, Copyright 1993 Free Software Foundation, Inc...
(gdb)

gdb wartet jetzt auf einen Befehl. (Mit help erhalten Sie eine Liste der verfügbaren Befehle). Als erstes sollten wir das Programm starten, um sein Verhalten beobachten zu können. Wenn wir allerdings sofort den Befehl run eingeben, wird das Programm einfach ausgeführt, bis es beendet oder abgestürzt ist.

Wir müssen erst irgendwo im Programm einen Breakpoint (Haltepunkt) setzen. Ein Breakpoint ist einfach eine Stelle im Programm, an der gdb anhalten soll, damit wir seine Ausführung beeinflussen können. Der Einfachheit halber wollen wir einen Breakpoint auf die erste Zeile des eigentlichen Codes setzen, so daß das Programm anhält, wenn es gerade anfängt den Code auszuführen. Mit dem Befehl list lassen Sie mehrere Codezeilen gleichzeitig anzeigen (die Anzahl können Sie einstellen):

(gdb) list 
12        main() {
13
14         FloatImage inimage;
15         FloatImage outimage;
16         BinaryImage binimage;
17         int i,j;
18
19         inimage = (FloatImage)imLoadF(IMAGE_FLOAT,stdin);
20         outimage = laplacian_float(inimage);
21
(gdb) break 19 
Breakpoint 1 at 0x289c: file trymh.c, line 19.
(gdb)

In Zeile 19 des Quellcodes ist jetzt ein Breakpoint gesetzt. Sie können mehrere Haltepunkte in einem Programm setzen. Diese können bedingt sein (d.h., sie werden nur berücksichtigt, wenn ein bestimmter Ausdruck wahr ist), sie können auch bedingungslos, verzögert, zeitweise deaktiviert usw. sein. Sie können Breakpoints auf eine bestimmte Codezeile setzen, auf eine bestimmte Funktion, eine Reihe von Funktionen oder auf etwas anderes. Sie haben außerdem die Möglichkeit, mit dem Befehl watch einen Watchpoint (Beobachtungspunkt) zu setzen; ein Watchpoint funktioniert so ähnlich wie ein Breakpoint, wird aber nur dann aktiv, wenn ein bestimmtes Ereignis eintritt -- nicht unbedingt in einer bestimmten Codezeile im Programm. Wir kommen weiter hinten im Kapitel noch auf Breakpoints und Watchpoints zurück.

Als nächstes benutzen wir den Befehl run , um das Programm zu starten. run akzeptiert dieselben Argumente, die Sie auch trymh auf der Befehlszeile mitgeben -- weil der Befehl zur Ausführung an /bin/sh weitergereicht wird, sind hier auch Shell-Wildcards, Ein/Ausgabeumleitung usw. möglich:

(gdb) run < image00.pgm > image00.pfm 
Starting program: /amd/dusk/d/mdw/vis/src/trymh < image00.pgm >
image00.pfm

Breakpoint 1, main () at trymh.c:19
19         inimage = (FloatImage)imLoadF(IMAGE_FLOAT,stdin);
(gdb)

Wie erwartet wird der Breakpoint gleich in der ersten Codezeile erreicht -- unsere Stunde ist gekommen.

Die beiden wichtigsten Befehle im Einzelschrittmodus sind next und step . Beide führen die nächste Codezeile im Programm aus; der Unterschied liegt darin, daß step auch in alle Funktionsaufrufe im Programm eintaucht, während next nur bis zur nächsten Codezeile in derselben Funktion weitergeht. Der Befehl next führt den Code der Funktionen, die er vorfindet, zwar stillschweigend aus, zeigt ihn aber nicht zur Überprüfung an.

imLoadF ist eine Funktion, die ein Bild aus einer Datei auf der Festplatte lädt. Wir wissen, daß diese Funktion ordnungsgemäß funktioniert (das werden Sie uns einfach glauben müssen), deshalb wollen wir sie mit dem Befehl next überspringen:

(gdb) next 
20         outimage = laplacian_float(inimage);
(gdb)

Statt dessen wollen wir uns diese verdächtige Funktion laplacian_float anschauen, deshalb benutzen wir den Befehl step :

(gdb) step 
laplacian_float (fim=0x0) at laplacian.c:21
21          i = 20.0;
(gdb)

Lassen Sie uns mit dem Befehl list herausfinden, wo wir sind:

(gdb) list 
16        FloatImage laplacian_float(FloatImage fim) {
17
18          FloatImage mask;
19          float i;
20
21          i = 20.0;
22          mask=(FloatImage)imNew(IMAGE_FLOAT,3,3);
23          imRef(mask,0,0) = imRef(mask,2,0) = imRef(mask,0,2) = 1.0;
24          imRef(mask,2,2) = 1.0; imRef(mask,1,0) = imRef(mask,0,1) = i/5;
25          imRef(mask,2,1) = imRef(mask,1,2) = i/5; imRef(mask,1,1) = -i;
(gdb) list 
26
27          return convolveFloatWithFloat(fim,mask);
28        }
(gdb)

Wie Sie sehen, können Sie mit wiederholten list -Befehlen weitere Codezeilen anzeigen. Wir wollen nicht mehr schrittweise durch diesen Code gehen, und sind auch nicht an der Funktion imNew in Zeile 22 interessiert; statt dessen wollen wir in Zeile 27 weitermachen. Dazu benutzen wir den Befehl until :

(gdb) until 27 
laplacian_float (fim=0x0) at laplacian.c:27
27          return convolveFloatWithFloat(fim,mask);
(gdb)

Bevor wir in die Funktion convolveFloatWithFloat eintauchen, wollen wir uns noch vergewissern, daß die beiden Parameter fim und mask gültig sind. Der Befehl print zeigt uns den Wert einer Variablen an:

(gdb) print mask 
$1 = (struct {...} *) 0xe838
(gdb) print fim 
$2 = (struct {...} *) 0x0
(gdb)

mask sieht ganz normal aus, aber fim, unsere Eingabe, ist gleich null! Offensichtlich wurde statt eines Bildes ein leerer Zeiger an laplacian_float übergeben. Wenn Sie aufmerksam mitgelesen haben, ist Ihnen das vielleicht schon aufgefallen, als wir weiter oben die Funktion laplacian_float erreichten.

Statt im Programm weiterzugehen (hier ist ja bereits klar, daß etwas schiefgelaufen ist), wollen wir diese Funktion bis zu ihrem Ende ablaufen lassen. Wir benutzen dazu den Befehl finish :

(gdb) finish 
Run till exit from #0  laplacian_float (fim=0x0) at laplacian.c:27
0x28c0 in main () at trymh.c:20
20       outimage = laplacian_float(inimage);
Value returned is $3 = (struct {...} *) 0x0
(gdb)

Hiermit sind wir zurück in main . Lassen Sie uns die Werte einiger Variablen untersuchen, um dem Problem auf die Spur zu kommen:

(gdb) list 
15       FloatImage outimage;
16       BinaryImage binimage;
17       int i,j;
18     
19       inimage = (FloatImage)imLoadF(IMAGE_FLOAT,stdin);
20       outimage = laplacian_float(inimage);
21     
22       binimage = marr_hildreth(outimage);
23       if  (binimage == NULL) {
24         fprintf(stderr,"trymh: binimage returned NULL\n");
(gdb) print inimage 
$6 = (struct {...} *) 0x0
(gdb)

Die Variable inimage, die das Eingabebild enthält, das imLoadF geladen hat, ist gleich null. Die Übergabe eines leeren Zeigers an die Routinen zur Bildbearbeitung hätte in diesem Fall sicherlich einen Core-Dump zur Folge. Wir haben aber imLoadF getestet und für gut befunden -- wo liegt also das Problem?

Wir stellen schließlich fest, daß unsere Library-Funktion imLoadF im Fehlerfall den Wert NULL zurückliefert -- z.B. bei einem falschen Format der Eingabe. Wir haben den Rückgabewert von imLoadF nicht abgefragt, bevor wir ihn an laplacian_float übergaben; deshalb gerät das Programm durcheinander, wenn inimage den Wert NULL annimmt. Wir beseitigen das Problem, indem wir einfach Code einfügen, der das Programm mit einer Fehlermeldung beendet, wenn imLoadF einen leeren Zeiger zurückliefert.

Verlassen Sie den gdb mit dem Befehl quit . Wenn das Programm noch nicht beendet war, wird gdb folgende Warnung ausgeben:

(gdb) quit 
The program is running.  Quit anyway (and kill it)? (y or n) y 
papaya$

Nachdem wir uns jetzt einen ersten Eindruck vom Debugger verschafft haben, wollen wir in den folgenden Abschnitten einige seiner Besonderheiten vorstellen.

Eine Core-Datei analysieren

Hassen Sie das auch, wenn ein Programm zuerst abstürzt und Sie dann noch einmal ärgert, indem es eine zehn Megabytes große Core-Datei in Ihrem Arbeitsverzeichnis zurückläßt, die wertvollen Speicherplatz belegt? Sie sollten diese Core-Datei nicht sofort löschen -- sie kann noch von Nutzen sein. Eine Core-Datei enthält einfach die Kopie des Arbeitsspeichers eines Prozesses zum Zeitpunkt des Programmabbruchs. Mit gdb und diesem Speicherauszug können Sie den Zustand Ihres Programms analysieren (den Wert von Variablen ebenso wie feste Daten) und so die Ursache für den Programmabsturz ermitteln.

Die Core-Datei wird vom Betriebsystem auf die Festplatte geschrieben, wenn bestimmte Probleme auftauchen. Der häufigste Grund für einen Programmabsturz mit anschließendem Core-Dump ist eine Speicherverletzung -- d.h., daß Sie versucht haben, lesend oder schreibend auf Speicher zuzugreifen, auf den Ihr Programm keinen Zugriff hat. Wenn Sie beispielsweise versuchen, Daten an einen leeren Dateizeiger zu schreiben, kann das einen »segmentation fault« (Speicherbereichfehler) hervorrufen; das heißt eigentlich nichts anderes als: »Das haben Sie versaut«.

Nicht alle Speicherfehler werden sofort einen Programmabsturz nach sich ziehen. Es kann z.B. vorkommen, daß Sie irgendwo einen Speicherbereich überschreiben und das Programm trotzdem weiterläuft, weil es den Unterschied zwischen echten Daten und Programmcode oder Datenmüll nicht kennt. Leichte Speicherbereichsverletzungen können bewirken, daß sich das Verhalten des Programms nicht mehr vorhersagen läßt. Der Schreiber hat einst miterlebt, wie ein Programm wahllos hin- und hersprang; ohne Benutzung des gdb schien es allerdings ganz normal zu funktionieren. Der einzige Hinweis auf einen Programmfehler war, daß das Programm Ergebnisse lieferte, die in etwa andeuteten, daß zwei plus zwei nicht vier ergibt. Es stellte sich schließlich heraus, daß der Fehler darin bestand, daß ein Byte zuviel in einen zugewiesenen Speicherblock geschrieben werden sollte. Dieser Ein-Byte-Fehler verursachte stundenlanges Kopfzerbrechen.

Sie können solche Speicherfehler vermeiden (selbst die besten Programmierer machen Fehler!), indem Sie das Paket Checker benutzen; eine Reihe von Routinen für die Speicherverwaltung, die die üblichen Funktionen malloc() und free() ersetzen. Wir werden Checker im Abschnitt » Checker benutzen « besprechen.

Wenn Ihr Programm tatsächlich einen Speicherfehler hervorruft, wird es abstürzen und einen Core-Dump verursachen. Unter Linux heißen die Core-Dateien core.name , wobei name der Name der ausführbaren Datei ist, die abgestürzt ist. Wenn beispielsweise das Programm trymh abstürzt und einen Speicherauszug hervorruft, heißt die Core-Datei core.trymh . Die Core-Datei steht im aktuellen Arbeitsverzeichnis des laufenden Prozesses; in der Regel ist das auch das Arbeitsverzeichnis der Shell, die das Programm aufgerufen hat, aber es gibt Programme, die ihr eigenes Arbeitsverzeichnis wechseln.

Manche Shells bieten die Möglichkeit, zu bestimmen, ob Core-Dateien geschrieben werden oder nicht. Unter bash z.B. ist die Voreinstellung, daß keine Core-Dateien geschrieben werden. Mit dem Befehl:

ulimit -c unlimited

z.B. in Ihrer Startdatei .bashrc ermöglichen Sie Core-Dumps. Sie können auch festlegen, wie groß eine Core-Datei höchstens werden darf (anders als unlimited), aber verkürzte Core-Dateien sind eventuell beim Debuggen von Anwendungen nicht zu gebrauchen.

Außerdem muß das Programm mit Debugging-Code kompiliert werden, damit die Core-Datei genutzt werden kann; wir haben das im vorherigen Abschnitt beschrieben. Die meisten ausführbaren Dateien auf Ihrem System enthalten wahrscheinlich keinen Debugging-Code, so daß die Core-Datei nur beschränkt brauchbar ist.

In unserem Beispiel für die Benutzung des gdb und einer Core-Datei setzen wir ein anderes mythisches Programm namens cross ein. Ebenso wie trymh aus dem vorherigen Abschnitt nimmt auch cross eine Bilddatei als Eingabe, führt einige Berechnungen durch und gibt eine andere Bilddatei aus. Allerdings bekommen wir beim Aufruf von cross diesen Speicherbereichfehler:

papaya$ cross < image30.pfm > image30.pbm 
Segmentation fault (core dumped)
papaya$

Wenn Sie den gdb aufrufen, um eine Core-Datei zu analysieren, müssen Sie nicht nur deren Namen angeben, sondern auch den Namen der zugehörigen ausführbaren Datei. Das liegt daran, daß in der Core-Datei selbst nicht alle zum Debuggen notwendigen Informationen enthalten sind:

papaya$ gdb cross core.cross 
GDB is free software and you are welcome to 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.
GDB 4.8, Copyright 1993 Free Software Foundation, Inc...
Core was generated by `cross'.
Program terminated with signal 11, Segmentation fault.
#0  0x2494 in crossings (image=0xc7c8) at cross.c:31
31              if ((image[i][j] >= 0) &&
(gdb)

gdb teilt uns mit, daß die Core-Datei mit dem Signal 11 beendet wurde. Ein Signal ist eine Art Nachricht, die vom Kernel, vom Benutzer oder dem Programm selbst an ein laufendes Programm geschickt wird. Signale werden meist benutzt, um ein Programm zu beenden (und möglicherweise einen Core-Dump zu erzeugen). Wenn Sie beispielsweise den Unterbrechnungscode eingeben, wird ein Signal an das laufende Programm geschickt, welches daraufhin wahrscheinlich abgebrochen wird.

In unserem Beispiel wurde Signal 11 vom Kernel an den laufenden Prozeß cross geschickt, als cross versuchte, einen Speicherbereich zu lesen oder zu beschreiben, auf den es keinen Zugriff hatte. Dieses Signal ließ cross abstürzen und den Core-Dump erzeugen. gdb teilt uns mit, daß der illegale Speicherzugriff in Zeile 31 der Quelldatei cross.c auftrat.

(gdb) list 
26          xmax = imGetWidth(image)-1;
27          ymax = imGetHeight(image)-1;
28       
29          for (j=1; j<xmax; j++) {
30            for (i=1; i<ymax; i++) {
31              if ((image[i][j] >= 0) &&
32                  (image[i-1][j-1] < 0) ||
33                  (image[i-1][j] < 0) ||
34                  (image[i-1][j+1] < 0) ||
35                  (image[i][j-1] < 0) ||
(gdb)

Wir können hier mehrere Dinge ablesen. Zunächst einmal steht dort eine Schleife mit den beiden Schleifenzählern i und j, wahrscheinlich um Berechnungen in der Eingabedatei auszuführen. Zeile 31 bezieht sich auf Daten in image[i][j], einem zweidimensionalen Array. Wenn ein Programm in dem Augenblick abstürzt, in dem es versucht, auf Daten in einem zweidimensionalen Array zuzugreifen, ist das meist ein Hinweis darauf, daß einer der Indizes seinen Gültigkeitbereich verlassen hat. Lassen Sie uns die Indizes anschauen:

 
(gdb) print i  
$1 = 1
(gdb) print j 
$2 = 1194
(gdb) print xmax 
$3 = 1551
(gdb) print ymax 
$4 = 1194
(gdb)

Hier zeigt sich das Problem. Das Programm versuchte, das Element image[1][1194] anzusprechen, aber dieses Array reicht nur bis image[1550][1193] (erinnern Sie sich, daß in C die Arrays von 0 bis max -1 indiziert werden). Mit anderen Worten: Wir haben versucht, die 1195. Zeile eines Bildes zu lesen, das nur 1194 Zeilen hat.

Wenn wir uns jetzt die beiden Zeilen 29 und 30 aus dem Programmcode ansehen, stoßen wir auf das Problem: Die Werte xmax und ymax sind vertauscht. Die Variable j sollte von 1 bis ymax reichen (weil sie die Anzahl der Zeilen indiziert), und i sollte von 1 bis xmax laufen. Eine Änderung in den beiden for-Schleifen in Zeile 29 und 30 behebt den Fehler.

Nehmen wir einmal an, daß Ihr Programm innerhalb einer Funktion abstürzt, die von verschiedenen Stellen im Programm aus aufgerufen wird; Sie möchten jetzt herausfinden, wo die Funktion aufgerufen wurde und was zum Absturz führte. Mit dem Befehl backtrace können Sie den Call Stack (Aufrufstapel) des Programms zum Absturzzeitpunkt anzeigen lassen.

Der Call Stack ist einfach eine Liste der Funktionen, die vor der aktuellen Funktion aufgerufen wurden. Wenn das Programm beispielsweise mit main startet, das die Funktion foo aufruft, die wiederum bamf aufruft, sieht der Call Stack folgendermaßen aus:

(gdb) backtrace 
#0  0x1384 in bamf () at goop.c:31
#1  0x4280 in foo () at goop.c:48
#2  0x218 in main () at goop.c:116
(gdb)

Jede Funktion schiebt bei ihrem Aufruf einige Daten auf den Stapel -- etwa Registerinhalte, Funktionsargumente, lokale Variablen usw. Jeder Funktion steht eine gewisse Menge an Speicher auf dem Stapel zur Verfügung. Der Speicherpatz auf dem Stapel für eine bestimmte Funktion heißt stack frame , und der Call Stack ist einfach eine chronologische Liste der Stack Frames.

Im folgenden Beispiel schauen wir uns die Core-Datei eines Animationsprogramms unter X an. Mit backtrace erhalten wir:

 
(gdb) backtrace 
#0  0x602b4982 in _end ()
#1  0xbffff934 in _end ()
#2  0x13c6 in stream_drawimage (wgt=0x38330000, sn=4) at stream_display.c:94
#3  0x1497 in stream_refresh_all () at stream_display.c:116
#4  0x49c in control_update_all () at control_init.c:73
#5  0x224 in play_timeout (Cannot access memory at address 0x602b7676.
(gdb) 

Hier sehen Sie die Liste der Stack Frames für diesen Prozeß. Frame 0 zeigt die Funktion, die zuletzt aufgerufen wurde, nämlich die »Funktion« _end . Wir können hier ablesen, daß play_timeout die Funktion control_update_all aufgerufen hat, welche stream_refresh_all aufrief usw. Aus irgendeinem Grund ist das Programm nach _end gesprungen und dort abgestürzt.

Allerdings ist _end keine Funktion -- es ist einfach eine Markierung (label), die das Ende eines Datensegments anzeigt. Wenn ein Programm zu einer Adresse wie _end verzweigt, die keine richtige Funktion ist, ist das ein Hinweis darauf, daß irgend etwas diesen Prozeß ins Nirgendwo geschickt und den Call Stack durcheinandergebracht hat. (In Hackerkreisen nennt man das auch einen »Sprung in den Hyperspace«.) Auch die Fehlermeldung »Cannot access memory at address 0x602b7676« zeigt an, daß etwas äußerst Ungewöhnliches passiert ist.

Wir können aber auch ablesen, daß stream_drawimage die letzte »richtige« Funktion war, die aufgerufen wurde, und wir können annehmen, daß hier die Ursache des Problems liegt. Um den Zustand von stream_drawimage zu untersuchen, müssen wir mit dem Befehl frame seinen Stack Frame (Nummer 2) anzeigen lassen:

(gdb) frame 2 
#2  0x13c6 in stream_drawimage (wgt=0x38330000, sn=4) at stream_display.c:94
94        XCopyArea(mydisplay,streams[sn].frames[currentframe],XtWindow(wgt),
(gdb) list 
91
92        printf("CopyArea frame %d, sn %d, wid %d\n",currentframe,sn,wgt);
93
94        XCopyArea(mydisplay,streams[sn].frames[currentframe],XtWindow(wgt),
95                 picGC,0,0,streams[sn].width,streams[sn].height,0,0);
(gdb)

Da wir weiter nichts über das vorliegende Programm wissen, können wir hier nichts Falsches entdecken -- es sei denn, die Variable sn (die als Index für das Array streams benutzt wird) ist außerhalb ihres Gültigkeitbereichs. In der Ausgabe des Befehls frame können wir ablesen, daß stream_drawimage mit dem Wert 4 für den Parameter sn aufgerufen wurde. (Funktionsparameter werden in der Ausgabe von backtrace angezeigt und immer dann, wenn wir zu einem anderen Frame wechseln.)

Lassen Sie uns noch einen Frame zurückgehen (zu stream_refresh_all ) und nachschauen, wie stream_display aufgerufen wurde. Wir benutzen dazu den Befehl up , der uns zum Stack Frame oberhalb des aktuellen bringt:

(gdb) up 
#3  0x1497 in stream_refresh_all () at stream_display.c:116
116         stream_drawimage(streams[i].drawbox,i);
(gdb) list 
113     void stream_refresh_all(void) {
114       int i;
115       for (i=0; i<=numstreams; i++) {
116         stream_drawimage(streams[i].drawbox,i);
117
(gdb) print i 
$2 = 4
(gdb) print numstreams 
$3 = 4
(gdb)

Wir sehen hier, daß die Indexvariable i von 0 bis numstreams läuft, und daß i als zweiter Parameter zu stream_drawimage tatsächlich den Wert 4 hat. Aber auch numstreams hat den Wert 4. Was ist passiert?

Die for-Schleife in Zeile 115 sieht merkwürdig aus -- hier sollte stehen:

for (i=0; i<numstreams; i++) {

Der Fehler liegt darin, daß wir den Vergleichsoperator <= benutzt haben. Das Array streams wird von 0 bis numstreams-1 indiziert, nicht von 0 bis numstreams. Dieser kleine »Eins-daneben«-Fehler ließ das Programm in die Irre laufen.

Wie Sie sehen, ist es mit gdb und einem Core-Dump möglich, durch das Abbild eines abgestürzten Programms zu wandern, um Fehler zu finden. Sicherlich werden Sie diese nervigen Core-Dateien nie wieder löschen, oder?

Ein laufendes Programm debuggen

gdb ist auch in der Lage, ein bereits laufendes Programm zu debuggen, indem Sie es unterbrechen, analysieren, und dann den Prozeß wie vorgesehen weiterlaufen lassen. Der Vorgang ähnelt sehr dem Start eines Programms aus gdb heraus, und es gibt nur wenige neue Befehle.

Mit dem Befehl attach hängen Sie den gdb an einen laufenden Prozeß an. Damit Sie attach benutzen können, müssen Sie auch Zugriff auf die entsprechende ausführbare Datei haben.

Ein Beispiel: Wenn das Programm pgmseq mit der Prozeß-ID 254 bereits läuft, können Sie den gdb mit:

papaya$ gdb pgmseq

einklinken; sobald gdb aktiviert ist, geben Sie ein:

(gdb) attach 254 
Attaching program `/home/loomer/mdw/pgmseq/pgmseq', pid 254
__select (nd=4, in=0xbffff96c, out=0xbffff94c, ex=0xbffff92c, tv=0x0)
   at __select.c:22
__select.c:22: No such file or directory.
(gdb)

(Die Fehlermeldung »No such file or directory« erscheint, weil gdb den Quellcode zu __select nicht finden kann. Das passiert bei Systemaufrufen und Library-Funktionen recht häufig und ist kein Grund zur Beunruhigung.) Sie können gdb auch mit diesem Befehl starten:

papaya$ gdb pgmseq 254

Sobald der gdb sich an den laufenden Prozeß angehängt hat, wird er das Programm unterbrechen und Ihnen die Kontrolle überlassen -- geben Sie jetzt gdb -Befehle ein. Sie können auch Breakpoints und Watchpoints setzen (mit den Befehlen break und watch ), und Sie können mit continue das Programm bis zum nächsten Breakpoint weiterlaufen lassen.

Mit dem Befehl detach trennen Sie gdb vom laufenden Prozeß. Bei Bedarf können Sie sich mit attach an einen anderen Prozeß anhängen. Wenn Sie einen Fehler finden, können Sie mit detach den akuellen Prozeß wieder abhängen, den Quelltext korrigieren, neu kompilieren und mit dem Befehl file die neue ausführbare Datei in den gdb laden. Sie können anschließend die neue Version des Programms starten und mittels attach debuggen. Das alles, ohne den gdb zu verlassen!

gdb bietet Ihnen sogar die Möglichkeit, drei Programme gleichzeitig zu debuggen: eines, das direkt unter gdb läuft; eines, bei dem Sie die Core-Datei analysieren; und eines, das als selbständiger Prozeß läuft. Mit dem Befehl target wählen Sie aus, welches Programm Sie debuggen möchten.

Daten ändern und untersuchen

Wenn Sie die Werte von Programmvariablen betrachten wollen, können Sie einen der Befehle print , x oder ptype benutzen. Am häufigsten wird zur Inspektion von Daten der Befehl print benutzt; als Argument bekommt er einen Ausdruck aus dem Quellcode mit (meist C oder C++), und print gibt dann den Wert des Ausdrucks zurück. Ein Beispiel:

(gdb) print mydisplay 
$10 = (struct _XDisplay *) 0x9c800
(gdb)

Damit lassen Sie den Wert der Variablen mydisplay und einen Hinweis auf den Typ derselben anzeigen. Weil diese Variable ein Zeiger ist, können Sie den Inhalt untersuchen, indem Sie den Zeiger dereferenzieren, wie Sie das auch in C tun würden:

(gdb) print *mydisplay 
$11 = {ext_data = 0x0, free_funcs = 0x99c20, fd = 5, lock = 0,
  proto_major_version = 11, proto_minor_version = 0,
  vendor = 0x9dff0 "XFree86", resource_base = 41943040,
  ...
  error_vec = 0x0, cms = {defaultCCCs = 0xa3d80 "", clientCmaps = 0x991a0 "'",
    perVisualIntensityMaps = 0x0}, conn_checker = 0, im_filters = 0x0}
(gdb)

mydisplay ist eine längere Struktur, die von X-Programmen benutzt wird -- wir geben die Ausgabe verkürzt wieder, damit Sie nicht die Lust am Lesen verlieren.

print ist in der Lage, den Wert praktisch jeden Ausdrucks anzuzeigen -- einschließlich der Funktionsaufrufe von C (die Funktionen werden »im Vorübergehen« innerhalb des laufenden Programms ausgeführt):

(gdb) print getpid() 
$11 = 138
(gdb)

Natürlich lassen sich auf diese Art nicht alle Funktionen aufrufen, sondern nur solche, die mit dem laufenden Programm gebunden wurden. Wenn Sie versuchen, eine Funktion aufzurufen, die nicht mit diesem Programm gebunden wurde, wird gdb melden, daß ein solches Symbol in diesem Kontext nicht existiert.

Sie können print auch kompliziertere Ausdrücke als Argument mitgeben und Variablen einen Wert zuweisen. Mit:

(gdb) print mydisplay->vendor = ``Linux'' 
$19 = 0x9de70 "Linux"
(gdb)

weisen Sie der Variablen vendor aus der Struktur mydisplay den Wert "Linux" statt "XFree86" zu (eine nutzlose Änderung, aber doch interessant). Auf diese Weise können Sie in einem laufenden Programm interaktiv Daten ändern, um Fehler zu beheben oder neue Konstellationen zu testen.

Beachten Sie auch, daß nach jedem print -Befehl der angezeigte Wert einem der aktiven gdb -Register (convenience register) zugewiesen wird. Das sind interne Variablen in gdb , mit denen es sich bequem arbeiten läßt. Wenn Sie z.B. den Wert von mydisplay noch einmal anzeigen möchten, brauchen Sie nur die Variable $10 anzuzeigen:

(gdb) print $10 
$21 = (struct _XDisplay *) 0x9c800
(gdb)

Sie können mit dem Befehl print auch Ausdrücke wie z.B. explizite Typumwandlungen (typecasts) benutzen -- die Möglichkeiten sind fast unbegrenzt.

Mit dem Befehl ptype erhalten Sie detaillierte (und manchmal langatmige) Informationen über den Typ einer Variablen oder die Definition von struct- und typedef-Anweisungen. Geben Sie:

(gdb) ptype mydisplay 
type = struct _XDisplay {
    struct _XExtData *ext_data;
    struct _XFreeFuncs *free_funcs;
    int fd;
    int lock;
    int proto_major_version;
    ...
    struct _XIMFilter *im_filters;
} *
(gdb)

ein, um die Definition von struct _XDisplay anzuzeigen, die von der Variablen mydisplay benutzt wird. Wenn Sie den Arbeitspeicher auf einer ganz niedrigen Ebene und losgelöst von den kleinlichen Beschränkungen definierter Typen untersuchen möchten, können Sie dazu den Befehl x benutzen. x akzeptiert eine Speicheradresse als Argument. Wenn Sie x eine Variable mitgeben, wird es den Wert dieser Variablen als Adresse benutzen.

x akzeptiert auch einen Zähler und eine Typdefinition als optionales Argument. Der Zähler gibt an, wieviele Objekte des definierten Typs angezeigt werden sollen. Ein Beispiel: x/100x 0x4200 zeigt 100 Bytes an Daten in hexadezimaler Darstellung ab der Adresse 0x4200 an. Mit help x erhalten Sie eine Beschreibung der möglichen Ausgabeformate.

Um den Wert von mydisplay->vendor anzuzeigen, können wir folgende Befehle eingeben:

(gdb) x mydisplay->vendor 
0x9de70 <_end+35376>:   76 'L'
(gdb) x/6c mydisplay->vendor 
0x9de70 <_end+35376>:   76 'L'  105 'i' 110 'n' 117 'u' 120 'x' 0 '\000'
(gdb) x/s mydisplay->vendor 
0x9de70 <_end+35376>:    "Linux"
(gdb)

Das erste Feld in jeder Zeile gibt die absolute Adresse der Daten an. Das zweite Feld stellt die Adresse in Form eines Symbols (in diesem Fall _end) und eines Offsets in Bytes dar. Die restlichen Felder enthalten die eigentlichen Speicherdaten an dieser Adresse in dezimaler Schreibweise und als ASCII-Code. Wir haben bereits erwähnt, daß x auch andere Ausgabeformate beherrscht.

Informationen anzeigen

Der Befehl info zeigt Informationen über den Status des analysierten Programms an. info kennt eine ganze Reihe von Unterbefehlen; mit help info können Sie diese anzeigen lassen. Mit info program beispielsweise erhalten Sie Informationen zum Ablaufstatus des Programms:

(gdb) info program 
Using the running image of child process 138.
Program stopped at 0x9e.
It stopped at breakpoint 1.
(gdb)

Ein weiterer nützlicher Befehl ist info locals , mit dem Sie die Namen und Werte aller lokalen Variablen in der aktuellen Funktion anzeigen lassen:

(gdb) info locals 
inimage = (struct {...} *) 0x2000
outimage = (struct {...} *) 0x8000
(gdb)

Auf diese Weise erhalten Sie nur eine äußerst knappe Beschreibung der Variablen; die Befehle print und x liefern genauere Informationen.

In ähnlicher Weise erhalten Sie mit info variables eine Liste aller bekannten Variablen im Programm. Viele der angezeigten Variablen stammen nicht aus dem eigentlichen Programm -- es werden z.B. auch die Namen der Variablen im Library-Code angezeigt. Die Werte dieser Variablen werden nicht angezeigt, weil diese Liste mehr oder weniger direkt aus der Symboltabelle der ausführbaren Datei gewonnen wird. gdb hat nur Zugriff auf die lokalen Variablen des aktuellen Stack Frames sowie globale (statische) Variablen.

info address zeigt Informationen zum genauen Speicherort einer bestimmten Variablen an:

(gdb) info address inimage 
Symbol "inimage" is a local variable at frame offset -20.
(gdb)

Mit »frame offset« meint gdb , daß inimage 20 Bytes vom oberen Ende des Stack Frames entfernt gespeichert ist.

Mit info frame erhalten Sie Informationen über den aktuellen Stack Frame:

(gdb) info frame 
Stack level 0, frame at 0xbffffaa8:
 eip = 0x9e in main (main.c:44); saved eip 0x34
 source language c.
 Arglist at 0xbffffaa8, args: argc=1, argv=0xbffffabc
 Locals at 0xbffffaa8, Previous frame's sp is 0x0
 Saved registers:
  ebx at 0xbffffaa0, ebp at 0xbffffaa8, esi at 0xbffffaa4, eip at 0xbffffaac
(gdb)

Solche Informationen sind nützlich, wenn Sie mit den Befehlen disass , nexti und stepi auf der Assemblerebene debuggen möchten (lesen Sie auch den Abschnitt » Debuggen auf Assemblerebene « ).

Andere Fähigkeiten

Wir haben kaum die Oberfläche dessen angekratzt, was gdb zu leisten vermag. Es ist ein erstaunliches Programm mit vielen Fähigkeiten -- wir haben Ihnen nur die am häufigsten gebrauchten Befehle vorgestellt. Im folgenden Abschnitt werfen wir einen Blick auf einige weitere Eigenschaften des gdb , und dann sind Sie auf sich selbst gestellt.

Wenn Sie mehr über gdb lernen möchten, sollten Sie die entsprechende Manual-Page und das Handbuch der Free Software Foundation lesen. Das Handbuch gibt es auch als Info-Datei. (Sie können die Info-Datei mit Emacs oder mit dem info -Reader lesen; im Abschnitt » Das Lernprogramm und die Online-Hilfe « in Kapitel 5 finden Sie Details hierzu.)

Breakpoints und Watchpoints

Wie versprochen wollen wir noch einmal auf die Benutzung von Breakpoints und Watchpoints eingehen. Breakpoints werden mit dem Befehl break gesetzt, Watchpoints mit watch . Der einzige Unterschied zwischen den beiden besteht darin, daß die Breakpoints an einer bestimmten Stelle im Programm gesetzt werden müssen -- z.B. in einer bestimmten Programmzeile -- während die Watchpoints dann aktiviert werden, wenn ein bestimmter Ausdruck wahr wird; unabhängig davon, wo im Programm das passiert. Obwohl die Watchpoints ein sehr mächtiges Instrument sind, können sie auch äußerst ineffizient sein -- mit jeder Änderung des Programmstatus müssen alle Watchpoints neu berechnet werden.

Wenn ein Breakpoint oder Watchpoint ausgelöst wird, hält gdb das Programm an und gibt die Kontrolle an Sie ab. Breakpoints und Watchpoints geben Ihnen die Möglichkeit, das Programm laufen zu lassen (mit den Befehlen run und continue ) und es dabei nur an bestimmten Stellen anzuhalten. Das erpart Ihnen die Mühe, mit vielen next - und step -Befehlen »zu Fuß« durch das Programm zu gehen.

Es gibt viele Methoden, einen Breakpoint zu setzen. Sie können eine Zeilennummer angeben (wie in break 20 ) oder eine bestimmte Funktion ( break stream_unload ). Sie haben auch die Möglichkeit, eine Zeilennummer in einer anderen Quelldatei anzuführen ( break foo.c:38 ). Mit help break erhalten Sie einen Überblick über die komplette Syntax.

Breakpoints können auch bedingt gesetzt werden -- d.h., daß sie nur dann ausgelöst werden, wenn ein bestimmter Ausdruck wahr ist. Ein Beispiel:

break 184 if (status == 0)

setzt einen bedingten Breakpoint in Zeile 184 der aktuellen Quelldatei, der nur dann ausgelöst wird, wenn die Variable status gleich null ist. status muß entweder eine globale Variable oder eine lokale Variable aus dem aktuellen Stack Frame sein. Der Ausdruck kann ein beliebiger gültiger Ausdruck in der Programmiersprache sein, den gdb auswerten kann; das entspricht den Ausdrücken, die Sie mit dem Befehl print benutzen können. Bei bedingten Breakpoints können Sie mit dem Befehl condition die Bedingung ändern.

Mit dem Befehl info break erhalten Sie eine Liste aller Breakpoints und Watchpoints samt ihrem Status. Das gibt Ihnen die Möglichkeit, mit den Befehlen clear , delete und disable Breakpoints zu löschen oder zu deaktivieren. Ein deaktivierter Breakpoint ist nur solange inaktiv, bis Sie ihn wieder aktivieren (mit dem Befehl enable ) -- ein gelöschter Breakpoint wird dagegen endgültig aus der Liste der Breakpoints entfernt. Sie können auch festlegen, daß ein Breakpoint nur einmal aktiviert werden soll -- d.h., daß er nach dem ersten Auslösen wieder deaktiviert oder auch gelöscht wird.

Setzen Sie einen Watchpoint mit dem Befehl watch , etwa so:

watch (numticks < 1024 && incoming != clear)

Die Bedingungen für Watchpoints sind dieselben wie die für Breakpoints.

Debuggen auf Assemblerebene

gdb ist auch in der Lage, auf der Assemblerebene zu debuggen, so daß Sie das Innenleben Ihres Programms genauestens analysieren können. Wenn Sie allerdings verstehen möchten, was Sie dort sehen, brauchen Sie sowohl Kenntnisse der Prozessorarchitektur und Assemblersprache als auch ein gewisses Verständnis davon, wie die CPU den Prozessen ihren Adreßraum zuordnet. Es kann nicht schaden, wenn Sie z.B. die Regeln verstehen, nach denen Stack Frames aufgebaut und Funktionen aufgerufen, Parameter und Rückgabewerte übergeben werden usw. Jedes beliebige Buch über die Programmierung des Protected Mode der 80386/80486er CPUs klärt Sie darüber auf. Aber seien Sie vorsichtig: Die Programmierung des Protected Mode dieser CPUs ist völlig anders als die des Real Mode (der in der MS-DOS-Welt benutzt wird). Informieren Sie sich auf jeden Fall über die Programmierung des echten Protected Mode für den 386er, oder Sie riskieren die endgültige Verwirrung.

Die wichtigsten gdb -Befehle beim Debuggen auf Assemblerebene sind nexti , stepi und disass . nexti entspricht dem Befehl next , aber es springt zum nächsten Befehl statt zur nächsten Zeile im Quellcode; in ähnlicher Weise ist stepi das Gegenstück zu step .

Mit dem Befehl disass können Sie einen bestimmten Programmausschnitt disassemblieren. Geben Sie die Adresse des Bereichs direkt oder den Namen der Funktion an. Wenn Sie beispielsweise die Funktion play_timeout disassemblieren möchten, geben Sie ein:

(gdb) disass play_timeout 
Dump of assembler code for function play_timeout:
to 0x2ac:
0x21c <play_timeout>:           pushl  %ebp
0x21d <play_timeout+1>:         movl   %esp,%ebp
0x21f <play_timeout+3>:         call   0x494 <control_update_all>
0x224 <play_timeout+8>:         movl   0x952f4,%eax
0x229 <play_timeout+13>:        decl   %eax
0x22a <play_timeout+14>:        cmpl   %eax,0x9530c
0x230 <play_timeout+20>:        jne    0x24c <play_timeout+48>
0x232 <play_timeout+22>:        jmp    0x29c <play_timeout+128>
0x234 <play_timeout+24>:        nop   
0x235 <play_timeout+25>:        nop   
...
0x2a8 <play_timeout+140>:       addb   %al,(%eax)
0x2aa <play_timeout+142>:       addb   %al,(%eax)
(gdb)

Dies ist dasselbe wie der Befehl disass 0x21c (wobei 0x21c die Anfangsadresse der Funktion play_timeout ist).

Sie können dem Befehl disass ein optionales zweites Argument mitgeben, und die Disassemblierung wird dann bis zu dieser zweiten Adresse durchgeführt. Mit disass 0x21c 0x232 werden nur die ersten sieben Zeilen dieses Assemblerlistings angezeigt (der Befehl an der Adresse 0x232 erscheint nicht auf dem Bildschirm).

Wenn Sie die Befehle nexti und stepi häufig benutzen, ist es vielleicht einfacher, statt dessen:

display/i $pc

einzugeben. Damit bewirken Sie, daß nach jedem nexti - oder stepi -Befehl die aktuelle Adresse angezeigt wird. Mit display bestimmen Sie, welche Variablen beobachtet oder welche Befehle nach jedem Schrittbefehl ausgeführt werden sollen. $pc ist ein gdb -internes Register, das dem Programmzähler (program counter) der CPU entspricht, der immer die Adresse des aktuellen Befehls enthält.

Emacs und gdb

Emacs (das wir im Abschnitt » Der Editor Emacs « in Kapitel 5 beschreiben) kennt einen Debugging-Modus, der den Aufruf von gdb -- oder eines anderen Debuggers -- innerhalb der Debugging-Umgebung von Emacs gestattet. Diese sogenannte »Grand Unified Debugger«-Library ist ausgesprochen umfangreich und erlaubt das vollständige Debuggen und Editieren, ohne daß Sie Emacs verlassen müssen.

Starten Sie gdb unter Emacs, indem Sie den Emacs-Befehl M-x gdb mit dem Namen der zu debuggenden ausführbaren Datei als Argument eingeben. Emacs wird einen Puffer für die Interaktion mit dem gdb öffnen, der so ähnlich wie gdb alleine funktioniert. Sie können anschließend mit core-file eine Core-Datei laden oder gdb mit attach an einen laufenden Prozeß anhängen.

Jedesmal wenn Sie einen neuen Frame erreichen (z.B. wenn ein Breakpoint ausgelöst wird), öffnet gdb ein eigenes Fenster mit dem Quellcode, der zum aktuellen Frame gehört. In diesem Puffer können Sie den Quellcode editieren wie in einer normalen Emacs-Sitzung, aber die aktuelle Zeile des Programmcodes wird durch einen Pfeil hervorgehoben (aus den Zeichen »=>«). Auf diese Weise können Sie in einem Fenster den Quellcode verfolgen und im anderen gdb -Befehle ausführen.

Innerhalb des Debugging-Fensters sind verschiedene spezielle Tastenkombinationen wirksam. Diese sind allerdings ziemlich lang, so daß sie nicht unbedingt bequemer sind als der direkte Aufruf von gdb -Befehlen. Zu den häufig benutzten Befehlen gehören:

C-x C-a C-s
Entspricht dem gdb -Befehl step und erneuert das Quellcodefenster dementsprechend.
C-x C-a C-i
Entspricht dem Befehl stepi.
C-x C-a C-n
Entspricht dem Befehl next.
C-x C-a C-r
Entspricht dem Befehl continue.
C-x C-a <
Entspricht dem Befehl up.
C-x C-a >
Entspricht dem Befehl down.

Wenn Sie Ihre Befehle auf die traditionelle Art eingeben, können Sie mit M-p zu bereits ausgeführten Befehlen zurückgehen und mit M-n vorwärts durch die Befehle blättern. Sie können auch die die Emacs-Befehle zum Suchen, zur Cursorbewegung usw. benutzen, um sich im Puffer umherzubewegen. Insgesamt gesehen ist die Benutzung des gdb innerhalb von Emacs viel bequemer als von der Shell aus.

Außerdem können Sie den Quellcode in gdb s Quelltextpuffer editieren; der Hinweispfeil ist im abgespeicherten Quellcode nicht mehr enthalten.

Emacs ist extrem anpassungsfähig, und Sie könnten selbst eine ganze Reihe von Erweiterungen zu seiner gdb -Schnittstelle schreiben. Sie könnten unter Emacs bestimmte Tasten mit häufig benutzten gdb -Befehlen belegen oder das Verhalten des Quelltextfensters beeinflussen. (Es ließen sich beispielsweise alle Breakpoints irgendwie hervorheben oder Tastem zum deaktivieren und löschen von Breakpoints definieren.)


Fußnoten

(3)
Die Beispielprogramme in diesem Abschnitt werden Sie nirgendwo finden; ich habe sie lediglich zu Demonstrationszwecken zusammengestrickt.

Inhaltsverzeichnis Vorherige Abschnitt Nächste Abschnitt