Denne opgave går ud på at lære hvordan man debugger et program. Vi tager udgangspunkt i GDB, da den er tilgængelig på alle platforme.

Denne guide er skrevet på en Windows-maskine med MinGW; det nøjagtige output vil sandsynligvis se anderledes på din computer, men du bør stadig kunne følge med hele vejen.

Debugging af et crash

Start med at kopiere test.c og kompiler filen som normalt. Du finder test.c (og de andre test filer) i zip filen, som følger med denne tutorial. Forsøg herefter at køre programmet, og indtast et vilkårligt tal - efter du har gjort dette, skulle programmet gå ned - på Windows vil der normalt komme en "Programmet er holdt op med at virke" besked, mens Linux m.v. vil printe "Segmentation fault". Dette er ikke i sig selv ret sigende, så vi vil gerne prøve at få lidt flere oplysninger ud af computeren.

Før vi kan gøre dette, skal vi dog lige have inkluderet lidt debugoplysninger i vores program. Dette gøres under kompilering med parameteren -g, eks. gcc -g test.c -o test. Genkompiler programmet med denne parameter.

Nu kan vi så prøve at køre vores program gennem gdb. Skriv gdb test for at starte gdb og indlæse programmet test. Du vil så få et output der ser nogenlunde sådan ud:

C:\impr>gdb test
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\test.exe...done.
(gdb) 

GDB er et interaktivt program, og venter på en kommando. Lige nu vil vi bare gerne køre programmet, så skriv run. Programmet vil starte, og bede om et tal som før - skriv noget vilkårligt. Herefter vil programmet som før gå ned, men denne gang griber GDB ind og giver dig nogle oplysninger:

(gdb) run
Starting program: C:\impr\test.exe
[New Thread 2516.0x1518]
Enter a number: 1234

Program received signal SIGSEGV, Segmentation fault.
0x75edecc0 in strxfrm_l () from C:\Windows\syswow64\msvcrt.dll
(gdb)

GDB fortæller os her følgende ting:

Nu ved vi altså hvor programmet går ned, men vi ved stadig ikke hvordan vores kode er kommer derhen. Vi skal derfor bruge bt-kommandoen for at se call stacken - en liste over de funktionskald der er i gang:

(gdb) bt
#0  0x75edecc0 in strxfrm_l () from C:\Windows\syswow64\msvcrt.dll
#1  0x75f36896 in putws () from C:\Windows\syswow64\msvcrt.dll
#2  0x75f62900 in tzname () from C:\Windows\syswow64\msvcrt.dll
#3  0x00403075 in __register_frame_info ()
#4  0x75f36909 in scanf () from C:\Windows\syswow64\msvcrt.dll
#5  0x004013c2 in main () at test.c:8

I dette tilfælde er det først nede ved #5 at vi har vores kode. Bemærk at der står test.c:8 - dette betyder linje 8 i test.c, og denne oplysning er tilgængelig fordi vi kompilerede vores program med -g.

Vi kan bevæge os igennem call stacken ved at bruge up og down kommandoerne. Disse kommandoer tager evt. en parameter der siger hvor mange niveauer vi skal bevæge os. I vores tilfælde er vi lige nu ved #0, og vi vil gerne bevæge os op til #5, da det er den kode vi har kontrol over:

(gdb) up 5
#5  0x004013c2 in main () at test.c:8
8         scanf("%d", i);

Bemærk at GDB endda viser os den linje vi har med at gøre. Herfra kan vi så se præcist hvad fejlen er - der mangler et & før i!

Nu hvor vi ved hvad fejlen er, kan vi afslutte GDB, så vi kan gå ud og rette fejlen. Til dette bruges kommandoen quit eller blot q:

(gdb) q
A debugging session is active.

        Inferior 1 [process 2516] will be killed.

Quit anyway? (y or n)

Programmet kører stadig i baggrunden, så GDB spørger om du virkelig vil afslutte - det vil vi gerne, så hertil svares blot y.

Breakpoints

Hent og kompiler fact_wrong.c med parameteren -g. Kør programmet, der skal beregne 10! (10*9*...*2*1) ved brug af rekursion.

C:\impr>fact_wrong
10!=1

Det er selvfølgelig ikke det rigtige resultat. Kør gdb fact_wrong.

Denne gang crasher vores program ikke, så vi skal selv fortælle GDB hvornår den skal afbryde. Vi vil gerne stoppe den ved vores grundtilfælde - dvs. når n <= 1. Vi vil derfor sætte et breakpoint på linje 6, der kun køres når n <= 1, og dette gøres med kommandoen break, eller blot b:

(gdb) break fact_wrong.c:6
Breakpoint 1 at 0x401398: file fact_wrong.c, line 6.

I dette tilfælde skriver vi bare linjenummeret fordi det er samme fil som vores main-funktion ligger i, og progr - GDB finder selv ud af resten. Hvis vi havde flere filer, skulle vi også angive filnavnet, dvs. "break fact_wrong.c:6". Vi kan også skrive navnet på en funktion, og så finder GDB selv ud af resten.

