Introduktion till GDB

1. Varför ska jag läsa detta? (eller: "Hjälp, mitt program krashar!")

När ett program kraschar lämnar det inte speciellt mycket information om vad som gått snett, utan det skriver bara ut "Segmentation fault". Detta betyder att programmet har försökt använda minne som den inte får, och beror oftast på pekarfel; oinitierade pekare, pekare till borttaget minne eller att pekare används i en loop som går ett steg för långt.

För att fixa detta kan man göra på några olika sätt

Den första metoden är inte speciellt effektiv om man inte är väldigt duktig på att läsa kod, samt att det är svårt att hitta fel i det som man själv har skrivit (det är därför det verkar så lätt för kompisen/handledaren att hitta felet, de ser på koden med en annan utgångspunkt).

De två andra metoderna handlar om att lokalisera var problemet uppstår, genom att avgränsa området som behövs undersökas. Nackdelen med att lägga in debugutskrifter är att koden lätt blir oöverskådelig samt att det tar extra tid att skriva in dem; Oftast sker användningen i följande steg:
krasch -> lägg in debug -> krasch -> lägg in mer debug -> krasch -> lokalisera/fixa felet ->ta bort debugutskrifter
Det vore klart enklare om man kunde se direkt vad som hänt, och varför. Eftersom programvarufel har plågat programmerare i alla tider så har verktyg skapats för syftet att kunna se vad som händer i programmet hela tiden, så kallade avlusningsprogram eller debuggers.

GNU projektet som utvecklat bl.a. kompilatorn gcc har även gjort gdb, som är en förkortning av Gnu DeBugger, och det är denna debugger vi kommer använda oss av. Det finns även ett program som förenklar användningen av gdb med ett grafiskt gränssnitt som heter DDD (DataDisplayDebugger), men denna introduktion koncentrerar sig på gdb.

En annan anledning att spendera några timmar på att lära sig en debugger är att det återbetalar sig i väldigt stor grad. Tid är den resurs vi har minst av, och genom att själv kunna fixa sina buggar så sparar du tid åt dig själv, eftersom du inte behöver vänta på att handledaren ska komma, du sparar tid åt handledaren som inte behöver göra samma sak hela tiden, du sparar tid åt din kamrat som inte behöver vänta så länge på dig när du får hjälp. Vore inte du glad om handledningen gick fort för den som är före dig i kön? :)

2. Använda GDB (eller: "Jaja, jag är med så långt, hur gör man?")

För att gdb ska fungera bättre (kunna presentera källkod, argument till funktioner, radnummer) så ska kompileringen ske med debuginformation i sitt program, detta görs genom att använda flaggan -g till kompilatorn
  $ gcc -g list.cc -o list
      
För att sedan starta debuggern skrivs
  $ gdb ./list
      
Varpå debuggern svarar med
GNU gdb 19990928
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...
(gdb) 
      
Sist skriver gdb ut en s.k. prompt, liknande den som finns i kommandoskalet ("Terminalen"). Här skrivs de kommandon som man vill använda. Du kan som i kommandoskalet även använda pil-upp och ner för att använda gamla kommandon, TAB för att expandera kommandon eller funktionsnamn. Return kör det senast använda kommandot, vilket förenklar mycket när man stegar sig fram (dvs, istället för att skriva "step" 10 gånger kan man skriva det en gång och trycka på Return nio gånger).

2.1 Länkad Lista Exempel

När vi nu har fått igång gdb, hur ska vi få den att göra det vi vill, dvs hitta felet? För att visa vad gdb skriver ut och för att ge en möjlighet för Dig att följa med så har ett exempelprogram skapats, i form av en buggig länkad lista. Den finns här. Den innehåller klassen List med funktionerna put(), size(), print() och en mainfunktion som använder sig av listan. Först vill vi få programmet att krascha, så vi startar det med kommandot "run"
(gdb) run
Starting program: /a/home/alfs/WebDocs/gdb/./list1 
Storlek: 0

Program received signal SIGSEGV, Segmentation fault.
0x8048731 in List::put (this=0xbffffa68, element=5, pos=0) at list1.cc:51
51        ny->next = cur->next;
(gdb) 
      
Vad får vi reda på här? Vi ser att programmet har stannat på adress 0x8048731 i minnet (detta är inte så användbart såvida vi inte debuggar assemblerkod), vilket är i vår put funktion, närmare bestämt rad 51. Vi får även se vilka argument vi skickade med till put. Så varför krashade programmet? Tydligen har det något att göra med rad 51, så vi tittar efter vad ny och cur pekar på.

Till detta används kommandot print

(gdb) print ny
$1 = 0x8049b70
(gdb) 
      
Jaha, det sa inte så mycket, en adress i minnet. Kan det vara rätt? ny är en pekare, så den bör innehålla en minnesadress. Vi kan titta på vad som finns i denna adress på samma sätt som vi skulle gjort i C; Via dereferensiering med *
(gdb) print *ny
$2 = {data = 5, next = 0x0}
(gdb) 
      
