![]() |
|
Linux - Wegweiser zur Installation & Konfiguration, 3. AuflageOnline-VersionBitte denken Sie daran: Sie dürfen zwar die Online-Version ausdrucken, aber diesen Druck nicht fotokopieren oder verkaufen. Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen. Wünschen Sie mehr Informationen zu der gedruckten Version des Buches Linux - Wegweiser zur Installation & Konfiguration oder wollen Sie es bestellen, dann klicken Sie bitte hier.
|
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: gdb 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 ten; ohne die Hilfe eines Debuggers wie gdb ist es fast unmöglich, 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.
[59] gdb |
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.
Unser erstes Beispiel ist ein Programm namens trymh, das Kanten in einem Schwarzweiß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:
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). Fußnoten 1
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« in Kapitel 12, Kompatibilität mit Windows und Samba). trymh sollte also mit dem Schalter -g zum gcc kompiliert werden. |
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:
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.16 (i486-unknown-linux --target i486-linux),
Copyright 1996 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 wird oder abstürzt.
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):
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 später 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 - da der Befehl zur Ausführung an /bin/sh weitergereicht wird, sind hier auch Shell-Wildcards, Ein-/Ausgabeumleitung usw. möglich:
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:
Statt dessen wollen wir uns die verdächtige Funktion laplacian_float ansehen, deshalb benutzen wir den Befehl step:
Lassen Sie uns mit dem Befehl list herausfinden, wo wir sind:
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:
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:
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:
Hiermit sind wir zurück in main. Lassen Sie uns die Werte einiger Variablen untersuchen, um dem Problem auf die Spur zu kommen:
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 - zum Beispiel 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:
Nachdem wir uns jetzt einen ersten Eindruck vom Debugger verschafft haben, wollen wir in den folgenden Abschnitten einige seiner Besonderheiten vorstellen.
Hassen Sie das auch, wenn ein Programm zuerst abstürzt und Sie dann noch einmal ärgert, indem es eine zehn Megabyte 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 Betriebssystem 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« (Speicherbereichsfehler) hervorrufen; das heißt eigentlich nichts anderes als: »Das haben Sie versaut«. Andere Fehler, die zu Core-Dateien führen, sind die sogenannten »Bus Errors« (bei denen nicht etwa Ihr ÖPNV-Unternehmen versagt hat) und »Floating-Point Exceptions«. »Segmentation Faults« sind gängige Fehler, die auftreten, wenn Sie versuchen, lesend oder schreibend auf eine Speicherstelle zuzugreifen, die nicht zum Adreßraum Ihres Prozesses gehört. Dazu gehört auch die Adresse 0, ein häufig auftretender Fall bei nicht initialisierten Zeigern. »Bus Errors« stammen aus fehlerhaft ausgerichteten Daten und sind daher selten auf Intel-Architekturen, bei denen es keine so strengen Anforderungen an die Ausrichtung der Daten gibt wie auf anderen Architekturen wie etwa SPARC. »Floating-Point Exceptions« deuten auf ein schwerwiegendes Problem bei einer Fließkommaberechnung - wie etwa eine Wertebereichsüber- oder -unterschreitung - hin, aber der gängigste Fall ist das Teilen durch null.
Nicht alle Speicherfehler werden sofort einen Programmabsturz nach sich ziehen. Es kann zum Beispiel 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. Einer der Autoren 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, das eine Reihe von Routinen für die Speicherverwaltung enthält, die die üblichen Funktionen malloc() und free() ersetzen. Wir werden Chekker 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 sinnvollerweise core. 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 zum Beispiel ist die Voreinstellung, daß keine Core-Dateien geschrieben werden. Mit dem Befehl
(zum Beispiel 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 mit 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 Speicherbereichsfehler:
Wenn Sie 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:
gdb cross core
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.16, Copyright 1996 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 Unterbrechungscode eingeben, wird ein Signal an das laufende Programm geschickt, das 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.
Wir können hier mehrere Dinge erkennen. 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ültigkeitsbereich verlassen hat. Lassen Sie uns die Indizes betrachten:
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-te 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
reichen. 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. Wenn Sie wie wir sind und damit zu faul sind, um immer backtrace
einzugeben, dann wird es Sie freuen, daß man auch die Abkürzung bt verwenden kann.
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:
Jede Funktion schiebt bei ihrem Aufruf einige Daten auf den Stack - etwa Registerinhalte, Funktionsargumente, lokale Variablen usw. Jeder Funktion steht eine gewisse Menge an Speicher auf dem Stack zur Verfügung. Der Speicherpatz auf dem Stack für eine bestimmte Funktion heißt Stack Frame, und der Call Stack ist einfach eine chronologische Liste der Stack Frames.
Im folgenden Beispiel sehen wir uns die Core-Datei eines Animationsprogramms unter X an. Mit backtrace erhalten wir:
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 erkennen, daß play_timeout die Funktion control_update_all aufgerufen hat, die 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 erkennen, 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:
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ültigkeitsbereichs. Anhand der Ausgabe des Befehls frame können wir erkennen, 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 nachsehen, wie stream_display aufgerufen wurde. Wir benutzen dazu den Befehl up, der uns zum Stack Frame oberhalb des aktuellen bringt:
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:
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?
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 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 gdb mit
einklinken. Sobald gdb aktiviert ist, geben Sie ein:
(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:
Sobald 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 passiert 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.
Wenn Sie die Werte von Programmvariablen betrachten wollen, können Sie einen der Befehle print, x oder ptype benutzen. Am häufigsten wird der Befehl print zur Inspektion von Daten 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:
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:
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):
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
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 zum Beispiel den Wert von mydisplay
noch einmal anzeigen möchten, brauchen Sie nur die Variable $10
anzuzeigen:
Sie können mit dem Befehl print auch Ausdrücke wie etwa 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
ein, um die Definition von struct _XDisplay
anzuzeigen, das von der Variable mydisplay
benutzt wird. Wenn Sie den Arbeitsspeicher 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 Variable als Adresse benutzen.
x akzeptiert auch einen Zähler und eine Typdefinition als optionales Argument. Der Zähler gibt an, wie viele 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:
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.
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:
Ein weiterer nützlicher Befehl ist info locals, mit dem Sie die Namen und Werte aller lokalen Variablen in der aktuellen Funktion anzeigen lassen:
Auf diese Weise erhalten Sie nur eine äußerst knappe Beschreibung der Variablen; die Befehle print und x liefern genauere Informationen.
Auf ähnliche 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 zum Beispiel 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 Variable an:
Mit frame offset
drückt gdb aus, daß inimage 20 Bytes vom oberen Ende des Stack Frames entfernt gespeichert ist.
Mit info frame erhalten Sie Informationen über den aktuellen Stack Frame:
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 Assembler-Ebene debuggen möchten (lesen Sie auch den Abschnitt »Debuggen auf Assembler-Ebene«).
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 Manpage 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 9, Editoren, Textwerkzeuge, Grafiken und Drucken, finden Sie Details hierzu.) |
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 - zum Beispiel 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 erspart 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:
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, die 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 so lange 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:
Die Bedingungen für Watchpoints sind dieselben wie die für Breakpoints.
gdb ist auch in der Lage, auf der Assembler-Ebene 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 Assembler-Sprache als auch ein gewisses Verständnis davon, wie die CPU den Prozessen ihren Adreßraum zuordnet. Es kann nicht schaden, wenn Sie zum Beispiel 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 Assembler-Ebene sind nexti, stepi und disass. nexti entspricht dem Befehl next, aber es springt zum nächsten Befehl statt zur nächsten Zeile im Quellcode; ähnlich ist stepi das Gegenstück zu step.
Mit dem Befehl disass können Sie einen bestimmten Programmausschnitt disassemblieren. Geben Sie direkt die Adresse des Bereichs oder den Namen der Funktion an. Wenn Sie beispielsweise die Funktion play_timeout disassemblieren möchten, geben Sie ein:
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 Assembler-Listings 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
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 Schritt-Befehl 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 (das wir im Abschnitt »Der Editor Emacs« in Kapitel 9 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 Ihrer Programme, 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 (zum Beispiel, 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 (=>). 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
C-x C-a C-i
C-x C-a C-n
C-x C-a C-r
C-x C-a <
C-x C-a >
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 Emacs-Befehle zum Suchen, zur Cursor-Bewegung 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 im Quelltextpuffer von gdb 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 Tasten zum Deaktivieren und Löschen von Breakpoints definieren.)
Fußnoten 1![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Weitere Informationen zum Linux - Wegweiser zur Installation & Konfiguration
Weitere Online-Bücher & Probekapitel finden Sie in unserem Online Book Center
© 2000, O'Reilly Verlag