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