Detta ser ju bra ut, hur är det med cur då?
(gdb) print *cur
Cannot access memory at address 0x0.
(gdb) 
      
Ajaj, här är orsaken till kraschen. Vi har en pekare som pekar på NULL, och detta minnesområde får vi inte titta på. Vart kommer denna NULL ifrån? Med hjälp av kommandot list så får vi upp källkoden runtomkring problemet

(gdb) list
46        while(count-- > 0) { prev = cur; cur = cur->next; }
47      
48        ny = new nod;
49      
50        ny->data = element;
51        ny->next = cur->next;
52        
53        if (pos == 0)
54          head = ny;
55        else
(gdb) 

      
Vi ser inte riktigt var cur tilldelas, så vi använder list 46 för att titta på koden runt rad 46

(gdb) list 46
41        pnod cur, prev, ny;
42        int count;
43      
44        cur = head;
45        count = pos;
46        while(count-- > 0) { prev = cur; cur = cur->next; }
47      
48        ny = new nod;
49      
50        ny->data = element;
(gdb) 

      
Aha. cur är samma som head till att börja med, och räknas sedan fram i loopen. Men eftersom count = pos = 0 så kommer vi aldrig in i loopen, så vi undersöker hur head ser ut
(gdb) print head
$1 = 0x0
(gdb) 
    
Okej, head är också NULL, och det är som det ska vara eftersom listan är tom.

Vad är felet då? Tydligen kan vi inte använda rad 51 om vi har en tom lista, så vi måste flytta ner denna rad till testet för specialfallet (rad 53--56) och får följande

  if (pos == 0) {
    ny->next = head;
    head = ny;
  } else {
    ny->next = cur->next;
    prev->next = ny;
  }
    
Det färdiga programmet kan hämtas här

2.2 Vanliga kommandon

Nedan presenteras de mest användbara kommandona

2.2.1 Exempel

Break
Break kan användas på funktionsnamn ...
(gdb) break List::put
Breakpoint 1 at 0x80486de: file list1.cc, line 44.
(gdb) 
      
... eller radnummer
(gdb) break 92
Breakpoint 1 at 0x80487ee: file list1.cc, line 92.
(gdb) 
      
backtrace
(gdb) backtrace
#0  List::put (this=0xbffffa68, element=5, pos=0) at list1.cc:44
#1  0x8048855 in main () at list1.cc:96
(gdb) 

      
step/next
Notera skillnaden mellan att använda next och step; Först next:

(gdb) break main
Breakpoint 1 at 0x80486a2: file ex1.cc, line 12.
(gdb) run
Starting program: /a/home/alfs/WebDocs/gdb/./ex1 

Breakpoint 1, main () at ex1.cc:12
12        a = 2;
(gdb) next
13        b = kvadrat(a);
(gdb) next
14        cout << "Kvadraten på " << a << " är " << b << endl;
(gdb) 

      
Här "hoppade vi över" kvadratfunktionen. Vi kanske vet att den fungerar okej, eller vill av annan anledning inte se vad som finns i den. Använder vi step kommandot kommer vi istället se vad som händer inuti kvadrat-funktionen, som synes nedan.
(gdb) break main
Breakpoint 1 at 0x80486a2: file ex1.cc, line 12.
(gdb) run
Starting program: /a/home/alfs/WebDocs/gdb/./ex1 

Breakpoint 1, main () at ex1.cc:12
12        a = 2;
(gdb) step
13        b = kvadrat(a);
(gdb) step
kvadrat (x=2) at ex1.cc:5
5         y = x*x;
(gdb) step
6         return y;
(gdb) step
7       }
(gdb) step
main () at ex1.cc:14
14        cout << "Kvadraten på " << a << " är " << b << endl;
(gdb) 

      

Andra bra tips

En annan flagga till kompilatorn som är bra att använda är -Wall.
  $ gcc -Wall list.cc -o list
      
Ofta kan man genom att fixa dessa varningar lösa många problem.

Istället för att skriva "break", "step", "run" kan man förkorta dem till "b", "s", "r". Det finns förkortningar för de flesta kommandon, normalt räcker det med första bokstaven, men börjar många kommandon på samma bokstav måste man ange tydligare (dvs med fler bokstäver) vilket kommando som åsyftas.

Framtida arbete (Eller: Var detta allt?!)

Jag har tänkt att uppdatera denna introduktion med ett större exempel, som studenten själv får hitta felen i, tillsammans med ett "facit" om det blir för svårt.

Vill du veta mer om gdb, så kan du använda kommandot help.

Mer uttömmande information fås genom att skriva $ info gdb i kommandoprompten, alternativt Alt-x info i emacs.


Stefan Alfredsson
Last modified: Thu Aug 17 18:09:27 CEST 2000
Denna text finns online på http://www.cs.kau.se/~alfs/gdb/

$Id: gdb.html,v 1.2 2000/05/04 13:25:05 alfs Exp alfs $