Home

Debugger - GDB

Montag, 22. November 2010 | Autor:

Debug­ger sind Pro­gramme, die einem bei der Feh­ler­su­che hel­fen sol­len. Mit einem Debug­ger kann man das Pro­gramm quasi bei der Arbeit beob­ach­ten: Wel­che Code­zeile wird als nächs­tes aus­ge­führt? Was ent­hält diese und jene Varia­ble momen­tan? Wie sieht der Call-Stack/Backtrace momen­tan aus? An wel­cher Stelle stürzt das Pro­gramm genau ab? und vie­les ande­res mehr.

Für C++ (und jede Menge ande­rer Spra­chen) gibt es den GNU-Debugger, kurz GDB.

GDB

Eine gute Web­site zum GDB fin­det sich hier: RMS’s gdb Tutorial

Es folgt ein klei­nes Bei­spiel. Eine Liste soll mit 10 Zufalls­wer­ten zwi­schen 0 und 1 gefüllt wer­den. Unser Pro­gramm bekommt beim Auf­ruf als Para­me­ter die Länge mit­ge­ge­ben.
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 kom­pi­lie­ren ihn erfolg­reich, zum Bei­spiel mit:
g++ -Wall zufall.cpp -o zufall
und füh­ren ihn aus um, sagen wir mal 42 Zufalls­zah­len erzeugen:

D:\Code>zufall 42

D:\Code>

Wie wir sehen, läuft unser Pro­gramm zwar durch, gibt aber wider Erwar­ten nichts aus.

Um die­sen Bug zu fin­den, wol­len wir den GDB als Debug­ger benut­zen. Das geht so:

1. Mit Debugging-Informationen kompilieren

Nor­ma­ler­weise lässt sich am fer­tig kom­pi­lier­ten Pro­gramm nicht mehr erken­nen, aus wel­cher Datei oder Zeile im Quell­code ein Maschi­nen­be­fehl ursprüng­lich her­vor­ge­gan­gen ist oder mit wel­chem Varia­blen­na­men ein bestimm­ter Wert im Spei­cher mal benannt war. Um diese Infor­ma­tion zu bewah­ren, muss beim Kom­pi­lie­ren (und ggf. Lin­ken) der Para­me­ter -g benutzt wer­den. Sah unser Befehl zum Kom­pi­lie­ren bis­her so aus:
g++ -Wall zufall.cpp -o zufall
wird dar­aus nun
g++ -Wall -g zufall.cpp -o zufall

Es kommt ein­fach ein -g dazu. Mehr isses nicht.

GDB star­ten

Jetzt kön­nen wir den GDB benut­zen. Dazu star­ten wir gdb mit dem Namen unse­res Pro­gramms als Argu­ment:
D:\Code>gdb zufall

gdb zeigt jetzt erst­mal Ver­si­ons­num­mer und diverse Lizenz­in­for­ma­tio­nen an - unin­ter­es­sant.
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 fol­gen­den letz­ten bei­den Zei­len sind span­nend:
Reading symbols from D:\Code/zufall.exe...done.
(gdb) _

Hin­ter (gdb) blinkt der Cur­sor - GDB war­tet auf Anwei­sun­gen. Es gibt eine ganze Reihe von Befeh­len, die man jetzt ein­ge­ben könnte. Nur ein paar davon wer­den wir uns in die­ser klei­nen Debugging-Session anschauen.

run (r)

run führt das Pro­gramm aus. run gibt man als Para­me­ter die­je­ni­gen, die man nor­ma­ler­weise an das Pro­gramm selbst gege­ben hätte. In unse­rem 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 Pro­gramm ein­fach so durch­läuft nützt uns das natür­lich wenig. Wir wol­len unser Pro­gramm zwi­schen­durch anhal­ten, um nach­zu­schauen, was unter der Haube so pas­siert! Dazu gibt es soge­nannte Breakpoints…

break (b)

Bre­ak­points sind Punkte, an denen das Pro­gram solange pau­siert, bis man ihm sagt, dass es wei­ter­lau­fen soll. Diese Pau­sen geben einem Gele­gen­heit, wei­tere GDB-Befehle ein­zu­ge­ben, um den Inhalt von Varia­blen zu erfah­ren, back­tra­ces aus­zu­ge­ben und vie­les mehr!

Der Befehl zum Ein­fü­gen eines Bre­ak­points lau­tet break gefolgt von der Zei­len­num­mer, bei der man anhal­ten möchte. Wenn der Quell­code aus meh­re­ren Dateien besteht, muss man den Datei­na­men vor die Zei­len­num­mer schrie­ben - also unge­fähr so: /absoluter/Pfad/zur/Datei/zufall.cpp:Zeilennummer

Unser Code besteht aber nur aus einer Datei, also machen wir fol­gen­des, um einen Bre­ak­point in Zeile 14 zu set­zen:
(gdb) break 14
Breakpoint 1 at 0x4013f9: file zufall.cpp, line 14.
(gdb)

Warum Zeile 14? Meis­tens hat man ja schon so eine Ahnung, wo der Feh­ler sein könnte. In die­sem Fall ist das anders, des­we­gen habe ich den Bre­ak­point ein­fach mal in die Mitte gepackt.

