Home

Debugger - GDB

Montag, 22. November 2010 | Autor:

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

Für C++ (und jede Men­ge ande­rer Spra­chen) gibt es den GNU-Debug­ger, 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 Lis­te 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än­ge 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 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­wei­se lässt sich am fer­tig kom­pi­lier­ten Pro­gramm nicht mehr erken­nen, aus wel­cher Datei oder Zei­le 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 die­se Infor­ma­ti­on 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 starten

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

gdb zeigt jetzt erst­mal Ver­si­ons­num­mer und diver­se Lizenz­in­for­ma­tio­nen 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 fol­gen­den letz­ten bei­den Zei­len sind spannend:
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 gan­ze Rei­he von Befeh­len, die man jetzt ein­ge­ben könn­te. Nur ein paar davon wer­den wir uns in die­ser klei­nen Debug­ging-Ses­si­on 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­wei­se an das Pro­gramm selbst gege­ben hät­te. In unse­rem Fall sieht das Gan­ze 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­schau­en, was unter der Hau­be so pas­siert! Dazu gibt es soge­nann­te Breakpoints…

break (b)

Break­points sind Punk­te, an denen das Pro­gram solan­ge pau­siert, bis man ihm sagt, dass es wei­ter­lau­fen soll. Die­se Pau­sen geben einem Gele­gen­heit, wei­te­re GDB-Befeh­le ein­zu­ge­ben, um den Inhalt von Varia­blen zu erfah­ren, back­traces aus­zu­ge­ben und vie­les mehr!

Der Befehl zum Ein­fü­gen eines Break­points lau­tet break gefolgt von der Zei­len­num­mer, bei der man anhal­ten möch­te. Wenn der Quell­code aus meh­re­ren Datei­en 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 Break­point in Zei­le 14 zu setzen:
(gdb) break 14
Breakpoint 1 at 0x4013f9: file zufall.cpp, line 14.
(gdb)

War­um Zei­le 14? Meis­tens hat man ja schon so eine Ahnung, wo der Feh­ler sein könn­te. In die­sem Fall ist das anders, des­we­gen habe ich den Break­point ein­fach mal in die Mit­te gepackt.

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

list (l)

Als ers­tes machen wir mal list. list zeigt die aktu­el­le Code­zei­le 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 schau­en 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­fen­de Num­mer unse­rer prints und hat ansons­ten kei­ne 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. Schau­en wir statt­des­sen ein­fach mal auf die Grö­ße des Vec­tors und las­sen uns das ers­te und letz­te Ele­ment 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 ers­tes und letz­tes Ele­ment ent­hal­ten sinn­vol­le Wer­te. Offen­bar ist mit unse­rem Vec­tor also alles in Ord­nung. Der Feh­ler muss also in der Aus­ga­be lie­gen. Wir set­zen des­halb einen zwei­ten Break­point in das Inne­re der Schlei­fe in Zei­le 16:
(gdb) break 16
Breakpoint 2 at 0x40143b: file zufall.cpp, line 16.
(gdb)

continue (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 neu­en Break­point in Zei­le 16 hän­gen bleibt! Ist es aber nicht. Das führt uns zur Ver­mu­tung, dass mit der Schlei­fe 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 Break­point und wol­len jetzt ganz genau wis­sen, was passiert!

step (s) und next (n)

Mit step und next kann man den Code Zei­le für Zei­le 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­der­um Zei­le für Zei­le durch geht - next tut das nicht, bleibt qua­si immer auf der sel­ben „Ebe­ne”.
Wir benut­zen 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? Irgend­was mit vec­tor? Erst­mal mit list Ori­en­tie­rung 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 befin­den uns offen­bar im Inne­ren der Funk­ti­on 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 Fra­ge „Start it from the begin­ning? (y or n)” beant­wor­ten wir logi­scher­wei­se mit y.)

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

i ist 2686760? Soll­te 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 Zei­le 15 Pau­se 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, unse­re Aus­ga­be ist unge­schickt gemacht, da müss­ten mal noch Leer­zei­chen rein oder so. Aber sonst läufts 🙂

Weitere 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 Befeh­le gibt es auch als Kurz­ver­si­on, 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 Sche­ma aus­ge­ben las­sen: erstesElement@längeDesArrays
  1. 2.4.2
Tags » , «

Trackback: