Debugger - GDB
Montag, 22. November 2010 | Autor: Nico
Debugger sind Programme, die einem bei der Fehlersuche helfen sollen. Mit einem Debugger kann man das Programm quasi bei der Arbeit beobachten: Welche Codezeile wird als nächstes ausgeführt? Was enthält diese und jene Variable momentan? Wie sieht der Call-Stack/Backtrace momentan aus? An welcher Stelle stürzt das Programm genau ab? und vieles anderes mehr.
Für C++ (und jede Menge anderer Sprachen) gibt es den GNU-Debugger, kurz GDB.
GDB
Eine gute Website zum GDB findet sich hier: RMS’s gdb Tutorial
Es folgt ein kleines Beispiel. Eine Liste soll mit 10 Zufallswerten zwischen 0 und 1 gefüllt werden. Unser Programm bekommt beim Aufruf als Parameter die Länge mitgegeben.
Der Code - zufall.cpp - sieht so aus:
#include <iostream> #include <vector> #include <cstdlib> using namespace std; typedef unsigned int uint; int main(int argc, char * argv[]) { uint length = atoi( argv[1]); vector <double> liste; for(uint i = 0; i < length; ++i) liste.push_back( double(rand())/RAND_MAX ); for(uint i; i<liste.size(); ++i) cout << liste.at(i); return 0; }
Wir kompilieren ihn erfolgreich, zum Beispiel mit:
g++ -Wall zufall.cpp -o zufall
und führen ihn aus um, sagen wir mal 42 Zufallszahlen erzeugen:
D:\Code>zufall 42 D:\Code>
Wie wir sehen, läuft unser Programm zwar durch, gibt aber wider Erwarten nichts aus.
Um diesen Bug zu finden, wollen wir den GDB als Debugger benutzen. Das geht so:
1. Mit Debugging-Informationen kompilieren
Normalerweise lässt sich am fertig kompilierten Programm nicht mehr erkennen, aus welcher Datei oder Zeile im Quellcode ein Maschinenbefehl ursprünglich hervorgegangen ist oder mit welchem Variablennamen ein bestimmter Wert im Speicher mal benannt war. Um diese Information zu bewahren, muss beim Kompilieren (und ggf. Linken) der Parameter -g
benutzt werden. Sah unser Befehl zum Kompilieren bisher so aus:
g++ -Wall zufall.cpp -o zufall
wird daraus nun
g++ -Wall -g zufall.cpp -o zufall
Es kommt einfach ein -g
dazu. Mehr isses nicht.
GDB starten
Jetzt können wir den GDB benutzen. Dazu starten wir gdb
mit dem Namen unseres Programms als Argument:
D:\Code>gdb zufall
gdb zeigt jetzt erstmal Versionsnummer und diverse Lizenzinformationen an - uninteressant.
D:\Code>gdb zufall
GNU gdb (GDB) 7.1
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "mingw32".
For bug reporting instructions, please see:
...
aber die folgenden letzten beiden Zeilen sind spannend:
Reading symbols from D:\Code/zufall.exe...done.
(gdb) _
Hinter (gdb)
blinkt der Cursor - GDB wartet auf Anweisungen. Es gibt eine ganze Reihe von Befehlen, die man jetzt eingeben könnte. Nur ein paar davon werden wir uns in dieser kleinen Debugging-Session anschauen.
run (r)
run
führt das Programm aus. run gibt man als Parameter diejenigen, die man normalerweise an das Programm selbst gegeben hätte. In unserem Fall sieht das Ganze also so aus:
(gdb) run 42 Starting program: D:\Code/zufall.exe 42 [New Thread 2356.0xb98] Program exited normally. (gdb) _
Wenn das Programm einfach so durchläuft nützt uns das natürlich wenig. Wir wollen unser Programm zwischendurch anhalten, um nachzuschauen, was unter der Haube so passiert! Dazu gibt es sogenannte Breakpoints…
break (b)
Breakpoints sind Punkte, an denen das Program solange pausiert, bis man ihm sagt, dass es weiterlaufen soll. Diese Pausen geben einem Gelegenheit, weitere GDB-Befehle einzugeben, um den Inhalt von Variablen zu erfahren, backtraces auszugeben und vieles mehr!
Der Befehl zum Einfügen eines Breakpoints lautet break
gefolgt von der Zeilennummer, bei der man anhalten möchte. Wenn der Quellcode aus mehreren Dateien besteht, muss man den Dateinamen vor die Zeilennummer schrieben - also ungefähr so: /absoluter/Pfad/zur/Datei/zufall.cpp:Zeilennummer
Unser Code besteht aber nur aus einer Datei, also machen wir folgendes, um einen Breakpoint in Zeile 14 zu setzen:
(gdb) break 14
Breakpoint 1 at 0x4013f9: file zufall.cpp, line 14.
(gdb)
Warum Zeile 14? Meistens hat man ja schon so eine Ahnung, wo der Fehler sein könnte. In diesem Fall ist das anders, deswegen habe ich den Breakpoint einfach mal in die Mitte gepackt.
Wenn wir den Code jetzt ausführen, wird die Ausführung am Anfang von Zeile 14 anhalten. Wir machen also erneut run 42
um das Programm wieder zu starten:
(gdb) run 42
Starting program: D:\Code/zufall.exe
[New Thread 2624.0x10a4]
Breakpoint 1, main (argc=1, argv=0x9110a0) at zufall.cpp:15 15 for(uint i; i<liste.size(); ++i) (gdb) _
In der zweiten Zeile von unten sagt uns GDB, an welchem Breakpoint wir uns befinden (die sind einfach durchnummeriert). Darunter steht die aktuelle Zeilenzahl und der Code, der in dieser Zeile steht. GDB ist gleich zu Zeile 15 gesprungen, da Zeile 14 leer ist und somit irrelevant.
GDB wartet nun auf weitere Befehle.
list (l)
Als erstes machen wir mal list
. list
zeigt die aktuelle Codezeile an, sowie ein paar Zeilen davor und danach.
(gdb) list 10 11 vector <double> liste; 12 for(uint i = 0; i < length; ++i) 13 liste.push_back( double(rand())/RAND_MAX ); 14 15 for(uint i; i<liste.size(); ++i) 16 cout << liste.at(i); 17 return 0; 18 } (gdb)
Als nächstes schauen wir uns am besten mal ein paar Variablen an…
print (p)
Mit print
kann man sich den Inhalt von Variablen anzeigen lassen oder sogar Funktionen aufrufen.
Zuerst interessiert uns mal, ob in length
wirklich 42 drin steht.
(gdb) print length
$1 = 42
(gdb)
Okay, hier ist alles in Ordnung. Die $1
ist nur eine fortlaufende Nummer unserer prints und hat ansonsten keine Bewandnis.
Als nächstes wollen wir uns den Vector anschauen:
(gdb) print liste
$2 = { >> = {
_M_impl = {> = {<__gnu_cxx::new_allocator> = {},
_M_finish = 0x8a27b8, _M_end_of_storage = 0x8a2868}}, }
(gdb)
Das ist jetzt eher mäßig nützlich. Schauen wir stattdessen einfach mal auf die Größe des Vectors und lassen uns das erste und letzte Element anzeigen:
(gdb) print liste.size()
$3 = 42
(gdb) print liste[0]
$4 = (double &) @0x8a2668: 0.0012512588885158849
(gdb) print liste[41]
$5 = (double &) @0x8a27b0: 0.72667622913296914
(gdb)
Die size()
haut hin und erstes und letztes Element enthalten sinnvolle Werte. Offenbar ist mit unserem Vector also alles in Ordnung. Der Fehler muss also in der Ausgabe liegen. Wir setzen deshalb einen zweiten Breakpoint in das Innere der Schleife in Zeile 16:
(gdb) break 16
Breakpoint 2 at 0x40143b: file zufall.cpp, line 16.
(gdb)
continue (c)
Die Ausführung des Programms ist immer noch angehalten - mit continue
lassen wir das Programm weiterlaufen:
(gdb) continue
Continuing.
Program exited normally.
(gdb) _
Huch?! Wir hatten eigentlich erwartet, dass unser Programm bei dem neuen Breakpoint in Zeile 16 hängen bleibt! Ist es aber nicht. Das führt uns zur Vermutung, dass mit der Schleife irgendetwas nicht stimmt.
Starten wir das Programm erstmal erneut mit run
:
(gdb) run 42 Starting program: D:\Code/zufall.exe 42 [New Thread 4124.0x10c4] Breakpoint 1, main (argc=2, argv=0x3810d8) at zufall.cpp:15 15 for(uint i; i<liste.size(); ++i) (gdb) _
Wir sind wieder an unserem ersten Breakpoint und wollen jetzt ganz genau wissen, was passiert!
step (s) und next (n)
Mit step
und next
kann man den Code Zeile für Zeile weiterlaufen lassen. Der Unterschied zwischen beiden ist, dass step
auch in Funktionen reingeht und dort wiederum Zeile für Zeile durch geht - next
tut das nicht, bleibt quasi immer auf der selben „Ebene”.
Wir benutzen mal step, damit wir nichts verpassen:
(gdb) step
std::vector >::size (this=0x28feec) at c:/code/mingw/bin/../lib/gcc/m
534 { return size_type(this->_M_impl._M_finish - this->_M_impl._M_start); }
(gdb) _
Hm? Irgendwas mit vector? Erstmal mit list
Orientierung verschaffen:
(gdb) list
529
530 // 1 capacity
531 /** Returns the number of elements in the %vector. */
532 size_type
533 size() const
534 { return size_type(this->_M_impl._M_finish - this->_M_impl._M_start); }
535
536 /** Returns the size() of the largest possible %vector. */
537 size_type
538 max_size() const
(gdb) _
Ah! Wir befinden uns offenbar im Inneren der Funktion size(), die wir in unserem Schleifenkopf aufrufen. Wir machen mal erneut step.
(gdb) step
_fu0___ZSt4cout (argc=2, argv=0x3410d8) at zufall.cpp:17
17 return 0;
(gdb)
Wir sind wieder in unserer zufall.cpp am return und somit am Ende unseres Programms angelangt.
Starten wir unser Programm also wieder neu:
(gdb) run 42
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: D:\Code/zufall.exe 42
[New Thread 3792.0x12d8]
Breakpoint 1, main (argc=2, argv=0x3f10d8) at zufall.cpp:15
15 for(uint i; i
(gdb)
(Die Frage „Start it from the beginning? (y or n)” beantworten wir logischerweise mit y.)
Wir lassen uns mal i ausgeben:
(gdb) print i
$6 = 2686760
(gdb)
i ist 2686760? Sollte das nicht Nul… Spätestens jetzt merken wir hoffentlich, dass wir vergessen haben, i zu intialisieren! Bug gefunden! Wir ändern
for(uint i;
zu
for(uint i = 0;
quit (q)
Jetzt wollen wir neu kompilieren, aber gdb läuft noch! Man beendet gdb mit quit
. Es kommt noch eine Warnung, dass mit dem Beenden von GDB auch unser Programm (das ja immer noch in Zeile 15 Pause macht) gekillt wird. Die bestätigen wir einfach mal, kompilieren dann erneut und führen unser Programm erneut aus:
(gdb) quit A debugging session is active. Inferior 1 [process 3792] will be killed. Quit anyway? (y or n) y D:\Code>g++ -Wall -g zufall.cpp -o zufall D:\Code>zufall 42 0.001251260.5635850.1933040.8087410.5850090.4798730.3502910.8959620.822840.74 66050.1741080.8589430.7105010.5135350.3039950.01498460.09140290.3644520.14731 30.1658990.9885250.4456920.1190830.004669330.00891140.377880.5316630.5711840. 6017640.6071660.1662340.6630450.4507890.3521230.05703910.6076850.7833190.8026 060.5198830.301950.8759730.726676 D:\Code>
Erfolg! Okay, unsere Ausgabe ist ungeschickt gemacht, da müssten mal noch Leerzeichen rein oder so. Aber sonst läufts 🙂
Weitere Tipps
- Einfach Enter drücken reicht, um den letzten Befehl zu wiederholen. Besonders für
step
undnext
sinnvoll. - Alle Befehle gibt es auch als Kurzversion, die ich oben in Klammern hinter den Befehl geschrieben habe - zum Beispiel
n
stattnext
. - Arrays kann man nach folgendem Schema ausgeben lassen:
erstesElement@längeDesArrays
- 2.4.2 ↩