Wenn wir den Code jetzt aus­füh­ren, wird die Aus­füh­rung am Anfang von Zeile 14 anhal­ten. Wir machen also erneut run 42 um das Pro­gramm wie­der zu star­ten:
(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 zwei­ten Zeile von unten sagt uns GDB, an wel­chem Bre­ak­point wir uns befin­den (die sind ein­fach durch­num­me­riert). Dar­un­ter steht die aktu­elle Zei­len­zahl und der Code, der in die­ser Zeile steht. GDB ist gleich zu Zeile 15 gesprun­gen, da Zeile 14 leer ist und somit irre­le­vant.
GDB war­tet nun auf wei­tere Befehle.

list (l)

Als ers­tes machen wir mal list. list zeigt die aktu­elle Code­zeile an, sowie ein paar Zei­len 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ächs­tes schauen wir uns am bes­ten mal ein paar Varia­blen an…

print (p)

Mit print kann man sich den Inhalt von Varia­blen anzei­gen las­sen oder sogar Funk­tio­nen aufrufen.

Zuerst inter­es­siert uns mal, ob in length wirk­lich 42 drin steht.
(gdb) print length
$1 = 42
(gdb)

Okay, hier ist alles in Ord­nung. Die $1 ist nur eine fort­lau­fende Num­mer unse­rer prints und hat ansons­ten keine Bewandnis.

Als nächs­tes wol­len wir uns den Vec­tor 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ütz­lich. Schauen wir statt­des­sen ein­fach mal auf die Größe des Vec­tors und las­sen uns das erste und letzte Ele­ment anzei­gen:
(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 ers­tes und letz­tes Ele­ment ent­hal­ten sinn­volle Werte. Offen­bar ist mit unse­rem Vec­tor also alles in Ord­nung. Der Feh­ler muss also in der Aus­gabe lie­gen. Wir set­zen des­halb einen zwei­ten Bre­ak­point in das Innere der Schleife in Zeile 16:
(gdb) break 16
Breakpoint 2 at 0x40143b: file zufall.cpp, line 16.
(gdb)

con­ti­nue (c)

Die Aus­füh­rung des Pro­gramms ist immer noch ange­hal­ten - mit continue las­sen wir das Pro­gramm weiterlaufen:

(gdb) continue
Continuing.

Program exited normally.
(gdb) _

Huch?! Wir hat­ten eigent­lich erwar­tet, dass unser Pro­gramm bei dem neuen Bre­ak­point in Zeile 16 hän­gen bleibt! Ist es aber nicht. Das führt uns zur Ver­mu­tung, dass mit der Schleife irgend­et­was nicht stimmt.

Star­ten wir das Pro­gramm erst­mal 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 wie­der an unse­rem ers­ten Bre­ak­point und wol­len jetzt ganz genau wis­sen, was passiert!

step (s) und next (n)

Mit step und next kann man den Code Zeile für Zeile wei­ter­lau­fen las­sen. Der Unter­schied zwi­schen bei­den ist, dass step auch in Funk­tio­nen rein­geht und dort wie­derum Zeile für Zeile durch geht - next tut das nicht, bleibt quasi immer auf der sel­ben „Ebene”.
Wir benut­zen mal step, damit wir nichts ver­pas­sen:
(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? Irgend­was mit vec­tor? Erst­mal mit list Ori­en­tie­rung ver­schaf­fen:
(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 befin­den uns offen­bar im Inne­ren der Funk­tion size(), die wir in unse­rem Schlei­fen­kopf auf­ru­fen. Wir machen mal erneut step.
(gdb) step
_fu0___ZSt4cout (argc=2, argv=0x3410d8) at zufall.cpp:17
17 return 0;
(gdb)

Wir sind wie­der in unse­rer zufall.cpp am return und somit am Ende unse­res Pro­gramms angelangt.

Star­ten wir unser Pro­gramm also wie­der 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 begin­ning? (y or n)” beant­wor­ten wir logi­scher­weise mit y.)

Wir las­sen uns mal i aus­ge­ben:
(gdb) print i
$6 = 2686760
(gdb)

i ist 2686760? Sollte das nicht Nul… Spä­tes­tens jetzt mer­ken wir hof­fent­lich, dass wir ver­ges­sen haben, i zu intia­li­sie­ren! Bug gefun­den! Wir ändern
for(uint i;
zu
for(uint i = 0;

quit (q)

Jetzt wol­len wir neu kom­pi­lie­ren, aber gdb läuft noch! Man been­det gdb mit quit. Es kommt noch eine War­nung, dass mit dem Been­den von GDB auch unser Pro­gramm (das ja immer noch in Zeile 15 Pause macht) gekillt wird. Die bestä­ti­gen wir ein­fach mal, kom­pi­lie­ren dann erneut und füh­ren unser Pro­gramm 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 Aus­gabe ist unge­schickt gemacht, da müss­ten mal noch Leer­zei­chen rein oder so. Aber sonst läufts :)

Wei­tere Tipps

  • Ein­fach Enter drü­cken reicht, um den letz­ten Befehl zu wie­der­ho­len. Beson­ders für step und next sinnvoll.
  • Alle Befehle gibt es auch als Kurz­ver­sion, die ich oben in Klam­mern hin­ter den Befehl geschrie­ben habe - zum Bei­spiel n statt next.
  • Arrays kann man nach fol­gen­dem Schema aus­ge­ben las­sen: erstesElement@längeDesArrays
  1. 2.4.2

Tags » , «

Trackback: Trackback-URL |  Feed zum Beitrag: RSS 2.0
Thema: Programmieren, Sezierte C++-Käfer

Diesen Beitrag kommentieren.

Kommentar abgeben