Handleiding software debugging met GDB/DDD

advertisement
Handleiding software debugging met GDB/DDD
ir drs E.J Boks en ing. H. Muileman
Inhoudsopgave
Inleiding...................................................................................................................2
Beschikbaarheid...................................................................................................... 2
Begrippen................................................................................................................2
Debugging mogelijk maken.....................................................................................3
GDB: een lokale debugging sessie.........................................................................4
DDD: een lokale debugging sessie.......................................................................16
Inspectie van processor en variabelen.............................................................19
Datastructuren via extern grafisch programma............................................21
Onderbreking programmaverloop.....................................................................22
Processen aanhaken....................................................................................23
Threads aanhaken....................................................................................... 24
Programmaverloop........................................................................................... 24
DDD: een remote debugging sessie.....................................................................25
Inleiding
Dit document geeft een korte introductie van het gebruik van de Gnu Debugger
(GDB) zowel in command-line mode als in combinatie met het Data Display
Debugger (DDD) front-end. Bij de uitleg is gebruikt gemaakt van Ubuntu Linux en
FreeBSD. De methodiek is bruikbaar op vrijwel alle Unix/Linux platforms,
alsmede Mac OS X en de Cygwin omgeving onder MS-Windows.
GDB is een programma voor het opsporen van fouten in programmatuur. Deze
programmatuur kan geschreven zijn in een groot aantal hogere
programmeertalen. Daarnaast is het niet noodzakelijk dat het te testen
programma op dezelfde computer draait als GDB. Ook is het niet noodzakelijk
dat deze te testen computer met dezelfde processorarchitectuur is uitgerust als
de computer waarop GDB draait. Met zogenaamd Remote Debugging kan een
programma op een andere computer/andere architectuur worden getest alsof het
programma op de eigen computer draait.
GDB is een commando gestuurd programma. Alle acties binnen GDB worden
met een commando begonnen. Om een complex verloop van de debugging
sessie gemakkelijke te maken, kan naast/om GDB een grafische schil worden
gebruikt. DDD is zo een schil. Met DDD kunnen complexe datastructuren uit het
te testen programma worden afgebeeld.
Indien het gewenst is kan DDD/GDB op een andere computer draaien waar de
gebruiker aanwezig is. Communicatie tussen de twee systemen geschiedt dan
via het X Windows systeem.
Beschikbaarheid
DDD en GDB zijn vaak al geinstalleerd op veel Unix/Linux platforms. Als dat niet
zo is, dan kan men kostenloos de broncode van GDB en DDD op internet vinden,
downloaden en installeren. Meer informatie hierover op :
http://www.gnu.org/software/gdb
en
http://www.gnu.org/software/ddd
Het is ook mogelijk de debugger te installeren via het package mgmt system van
het gebruikte Operating System:
•
Ubuntu Linux/Debian Linux: de Synaptic pkg manager of apt-get.
•
FreeBSD: de Ports structuur
•
Apple OS X: de MacPorts structuur
Begrippen
De volgende begrippen worden gebruikt in de GDB/DDD context:
• host . Dit is de computer waarop GDB/DDD draait.
• target. Dit is de computer waarop de te testen software draait.
Debugging mogelijk maken
Om debugging met GDB/DDD mogelijk te maken, moet de broncode
gecompileerd worden met debugging informatie. Deze informatie wordt in een
debug formaat aan het uitvoerbare (ELF) bestand toegevoegd.
Bij de GNU compiler (gcc) gebeurt dit met toevoeging van een van de volgende
switches aan gcc:
gcc -gstabs+ .......
(BSD DBX formaat).
of
gcc -gdwarf+........ (System V Release 4 formaat )
of
gcc -ggdb .......
(GNU formaat )
de switch -g schakelt bij compilatie automatisch de op het systeem standaard
gebruikte debugging methode in. Dus, op FreeBSD impliceert -g -gstabs+, maar
onder Linux is dit -ggdb .
Het is raadzaam maar niet verplicht om optimalisatie uit te schakelen bij
debugging. Optimalisatie leidt echter tot ombouw van code die niet geheel
overeenkomt met de aanwezig C/C++ broncode, dus U bent gewaarschuwd voor
onverklaarbare en vreemde effecten.
GDB: een lokale debugging sessie
De volledige documentatie van de GNU-debugger is te vinden op het internet.
Google maar eens op gdb.pdf (niet op Nederlandse pagina's!)
Voer met een editor het onderstaande programmaatje bugje.c in:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int array[2]={5064,-5064},i;
for (i=0; i<=2;i++)
{
array[i]=0;
}
return 0;
}
Compileer het programma vervolgens en run het. Het programma hangt zichzelf
op!
N.B. Als je een Linux distributie voor 64-bits hebt geïnstalleerd, gebeurt dit niet!
Om een gecompileerd programma te kunnen debuggen, is tijdens het compileren
een debug optie nodig, bv de optie -g
Compileer bugje.c met de optie -g , b.v. met het commando:
gcc -Wall bugje.c -g -o bugje
De GNU-debugger kan vervolgens worden aangeroepen met het commando:
gdb bugje
Ook de output a.out van de compiler zonder -o optie kan worden gebruikt:
gdb a.out
Als je bovenstaand commando (gdb bugje) uitvoert, zonder dat je met -g hebt
gecompileerd, krijg je de volgende reactie van de debugger:
GNU gdb 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
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 "i486-linux-gnu"...
bugje: No such file or directory.
(gdb)
Roep je de debugger wel correct aan, krijg je hetzelfde verhaal, maar zonder de
een na laatste regel.
Vind je de welkomsttekst overbodig, gebruik je de quiet optie: gdb bugje -q
De debugger meldt zich dan alleen met de prompt, of met een foutmelding:
(gdb)
Ook is het mogelijk de debugger aan te roepen met b.v. gdb -q
(gdb) file bugje (d.i. binnen de debugger het commando: file bugje)
Reading symbols from padnaam/bugje...done. (reactie van debugger als dit
succesvol is gebeurd)
Een help menu is op te vragen met:
(gdb) help
List of classes of commands:
aliases -- Aliases of other commands
breakpoints -- Making program stop at certain points
data -- Examining data
files -- Specifying and examining files
internals -- Maintenance commands
obscure -- Obscure features
running -- Running the program
stack -- Examining the stack
status -- Status inquiries
support -- Support facilities
tracepoints -- Tracing of program execution without stopping the program
user-defined -- User-defined commands
Type "help" followed by a class name for a list of commands in that class.
Type "help all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.
(gdb)
Een van de klassen van commando's is op te vragen met:
(gdb) help running
Running the program.
List of commands:
advance -- Continue the program up to the given location (same form as args for
break command)
attach -- Attach to a process or file outside of GDB
continue -- Continue program being debugged
detach -- Detach a process or file previously attached
detach checkpoint -- Detach from a fork/checkpoint (experimental)
disconnect -- Disconnect from a target
finish -- Execute until selected stack frame returns
handle -- Specify how to handle a signal
interrupt -- Interrupt the execution of the debugged program
jump -- Continue program being debugged at specified line or address
kill -- Kill execution of program being debugged
next -- Step program
nexti -- Step one instruction
run -- Start debugged program
signal -- Continue program giving it signal specified by the argument
start -- Run the debugged program until the beginning of the main procedure
step -- Step program until it reaches a different source line
stepi -- Step one instruction exactly
target -- Connect to a target machine or process
target async -- Use a remote computer via a serial line
target child -- Unix child process (started by the "run" command)
target core -- Use a core file as a target
target exec -- Use an executable file as a target
target extended-async -- Use a remote computer via a serial line
target extended-remote -- Use a remote computer via a serial line
target multi-thread -- Threads and pthreads support
target remote -- Use a remote computer via a serial line
thread -- Use this command to switch between threads
thread apply -- Apply a command to a list of threads
thread apply all -- Apply a command to all threads
until -- Execute until the program reaches a source line greater than the current
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.
(gdb)
Met het commando l (list) worden default 10 regels van de programmasource,
voorzien van regelnummers, afgedrukt:
(gdb) l
1
#include <stdio.h>
2
int main()
3
{
4
int array[2]={5064,-5064},i;
5
for (i=0; i<=2;i++)
6
{
7
array[i]=0;
8
}
9
return 0;
10
}
Wordt daarna op de enter toets gedrukt, wordt opnieuw een l commando
gegeven, met als reactie:
Line number 11 out of range; bugje.c has 10 lines. (bij een langere source
zouden de volgende 10 regels worden getoond)
Met het commando: (gdb) disassemble main krijg je het volgende te zien:
Dump of assembler code for function main:
0x08048394 <main+0>: lea
0x4(%esp),%ecx
0x08048398 <main+4>: and $0xfffffff0,%esp
0x0804839b <main+7>: pushl -0x4(%ecx)
0x0804839e <main+10>: push %ebp
0x0804839f <main+11>: mov %esp,%ebp
0x080483a1 <main+13>:
0x080483a2 <main+14>:
0x080483a5 <main+17>:
0x080483ac <main+24>:
0x080483b3 <main+31>:
0x080483ba <main+38>:
0x080483bc <main+40>:
0x080483bf <main+43>:
0x080483c7 <main+51>:
0x080483cb <main+55>:
0x080483cf <main+59>:
0x080483d1 <main+61>:
0x080483d6 <main+66>:
0x080483d9 <main+69>:
0x080483da <main+70>:
0x080483db <main+71>:
0x080483de <main+74>:
End of assembler dump
push
sub
movl
movl
movl
jmp
mov
movl
addl
cmpl
jle
mov
add
pop
pop
lea
ret
%ecx
$0x10,%esp
$0x13c8,-0x10(%ebp)
$0xffffec38,-0xc(%ebp)
$0x0,-0x8(%ebp)
0x80483cb <main+55>
-0x8(%ebp),%eax
$0x0,-0x10(%ebp,%eax,4)
$0x1,-0x8(%ebp)
$0x2,-0x8(%ebp)
0x80483bc <main+40>
$0x0,%eax
$0x10,%esp
%ecx
%ebp
-0x4(%ecx),%esp
Dit is de assembler code van het C-programma bugje, zoals dat in het geheugen
van de computer staat. Het beslaat de adressen van 0x08048394 tot en met
0x080483de Dat zijn in totaal 75 bytes! Vergelijk dit met de 9115 bytes die de
executable van bugje.c oplevert!
Merk ook op dat de code van lage naar hogere adressen in het geheugen wordt
gezet; dit geldt ook voor data in het geheugen!
De inhoud van het geheugen op bitniveau, is te onderzoeken met het commando
x.
(gdb) help x
Examine memory: x/FMT ADDRESS.
ADDRESS is an expression for the memory address to examine.
FMT is a repeat count followed by a format letter and a size letter.
Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), c(char) and s(string).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes).
The specified number of objects of the specified size are printed
according to the format.
Defaults for format and size letters are those previously used.
Default count is 1. Default address is following last thing printed
with this command or "print".
Zo kunnen we met het commando (gdb) x/7b 0x080483a5 zien hoe de opcode
van de instructie
movl $0x13c8,-0x10(%ebp) op adres 0x080483a5, regel <main+17> in het
geheugen staat:
0x80483a5 <main+17>:
0xc7 0x45 0xf0
0xc8 0x13 0x00 0x00
In feite wordt met deze instructie het eerste getal (5064) uit het array in het
geheugen gezet. De totale instructie beslaat 7 bytes (van het adres 0x80483a5
t/m het adres 0x080483ab) Idem dito wordt met de instructie movl $0xffffec38,-
0xc(%ebp) het tweede getal (-5064) uit het array in het geheugen gezet. De
opcode van deze instructie bestaat ook uit 7 bytes (van het adres 0x080483ac
t/m het adres 0x080483b2):
(gdb) x/7b 0x080483ac
0x80483ac <main+24>:
0xc7 0x45 0xf4
0x38 0xec 0xff
0xff
De for lus van bugje beslaat de regels <main+40> t/m <main+59>
Om het debuggen te starten, moet het commando start worden ingevoerd:
(gdb) start
Breakpoint 1 at 0x80483a5: file bugje.c, line 4.
Starting program:<padnaam>/bugje
main () at bugje.c:4
4
int array[2]={ 5064,-5064},i;
De debugger zet zelf een breakpoint op het eerste statement en stopt daar. Dit
betekent dat dit statement nog niet is uitgevoerd!
Met het commando p kan een variabele worden afgedrukt:
(gdb) p array
$1 = {-1076422040, 134513673}
(gdb) p i
$1 = -1208644176
De waarden voor de getallen uit de array zijn nog niet bepaald; -1076422040 en
134513673 zijn willekeurige waarden, die op dit moment in de geheugenlocaties
die gebruikt worden voor het array, staan.
Idem dito de waarde voor i; de waarde -1208644176 is ook een willekeurige
waarde, die op dit moment in de geheugenlocatie die gebruikt wordt voor i, staat.
We kunnen ook op bitniveau kijken wat er in het geheugen staat:
(gdb) x/4xb array
0xbfd71a48: 0xc8 0x13 0x00 0x00
(gdb) x/4xb array+1
0xbfd71a4c: 0x38 0xec 0xff
0xff
(gdb) x/4xb i
0xbfd71a50: 0x55 0x89 0xe5 0x57
Met het commando (gdb) x kijk je wat er in een geheugenplaats op bitniveau
staat: met 4xb specificeer je in dit geval dat je 4 bytes in hexadecimaal formaat
wilt zien.
Merk op dat als je array+1 gebruikt in het print commando, de debugger vier
adressen verder kijkt. Voor integer getallen worden op dit platform 4 bytes
gebruikt, die hierboven in hexadecimaal formaat zijn weergegeven. Zodoende
weet de debugger dat hij bij array+1 vier bytes verder moet kijken naar het
volgende getal.
Merk ook op dat array[0], array[1] en i, op opeenvolgende plaatsen in het
geheugen staan!
Met het commando (gdb) s kan vervolgens statement voor statement door de
code worden gestapt:
(gdb) s
5
for (i=0; i<=2;i++)
Nu zijn de getallen uit het array in het geheugen gezet (regel 4 is uitgevoerd) en
stopt de debugger op
regel 5:
(gdb) p array
$3 = {5064, -5064}
(gdb) x/4xb array
0xbfd71a48: 0xc8 0x13 0x00 0x00
In het geheugen staan vanaf adres 0xbfd71a48 4 bytes die kennelijk het getal
5064 representeren. Als we getallen in een computergeheugen opslaan, moeten
we eerst afspreken hoe we dat doen: in welke volgorde worden de bytes in het
geheugen gezet (‘byte order’). In de Intel architectuur is gekozen voor de
methode van de “Little Endian” , dat betekent dat we op het laagste adres het
laagstwaardige deel van het getal neerzetten.
In andere architecturen, zoals die van b.v. Motorola bij de 68000 en de Powerpc,
is gekozen voor de “Big Endian” , dat betekent dat we op het laagste adres het
hoogstwaardige deel van het getal neerzetten.
In moderne architecturen, zoals die van b.v. de Arm processor, is simpelweg te
programmeren in welke volgorde de bytes in het geheugen worden gezet.
Keren we nu terug naar de Intel-architectuur en naar het decimale getal 5064. Dit
getal wordt in 4 bytes opgeslagen en is met 8 hexadecimale digits te beschrijven:
5064 =
0x167+0x166+0x165+0x164+1x163+3x162+Cx161+8x160=1x4096+3x256+12x16+8
=4096+768+192+8
Het getal 5064 is hexadecimaal dus te schrijven als 0x000013C8. De
laagstwaardige byte (least significant byte) is daarbij de byte met de inhoud
0xC8; de most significant byte heeft daarbij een inhoud van 0x00
Kijken we nu naar de display van (gdb) x/4xb array , dan zien we dat het least
significant byte op het laagste adres (0xbfd71a48) en het most significant byte op
het hoogste adres (0xbfd71a4b) staat.
Het tweede getal in het array is -5064. Negatieve getallen worden in 2’scomplement in het geheugen gezet.
De 2’s-complement waarde van een negatief getal is te bepalen door de bits van
het positieve getal te inverteren en er 1 bij op te tellen.
De binaire waarde van +5064 in 32-bits (4 bytes) is: %0000 0000 0000 0000
0001 0011 1100 1000
De geïnverteerde waarde hiervan is:
%1111 1111 1111 1111 1110
1100 0011 0111
Een hierbij opgeteld, levert op:
%1111 1111 1111 1111 1110
1100 0011 1000
Dit is hexadecimaal: 0xFFFFEC38. Vergelijk dit met wat in het geheugen is gezet
(zie vorige bladzijde):
0xbfd71a4c: 0x38 0xec 0xff 0xff en konstateer dat het getal -5064
inderdaad in 2’s-complement notatie en in de volgorde van “Little Endian” in het
geheugen staat.
Tevens leer je hieruit dat je met de debugger tot op bitniveau kunt zien hoe code
en data in het geheugen staan.
Keren we nu weer terug naar het verder debuggen van bugje. We waren
gebleven bij het in het geheugen zetten van de twee getallen uit het array. De
debugger staat nog steeds te wachten op regel 5, zie bovenaan deze bladzijde.
Als we nu de waarde van i afdrukken, levert dat het volgende op:
(gdb) p i
$1 = -1207007120
i is nog niet geïnitialiseerd, wat logisch is, omdat we nog niet de for lus zijn
binnen gegaan.
Na een volgend step commando is dit wel gebeurd:
(gdb) s
7 array[i]=0;
(gdb) p i
$2 = 0;
(gdb) p array
$3 = {5064, -5064}
(gdb) s
5 for (i=0; i<=2;i++)
(gdb) s
7 array[i]=0;
(gdb) p i
$4 = 1
(gdb) p array
$5 = {0, -5064}
(gdb) s
5 for (i=0; i<=2;i++)
(gdb) s
7 array[i]=0;
(gdb) p i
$6 = 2
(gdb) p array
$7 = {0, 0}
(gdb) s
5
for (i=0; i<=2;i++)
(gdb) p i
$8 = 0
Uit de vorige regel blijkt dat i weer nul is geworden, terwijl deze 3 had moeten
zijn. Hier zit de oorzaak van de bug. Probeer af te leiden wat er hier nu precies
gebeurt. (Bekijk nog eens de adressen van array en i op blz. 4!)
Bij de voorgaande debugsessie zijn we stap voor stap door de code gelopen, en
hebben zo de bug ontdekt.
Stel dat i gelopen zou hebben tot 100, dan zou het stap voor stap door de code
lopen erg veel tijd kosten en daarmee een ongeschikte manier van debuggen
zijn.
In dergelijke situaties maken we gebruik van de mogelijkheid om ergens in de
code een breakpoint te zetten. Vervolgens laten we het programma met volle
snelheid het programma uitvoeren tot aan het breakpoint, waar het stopt.
We kunnen nu b.v. een breakpoint zetten op regel 7 van bugje, met het
commando:
(gdb) b 7
Breakpoint 2 at 0x80483bc: file bugje.c, line 7.
Voordat we verder gaan bekijken we eerst de inhoud van i en van het array:
(gdb) p i
$1 = -1207936912
(gdb) p array
$2 = {-1076813304, 134513673}
Zowel i als het array zijn nog niet geïnitialiseerd. Dit klopt want we staan nog stil
bij het eerste statement!
(We gaan uit van de beginsituatie na het commando start , zie blz.4)
Met het commando continue, kunnen we nu het programma uitvoeren tot aan het
breakpoint op regel 7.
Daarna bekijken we weer de inhoud van i en van array.
(gdb) c
Continuing.
Breakpoint 2, main () at bugje.c:7
7 array[i]=0;
(gdb) p i
$3 = 0
(gdb) p array
$4 = {5064, -5064}
En opnieuw tot aan het breakpoint:
(gdb) c
Continuing.
Breakpoint 2, main () at bugje.c:7
7 array[i]=0;
(gdb) p i
$5 = 1
(gdb) p array
$6 = {0, -5064}
En opnieuw tot aan het breakpoint:
(gdb) c
Continuing.
Breakpoint 2, main () at bugje.c:7
7 array[i]=0;
(gdb) p i
$7 = 2
(gdb) p array
$8 = {0, 0}
Vervolgens nog een keer stappen:
(gdb) s
5 for (i=0; i<=2;i++)
(gdb) p i
$9 = 0
Vergelijk bovenstaande sessie met die op blz. 6 en stel vast dat dit sneller tot het
resultaat i = 0 leidt! In dit geval is het verschil in aantal stappen nog niet erg
groot, omdat het hier om een erg klein programma gaat.
Als de teller in de for lus tot b.v. 1000 loopt, is boven beschreven wijze van
debuggen nog steeds niet de aangewezen manier.
In zo’n geval moeten we werken met een voorwaardelijk breakpoint. Dat
betekent dat we een boolean expressie combineren met een breakpoint. Alleen
als de expressie true is, wordt er gestopt op het breakpoint.
In bovenstaand voorbeeld willen we alleen stoppen op regel 7 als i gelijk is aan
2. Dat gaat dan als volgt in z’n werk (na het commando start):
Breakpoint 1 at 0x80483a5: file bugje.c, line 4.
Starting program:<padnaam>/bugje
main () at bugje.c:4
4
int array[2]={5064,-5064},i;
De syntax van het commando om voorwaardelijk te stoppen op regel 7 als de
variabele i gelijk is aan 2, ziet er als volgt uit:
(gdb) b 7 i i==2
Breakpoint 2 at 0x80483bc: file bugje.c, line 7.
(gdb) c
Continuing.
Breakpoint 2, main () at bugje.c:7
7
array[i]=0;
(gdb) p i
$1 = 2
(gdb) p array
$2 = {0, 0}
(gdb) b
Note: breakpoint 2 also set at pc 0x80483bc.
Breakpoint 3 at 0x80483bc: file bugje.c, line 7.
Om vanaf nu via het step commando verder te gaan, moet het voorwaardelijk
breakpoint worden gedisabled. Dat gebeurt met het commando:
(gdb) disable 3
Merk op dat bij het disable commando niet het regelnummer, maar het
administratieve nummer van de debugger moet worden gebruikt, zoals bij het
commando b wordt getoond!
(gdb) b
Note: breakpoints 2 and 3 (disabled) also set at pc 0x80483bc.
Breakpoint 4 at 0x80483bc: file bugje.c, line 7.
(gdb) s
5
for (i=0; i<=2;i++)
(gdb) p i
$3 = 0
(gdb) p array
$4 = {0, 0}
(gdb) quit
The program is running. Exit anyway? (y or n) y
Duidelijk mag zijn dat dit een zeer efficiënte manier van debuggen is.
Tot slot behandelen we nog een andere manier om een programma te debuggen:
de watch. Daarmee is het mogelijk om het programma te stoppen, telkens als
een variabele of de waarde van een expressie verandert.
Dat kan dan b.v. op de volgende manier:
(gdb) start
Breakpoint 1 at 0x80483a5: file bugje.c, line 4.
Starting program:<padnaam>/bugje
main () at bugje.c:4
4
int array[2]={ 5064,-5064},i;
(gdb) watch i
Hardware watchpoint 2: i
(gdb) c
Continuing.
Hardware watchpoint 2: i
Old value = -1207314320
New value = 0
0x080483ba in main () at bugje.c:5
5
for (i=0; i<=2;i++)
(gdb) c
Continuing.
Hardware watchpoint 2: i
Old value = 0
New value = 1
0x080483cb in main () at bugje.c:5
5
for (i=0; i<=2;i++)
(gdb) c
Continuing.
Hardware watchpoint 2: i
Old value = 1
New value = 2
0x080483cb in main () at bugje.c:5
5
for (i=0; i<=2;i++)
(gdb) c
Continuing.
Hardware watchpoint 2: i
Old value = 2
New value = 0
main () at bugje.c:5
5
for (i=0; i<=2;i++)
(gdb) quit
The program is running. Exit anyway? (y or n) y
Tot slot nog enkele opmerkingen en tips.
De adressen, zoals getoond door de debugger, in de verschillende situaties
hierboven, hangen niet altijd zo samen als je zou verwachten. Dat komt omdat
de voorbeelden op verschillende tijdstippen in experimenten met de debugger
zijn ontstaan.
In bovenstaand voorbeeld, hebben we steeds een breakpoint gezet op regel 7:
array[i]=0; Misschien heb je je afgevraagd waarom niet op regel 5: for (i=0;
i<=2;i++) , wat een minstens zo geschikte plek lijkt om te stoppen als regel 7.
Het zetten van een breakpoint op een for lus, is echter niet handig: de code voor
deze lus op machineniveau (zie blz. 3 voor de liefhebbers) bestaat uit drie
gedeeltes. Een initialisatie deel: i=0, een evaluatiedeel (stopcriterium): i<2 en een
deel dat zorgt voor ophoging of verlaging: i++
Zet je nu een breakpoint op de for lus, dan zal deze geplaatst worden bij het
initialisatiedeel. Dat betekent dat er slechts een keer wordt gestopt bij dit
breakpoint, namelijk alleen de eerste keer als de teller wordt geïnitialiseerd (ga
dit na!).
Als je een commando, b.v. (gdb) c hebt ingevoerd, kun je dit commando
herhalen door simpelweg een return in te voeren.
Als je een debugsessie wilt opslaan, kan dat door aan het begin van die sessie
het commando (gdb) set logging on te gebruiken.
Alle output van de debugger (behalve de ingevoerde commando’s) wordt dan
weggeschreven naar een file gdb.txt in de working directory.
Wil je de commando’s ook in gdb.txt schrijven, kun je gebruik maken van het
commando echo \(gdb) s\n
alvorens je het echte commando (gdb) s geeft.
Als je een programma hebt geschreven dat gebruik maakt van parameters via de
commandline op basis van het argc,argv[] mechanisme, kun je met een speciale
optie de debugger aanroepen om binnen de debugger de
commandlineparameters te kunnen zien.
Stel dat de executable van je programma prog heet en een filenaam file.txt
meekrijgt op de commandline, dus opgestart wordt met: ./prog file.txt
Je roept de debugger nu als volgt aan: gdb - -args prog file.txt In de debugger
kun je nu de parameter file.txt zien met het commando: (gdb) p argv[1]
Als laatste een dubbele tip: wil je succesvol je programma kunnen debuggen is
het enerzijds nodig dat je om kunt gaan met deze tool. Dus regelmatig gebruiken
en er ervaring mee opdoen.
Anderzijds is het heel belangrijk dat je een ontwerp van je programma hebt
gemaakt middels PSD of pseudocode, waarop je code is gebaseerd.
Op basis van zo’n ontwerp kun je veel efficiënter en sneller bugs in je code
opsporen en elimineren.
DDD: een lokale debugging sessie
In dit hoofdstuk voeren we een debugging sessie uit met gdb in compbinatie met
ddd op een filterdemonstratieprogramma. Het programma is in C++ geschreven
en maakt gebruik van de grafische toolkit wxWidgets :
(http://www.wxwidgets.org ).
Om te beginnen met een debugging sessie zorgen we ervoor dat we een vers
uitvoerbaar bestand hebben. Daarna starten we de debugger met toevoeging
van de naam van het uitvoerbaar bestand:
Figuur 1: ddd opstarten
Na opstart van ddd zijn er drie enkele velden te observeren, die al dan niet aan
of uitgezet kunnen worden:
• Het broncode veld, waarin de code van het programma te zien is.
• Het assembly veld, waarin de corresponderende assembly te zien is.
• Het commandovenster, waarin de gdb commando's staan die worden
uitgevoerd door ddd namens de gebruiker.
• Het datastructurenveld, waarin tijdens programmauitvoering data wordt
afgebeeld.
Daarnaast is er en los commando console venster, waarin de belangrijkste
commando's staan vermeld. Het is handiger om dit commando console venster
onder te brengen in het DDD hoofdvenster – dit kan worden aangegeven bij de
Voorkeuren.
Bij een lokale target sessie draaien zowel gdb/ddd als het te testen programma
onder controle van het Operating System. Dit betekent dat de
programmauitvoering uit staat bij start van de debug sessie. Om te beginnen met
de uitvoering moet in de commandoconsole op 'Run' worden gedrukt, of in het
commandovenster run worden ingegeven. Dit dualisme is op alle mogelijkheden
in DDD van toepassing; alle GDB commando's kunnen zelf worden ingetoetst, of
uitgevoerd via de DDD grafsiche schil.
Als het programma wordt uitgevoerd, dan loop het alsof de debugger niet
aanwezig is. Er volgt pas een onderbreking als:
• De gebruiker ergens in de broncode (hetzij hogere taal/ hetzij assembly) een
breakpunt heeft aangebracht.
• Het programma door een fout stopt of eindigt.
Een breakpunt wordt wordt aangebracht door de cursor op een programmaregel
te brengen en dan dmv de rechter muisknop aan te geven dat er een (tijdelijk of
permanent) breakpunt moet komen.
Op het moment dat de programmaexecutie bij het breakpunt aankomt, zal de
uitvoering stoppen en alle controle over het programma worden overgegeven
aan GDB. Hieronder is zo'n moment aangegeven:
dmv de pijl in het C++ venster is te zien waar de Program Counter (PC) van de
processor zich nu bevindt. Via een extra scherm (dat op te vragen is via de
menuoptie 'Status/Backtrace' geeft de debugger aan via welke funktieaanroepen
we zijn aangeland in de huidige funktie. Door een van de regels in het backtrace
venster aan te klikken, krijgen we desgewenst ook de broncode van die plek te
zien.
Inspectie van processor en variabelen
We kunnen nu alles inspecteren wat van belang is bij programmaverloop:
• lokale (stack) variabelen.
• Funktie aanroep variabelen (via stack of registers)
• Globale (heap) variabelen
• processorregisters
• geheugenlokaties
• Unix signalen
De gewenste informatie kan bij DDD op drie verschillende manieren worden
gepresenteerd:
• via de GDB command shell. Dit is identiek aan de hiervoor beschreven gdb
command-line sessie.
• via het grafische data venster
• via een extern grafisch programma.
Als we bijvoorbeeld via de GDB command shell informatie willen printen, dan
wordt het info commando gebruikt. Een uitgebreide lijst van info geeft de
volgende mogelijkheden:
info address -- Describe where symbol SYM is stored
info all-registers -- List of all registers and their contents
info args -- Argument variables of current stack frame
info auxv -- Display the inferior's auxiliary vector
info breakpoints -- Status of user-settable breakpoints
info catch -- Exceptions that can be caught in the current stack frame
info classes -- All Objective-C classes
info common -- Print out the values contained in a Fortran COMMON block
info copying -- Conditions for redistributing copies of GDB
info dcache -- Print information on the dcache performance
info display -- Expressions to display when program stops
info extensions -- All filename extensions associated with a source language
info files -- Names of targets and files being debugged
info float -- Print the status of the floating point unit
info frame -- All about selected stack frame
info functions -- All function names
info handle -- What debugger does when program gets various signals
info line -- Core addresses of the code for a source line
info locals -- Local variables of current stack frame
info macro -- Show the definition of MACRO
info mem -- Memory region attributes
info program -- Execution status of the program
info registers -- List of integer registers and their contents
info scope -- List the variables local to a scope
info selectors -- All Objective-C selectors
info set -- Show all GDB settings
info sharedlibrary -- Status of loaded shared object libraries
info signals -- What debugger does when program gets various signals
info source -- Information about the current source file
info sources -- Source files in the program
info stack -- Backtrace of the stack
info symbol -- Describe what symbol is at location ADDR
info target -- Names of targets and files being debugged
info terminal -- Print inferior's saved terminal status
info threads -- IDs of currently known threads
info tracepoints -- Status of tracepoints
info types -- All type names
info udot -- Print contents of kernel ``struct user'' for current child
info variables -- All global and static variable names
info vector -- Print the status of the vector unit
info warranty -- Various kinds of warranty you do not have
info watchpoints -- Synonym for ``info breakpoints''
info win -- List of all displayed windows
Om het DDD data display venster te gebruiken, klikken we op een
variabelenaam in het hogere taalvenster en vervolgens op Display in de
commando console.
Speciale gevallen zoals alle lokale variabelen kunnen automatisch worden
weergegeven via de menuoptie 'data/local variables'.
In het display venster kan bij pointer variabelen ook de plaats, waarnaar de
pointer wijst, worden weergegeven. De link tussen pointer en lokatie wordt ook
aangegeven met een pointer (pijl).
Structuren en Unions in C/C++ worden weergegeven met alle velden.
Onderstaand voorbeeld geeft een aantal mogelijkheden weer:
In bovenstaande afbeelding wordt de lokatie waar de arg variabele filter naar
wijst apart afgebeeld (met label *filter). Daarnaast wordt ook de inhoud waar de
pointer variabele filterTeller naar wijst apart afgebeeld.
Ook is te zien wat de waarde is van alle AMD Athlon registers op mijn computer.
Dit is via de menuoptie 'status/registers/ aangezet.
Datastructuren via extern grafisch programma
DDD kan informatie uit het programma doorgeven aan een extern grafisch
programma. De meest geschikte kandidaat daarvoor is gnuplot. In menuoptie
'preferences/helpers' moet dit correct aangeven zijn. Door selectie van een
variabele en vervolgens keuze van de knop 'plot' wordt de data naar gnuplot
gestuurd.
Een voorbeeld is het volgende, waarin het gefilterde signaal uit het
filterprogramma via gnuplot wordt gevisualiseerd. Dit is erg handig bij grote
hoeveelheden meetdata, omdat dan verbanden veel sneller kunnen worden
geobserveerd. Bij remote targets is het helemaal handig, omdat data op een
remote target vaak lastig op te slaan is.
Onderbreking programmaverloop
GDB kan op een lokale target op de volgende manieren met
programmauitvoering omgaan:
•
•
•
aanhaken bij de start van een programma (zoals hiervoor in het voorbeeld).
Aanhaken bij een reeds lopend programma.
Binnen een programma aanhaken bij verschillende threads.
Processen aanhaken
Als ons programma los draait, dan heeft het een eigen proces nummer in Unix.
Hieronder is te zien hoe het filterprogramma apart draait (is opgestart uit de
shell). Kijk vooral goed naar de procesnummers die bij mijn login horen :
Nu geven we gdb opdracht om aan te haken bij proces 1564. Dit gaat als volgt:
• Eerst laden we het ELF bestand in DDD waarin de debug broncode aanwezig
is (menu: File/Open Program).
• Vervolgens haken we aan het process aan (menu 'File/Attach' optie).
Daarna kunnen op precies dezelfde manier met debugging te werk gaan als
hierboven al beschreven.
Houd er rekening mee dat bij Unix processen die via fork() tot stand gekomen
zijn vaak ook alle geheugenadressen dezelfde waarden zullen hebben. Denk
echter niet dat dit daadwerkelijk dezelfde adressen zijn. Het getoonde adres is
een virtueel adres dat bestaat binnen de geheugenruimte van het proces. Binnen
de kernel van het operating system wordt dit naar de MMU (Memory
Management Unit) gestuurd, die dit mapt naar een fysiek RAM adres.
Threads aanhaken
Binnen DDD kan men een thread (een zogenaamd light weight process)
selecteren via de menuoptie 'status/threads'. Breakpunten en data kunnen dan
per thread worden gezet/geevalueerd.
Programmaverloop
Binnen een proces of thread kan men het programmaverloop op de volgende
manieren laten gebeuren:
•
•
•
•
•
op volle snelheid tot een breakpunt. Dit wordt met het run of cont commando
bewerkstelligd.
Na stop door een breakpunt, stappend per hogere taalinstructie door de code.
Eventuele subroutines onderweg worden ingegaan. Dit wordt met het step
commando gerealiseerd.
Na stop door een breakpunt, stappend per hogere taalinstructie door de code.
Eventuele subroutines onderweg worden uitgevoerd maar niet ingegaan. Dit
wordt met het next commando gerealiseerd.
Na stop door een breakpunt, stappend per assemblyinstructie door de code.
Eventuele subroutines onderweg worden ingegaan. Dit wordt met het stepi
commando gerealiseerd.
Na stop door een breakpunt, stappend per assemblyinstructie door de code.
Eventuele subroutines onderweg worden uitgevoerd maar niet ingegaan. Dit
wordt met het nexti commando gerealiseerd.
Een voorbeeld van stepi (assembly is aangezet via menuoptie view/machine
code window) :
met stepi verspringt de PC in het assembly venster en daarnaast onregelmatig
de PC in het C++ venster.
DDD: een remote debugging sessie
Een remote debugging sessie gaat in grote lijnen op dezelfde manier als een
lokale debugging sessie. We moeten echter er voor zorgen dat GDB kan
communiceren met het remote target. Hiervoor is in de meeste gevallen extra
hardware nodig. Voorbeelden:
• De Atmel AVR kan met behulp van de JTAG debugger vanaf een werkstation
worden bestuurd.
• Een ARM processor heeft In Circuit Emulation ondersteuning (ICE) aan boord,
die dmv JTAG wordt bestuurd.
Als voorbeeld wordt hier een Atmel AT91 SAM7S ontwikkelbord gebruikt, dat
dmv een ICE debugger (Ronetix Peedi, http://www.ronetix.at) aan het FreeBSD
werkstation hangt.
De set-up is dan als volgt:
FreeBSD
Host
GDB
socket
socket
ICE debugger
jtag
Atmel AT91SAM7S
ARM7TDMI
jtag
De ICE debugger wordt apart bestuurd, in dit geval dmv een telnet sessie. Dit
wordt hier verder niet behandeld.
Er moet worden gezorgd dat:
• het uitvoerbaarbestand voor de target klaar is met debugging informatie.
• DDD wordt opgestart met koppeling aan een remote versie van de debugger,
in plaats van de debugger voor de host. In dit voorbeeld heet GDB arm-elf-gdb
in plaats van gdb. arm-elf-gdb is vanaf broncode gecompileerdmet als optie
dat het met arm elf instructies moet kunnen omgaan.
• GDB (arm-elf-gdb) moet de internetadres aangegeven krijgen waarop het kan
communiceren met de ICE.
GDB kan worden geinstrueerd dmv het remote <adres> commando waar het
target is. In onderstaand voorbeeld is dat op IP adres 192.168.84.5:4242 .
er is nu een remote debugging sessie opgezet. Deze is vrijwel gelijk aan een
host gebaseerde sessie. Verschillen zijn onder andere:
• Op een remote target loopt het programma al bij begin van de sessie. Het run
commando kan daarom niet worden gebruik. Willen we vanaf een HW reset
verder gaan (zoals hierboven, vanaf adres 0x00000000) dan volstaat het cont
commando.
• het aantal breakpunten wordt beperkt door de hardware. In het geval van de
ARM7TDMI ICE worden, bij debugging van code in flash, slechts twee
breakpunten ondersteund. Bij debugging in sram vervalt deze beperking.
• De snelheid waarmee door het programma kan worden gelopen hangt in grote
mate af van de verbinding tussen host en target. Een seriële verbinding, zoals
tussen Atmel AVR JTAG en werkstation, is traag! Het inladen van grote
structuren of data kost vele seconden. De Peedi ICE debugger van de ARM
werkt met een ethernet of USB verbinding en is veel sneller.
Als laatste wordt hierboven de ARM sessie afgebeeld met een backtrace venster
en de ARM 7TMDI registers afgebeeld. Deze verschillen natuurlijk compleet van
de i386 registers zoals afgebeeld bij de lokale sessie eerder.
Tot slot het volgende: Heel veel succes in de omgang met de GNU/DDD
-debugger!
Arnhem, Maart 2010
E.J Boks en H. Muileman
Download