Kør nu programmet som før med run. GDB vil så stoppe umiddelbart inden vores angivne linje bliver kørt:

(gdb) run
Starting program: C:\impr\fact_wrong.exe
[New Thread 1300.0x19b8]

Breakpoint 1, factorial (n=10) at fact_wrong.c:6
6           return 1;

Bemærk at GDB ikke kun fortæller os hvilken funktion vi er i, men også hvilken parameter den blev kaldt med - i dette tilfælde n=10. Vi kan også checke call stacken:

(gdb) bt
#0  factorial (n=10) at fact_wrong.c:6
#1  0x004013cb in main () at fact_wrong.c:12

Som vi kan se, så har factorial aldrig kaldt sig selv - den gik direkte ned i vores if, selvom det jo ikke var meningen. Årsagen er naturligvis at vores betingelse er forkert - der står n >= 1 i stedet for n <= 1. Hvis vi stopper GDB og retter dette, kan vi nu prøve det samme igen for at se hvordan det skulle se ud:

C:\impr>gdb fact_right
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\fact_right.exe...done.
(gdb) b fact_right.c:6
Breakpoint 1 at 0x401398: file fact_right.c, line 6.
(gdb) run
Starting program: C:\impr\fact_right.exe
[New Thread 5420.0x1484]

Breakpoint 1, factorial (n=1) at fact_right.c:6
6           return 1;
(gdb) bt
#0  factorial (n=1) at fact_right.c:6
#1  0x004013ab in factorial (n=2) at fact_right.c:7
#2  0x004013ab in factorial (n=3) at fact_right.c:7
#3  0x004013ab in factorial (n=4) at fact_right.c:7
#4  0x004013ab in factorial (n=5) at fact_right.c:7
#5  0x004013ab in factorial (n=6) at fact_right.c:7
#6  0x004013ab in factorial (n=7) at fact_right.c:7
#7  0x004013ab in factorial (n=8) at fact_right.c:7
#8  0x004013ab in factorial (n=9) at fact_right.c:7
#9  0x004013ab in factorial (n=10) at fact_right.c:7
#10 0x004013cb in main () at fact_right.c:12
(gdb)

Hvis vi nu vil lade programmet køre til ende, bruger vi kommandoen continue, eller bare c:

(gdb) c
Continuing.
10!=3628800
[Inferior 1 (process 5420) exited normally]

Stepping og debugging af variable

Hent nu test2.c, og kompiler den. Dette program skal beregne den reciprokke værdi af en integer i - altså 1/i.

Kør programmet, og indtast værdien "100":

C:\impr>test2
Enter a number: 100
Reciprocal value is 0.000000

Det ser ikke helt rigtigt ud, men det er måske ikke helt oplagt hvor problemet ligger. Vi vil derfor steppe gennem vores kode, hvilket vil sige at køre en linje ad gangen. Dette gøres med kommandoen step (eller blot s, men før vi kan bruge denne skal programmet lige startes. Vi sætter derfor et breakpoint i starten af main, og kører:

(gdb) b main
Breakpoint 1 at 0x40139a: file test2.c, line 5.
(gdb) run
Starting program: C:\impr\test2.exe
[New Thread 5756.0x183c]

Breakpoint 1, main () at test2.c:5
5         int i = 0;
(gdb)

Vi stepper nu et par gange indtil vi når en linje der ændrer på i. For at gentage en kommando skal vi blot trykke Enter en ekstra gang, så derfor kan vi køre dette:

(gdb) step
7         printf("Enter a number:\n");
(gdb)
Enter a number:
8         scanf("%f", &i);
(gdb)

Her har vi en linje der ændrer på i, så lad os lige - for en god ordens skyld - bekræfte hvad der ligger her inden vi fortsætter. Dette gøres med kommandoen print, eller blot p, efterfulgt af det udtryk vi vil evaluere.

(gdb) p i
$1 = 0
(gdb) 

Lad os så køre denne linje og se hvad der sker med i:

(gdb) step
100
10        float f = 1/(float)i;
(gdb) p i
$2 = 1120403456

Det var jo overhovedet ikke det vi skrev - så der er sket noget uventet på linje 8, ergo må fejlen være her. Ganske rigtigt, så har vi givet det forkerte format til scanf; %f er til float-variable, men i er en int, så vi får et helt andet resultat ud.

I stedet for at steppe igennem for at spore ændringer til en variabel, kan vi også sætte et såkaldt watchpoint, der breaker når et udtryk ændrer sin værdi:

(gdb) break main
Breakpoint 1 at 0x40139a: file test2.c, line 5.
(gdb) run
Starting program: C:\impr\test2.exe
[New Thread 3384.0xa5c]

Breakpoint 1, main () at test2.c:5
5         int i = 0;
(gdb) watch i
Hardware watchpoint 2: i
(gdb) c
Continuing.
Enter a number:
100
Hardware watchpoint 2: i

Old value = 19
New value = 1120403456
0x75ee1935 in msvcrt!_ctime32 () from C:\Windows\syswow64\msvcrt.dll
(gdb)

Vi kan nu som sædvanligt kigge på call stacken for at se hvor vi kommer fra:

(gdb) bt
#0  0x75ee1935 in msvcrt!_ctime32 () from C:\Windows\syswow64\msvcrt.dll
#1  0x75f0c28c in msvcrt!_ftol2_sse_excpt ()
   from C:\Windows\syswow64\msvcrt.dll
#2  0x42c80000 in ?? ()
#3  0x0028ff18 in ?? ()
#4  0x75f36896 in putws () from C:\Windows\syswow64\msvcrt.dll
#5  0x75f62900 in tzname () from C:\Windows\syswow64\msvcrt.dll
#6  0x00403074 in __register_frame_info ()
#7  0x75f36909 in scanf () from C:\Windows\syswow64\msvcrt.dll
#8  0x004013c2 in main () at test2.c:8

Hvis du bruger et IDE, vil der ofte være to forskellige "step"-funktioner; denne funktion hedder typisk "step into".

Argumenter

Nogle gange tager vores programmer input i form af kommandolinjeparametre, og disse vil vi selvfølgelig også gerne kunne debugge.

Hent args.c og kompiler programmet. Det eneste dette gør er at udskrive alle de argumenter der kommer ind til programmet:

C:\impr>args test
args
test

Hvordan debugger vi dette? Vi kan ikke give parametrene med når vi starter gdb, for så får vi denne fejl:

C:\impr>gdb args test
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\args.exe...done.
C:\impr/test: No such file or directory.

Svaret er at vi først skal give argumenterne når vi kører programmet. Dette gøres ganske enkelt ved at skrive dem som en del af run-kommandoen:

C:\impr>gdb args
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\args.exe...done.
(gdb) run test
Starting program: C:\impr\args.exe test
[New Thread 6460.0x1880]
C:\impr\args.exe
test
[Inferior 1 (process 6460) exited normally]

Vi kan selvfølgelig stadig sætte breakpoints m.v. som ellers:

(gdb) b args.c:5
Breakpoint 1 at 0x401392: file args.c, line 5.
(gdb) run 1 2 3 4
Starting program: C:\impr\args.exe 1 2 3 4
[New Thread 6724.0x10b4]

Breakpoint 1, print_string (s=0x4a1771 "C:\\impr\\args.exe") at args.c:5
5               printf("%s\n", s);
(gdb) c
Continuing.
C:\impr\args.exe

Breakpoint 1, print_string (s=0x4a1783 "1") at args.c:5
5               printf("%s\n", s);
(gdb) c
Continuing.
1

Breakpoint 1, print_string (s=0x4a1786 "2") at args.c:5
5               printf("%s\n", s);
(gdb) c
Continuing.
2

Breakpoint 1, print_string (s=0x4a1789 "3") at args.c:5
5               printf("%s\n", s);
(gdb) c
Continuing.
3

Breakpoint 1, print_string (s=0x4a178c "4") at args.c:5
5               printf("%s\n", s);
(gdb) c
Continuing.
4
[Inferior 1 (process 6724) exited normally]
(gdb)

Forbigå funktionskald

Stepping har den ulempe at den går ned i de funktioner vi kalder - nogle gange vil vi egentlig bare gå til den næste linje i den nuværende funktion. Dette kan gøres med kommandoen next eller n:

C:\impr>gdb args
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\args.exe...done.
(gdb) break main
Breakpoint 1 at 0x4013ad: file args.c, line 11.
(gdb) run 1 2
Starting program: C:\impr\args.exe 1 2
[New Thread 2512.0x418]

Breakpoint 1, main (argc=3, argv=0x3924c0) at args.c:11
11              for (i = 0; i < argc; i++)
(gdb) n
12                print_string(argv[i]);
(gdb)
C:\impr\args.exe
11              for (i = 0; i < argc; i++)
(gdb)
12                print_string(argv[i]);
(gdb)
1
11              for (i = 0; i < argc; i++)
(gdb)
12                print_string(argv[i]);
(gdb)
2
11              for (i = 0; i < argc; i++)
(gdb)
13        return 0;
(gdb)
14      }
(gdb)

Hvis vi havde brugt step, var vi gået ned i print_string-funktionen hver gang den blev kaldt.

Hvis vi i stedet vil forbigå hele løkken, skal vi bruge until (u):

C:\impr>gdb args.exe
GNU gdb (GDB) 7.4
Copyright (C) 2012 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 "i686-pc-mingw32".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from C:\impr\args.exe...done.
(gdb) break main
Breakpoint 1 at 0x4013ad: file args.c, line 11.
(gdb) run 1 2 3 4
Starting program: C:\impr\args.exe 1 2 3 4
[New Thread 7944.0x1ed8]

Breakpoint 1, main (argc=5, argv=0x3a2568) at args.c:11
11              for (i = 0; i < argc; i++)
(gdb) u
12                print_string(argv[i]);
(gdb)
C:\impr\args.exe
11              for (i = 0; i < argc; i++)
(gdb)
1
2
3
4
13        return 0;
(gdb)

Hvis du bruger et IDE, vil denne funktion typisk hedde "step over".