Eine kleine GDB Einführung

Diese Seite gibt eine Einführung in wie man mit GDB effizient und angenehm Programme debuggen kann.

Sollte man Fehler finden, oder ist es etwas nicht deutlich, schickt mir eine Email. Ich versuche diese Seite wann immer es geht zu verbessern, und freue ich daher über Rückmeldungen.

1. Vorbereitung

GDB ist ein Debugger, kein Compiler. Um es zu benutzen, muss das Programm erst kompiliert werden. Dabei ist es zu empfehlen, gcc oder den Compiler deiner Wahl mit dem -g3 Flag zu starten. Dieses fügt zusätzliche Informationen über das Programm in die Ausführbare Datei ein (Zeilenummern, Typen, Macro Definitionen, ...), welche später beim Debuggen helfen werden.

Falls man Optimierungen aktiviert hat (-O1, -O2, -O3, -Os, ...) sollte man diese meist deaktivieren um die Programme besser zu verstehen. Standardmäßig sind diese nicht aktiviert.

Um nun den Debugger zu starten, führt diesen Befehl in eurer Shell aus:

gdb mein-program

unter der Annahme, dass euer Programm mein-program heißt. Man sollte dieses auch dann so starten, wenn mein-program Argumente erwartet, da diese erst später angegeben werden (siehe run oder start). Hier wird nur gesagt, wie die Datei mit dem Maschinencode heißt.

Wenn man ein Programm mit festen Argumenten startet, kann es einfacher sein diese direkt beim Starten von GDB anzugeben. Hierzu führt man am besten

gdb --args mein-program argument1 argument2 "argument drei"

aus.


2. Häufige Fehlermuster

In diesem Abschnitt werden verschiedene GDB Sessions mit verschiedenen Problemen durchgespielt.

Im grauen Kasten ist immer die GDB Session selbst zu sehen, wobei das fett Geschriebene die Nutzereingabe ist. Kommentare, in Grün, wurden von mir zur Verdeutlichung hinzugefügt, und sollen nicht mit eingegeben werden. Der Quelltext der C Programme ist, eingeklappt, unterhalb der GDB Session zu finden.

Falls man einen Befehl nicht erkennt, kann man entweder unten nachschauen, oder mit der Maus über dem Befehl hovern, wodurch eine kurze Erklärung erscheinen sollte. Diese sind zum größten Teil aus der GDB Manual übernommen, mit geringen Änderungen (in GDB kann man immer help [befehlname] benutzen).

Kleine Anmerkung: Ich schreibe Tastaturkürzel nach dem Muster C-c für Steuerung und C, M-o M-a für Alt und O, O loslassen und A drücken, usw., wie man es auch in Benutzeranleitungen sieht.

Segemntation Fault

Ein Programm stürzt mit der Nachricht SEGFAULT oder Sementation Fault ab, wenn auf ungültige Speicheradressen zugegriffen wird. Mit GDB können wir leicht nachverfolgen wo und wie dieses passiert.

(gdb) run
Starting program: /home/user/code/segfault Program received signal SIGSEGV, Segmentation fault. main () at segfault.c:12 12 printf("%d\n", *ptr); (gdb) list
Alternativ mit C-x 1 1 Quelltext immer anzeigen lassen. 7 int main() { 8 size_t i; 9 int *ptr; 10 11 for (i = 0; i <= sizeof data; i++) { 12 printf("%d\n", *ptr); 13 ptr++; 14 } 15 } (gdb) print
ptr
Welchen Wert hat ptr? $1 = (int *) 0x0 Oops…

Wir erkennen: In Zeile 12 wird ptr dereferenziert, aber der Wert von ptr ist NULL. Der Fehler war das wir ptr nicht auf data gesetzt haben. Beachtet, dass der Fehler nicht immer auftreten muss. Kompiliert man mit -O0 (anstatt -O2) würde man keinen Segmentation Fault bemerken!

Oft ist GDB nicht notwendig für offensichtliche Fehler dieser Art. Tools wie Valgrind können auch angeben, in welcher Zeile Fehler dieser Art auftreten (allerdings dann nicht interaktiv).

Referenzprogram
#include <stdio.h>
#include <stdlib.h>

int data[] = {
    2, 3, 5, 7, 13, 17, 19, 23, 19
};

int main()
{
    size_t i;
    int *ptr; /* hier wurde vergessen data auf ptr zuzuweisen! */

    for (i = 0; i <= sizeof data; i++) {
        printf("%d\n", *ptr);
        ptr++;
    }

    return EXIT_SUCCESS;
}

Endlosschleife

Oft kann ein Programm länger laufen als es beabsichtigt ist. Gründe hierfür können Programmierfehler sein, die dazu führen dass die Ausführung sich in einer Endlosschleife verfängt, das Warten blockierende Systemaufrufe (oft read(2)) oder Deadlines.

In diesem Beispiel führt ein Rundungsfehler dazu, dass ein Program (siehe Referenzprogram) wesentlich länger laufen würde als es beabsichtigt ist. Wir werden dieses untersuchen, indem wir aus dem Ausführungsmodus in den Debuggingmodus wechseln mit einem C-c. In diesem angelangt können wir mit einer Kombination von next/step und print/display Befehlen den Ablauf analysieren, um das Problem besser zu verstehen:

(gdb) run
Starting program: /home/user/code/pleaseterminate C-c C-c Einfach C-c C-c in GDB eingeben. Program received signal SIGINT, Interrupt. 0x00005555555551a2 in main (argc=1, argv=0x7fffffffe788) at pleaseterminate.c:15 15 x = (x+1)/x; Hier haben wir das Programm unterbrochen. (gdb) print
i
Es ist überraschend, dass i immernoch 1 ist $1 = 1 (gdb) display
i
Wir entscheiden uns immer i auszugeben … 1: i = 1 (gdb) next … und das Programm schrittweise weiter aufzuführen. 1: i = 1 16 i /= 2; 1: i = 1 (gdb) Ein einfaches enter wiederholt next automatisch! 1: i = 0 14 for (i = 0; i < n; i++) { 1: i = 0 (gdb) 1: i = 1 15 x = (x+1)/x; 1: i = 1 (gdb) 1: i = 1 16 i /= 2; 1: i = 1 (gdb) 1: i = 0 14 for (i = 0; i < n; i++) { Irgendwo hier bemerkt man was ungewöhnliches. 1: i = 0 (gdb) 1: i = 1 15 x = (x+1)/x; Wir sehen i wechselt immer zwischen 1 und 0! 1: i = 1 (gdb) 1: i = 1 16 i /= 2; 1: i = 1 (gdb) quit
Wir haben das Problem verstanden! Verlassen wir also GDB.

Hier ist es Ganzzahlendivision gewesen, dass das Programm dazu bringt immer i zwischen 0 und 1 zu wechseln (mit genaueren Zahlen wäre es auch nicht besser, da die Folge x0 = 0; xn = (xn-1/2)+1 auch so gegen ≈ 2 konvergiert).

In diesem Beispiel ist es einfach zu sehen wo ein Programm stecken bleibt, haben wir aber mehrere schleifen, die ggf. alle stecken bleiben könnten, wird GDB sich als sehr hilfreich erweisen — und übersichtlicher als ein printf-Dschungel.

Referenzprogram
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
     int i, n;
     double x;

     /* nicht daheim nachmachen: */
     n = atoi(argc > 1 ? argv[1] : "5");

     /* inhaltlicht nicht sinnvoll! */
     x = 3.14159265359;
     for (i = 0; i < n; i++) {
          x = (x+1)/x;
          i /= 2;
     }
     printf("Final: %f\n", x);

     return EXIT_SUCCESS;
}

Probleme mit Prozessen

Wenn man mit mehreren Prozessen arbeitet, ist in C meist fork(2) gefragt. Dessen Ausführung resultiert in zwei Prozessen — Eltern und Kind. Hier werde ich jedoch kein echtes Beispiel durcharbeiten (das Konzept sollte aus den obigen Beispielen verstanden worden sein), sondern zeige nur welche Operationen für Prozesse relevant sind. Der Bug würde in der unauffällig benannten Modul nobughere auftauchen.

(gdb) break
thiswontgowrong
Breakpoint 3 at 0x555555555197: file nobughere.c, line 4. (gdb) set follow-fork-mode childDas hier ist der Trick! (gdb) run
Starting program: /home/user/a.out [Attaching after process 5334 fork to child process 5335] [New inferior 4 (process 5335)] [Detaching after fork from parent process 5334] [Inferior 3 (process 5334) detached] [Switching to process 5335] Thread 4.1 "a.out" hit Breakpoint 3, thiswontgowrong () at nobughere.c:4 4 oopstherewasabughereafterall(); ...

Hätten wird nicht follow-fork-mode auf child gesetzt, wären wir im Elternprozess geblieben, und es wäre nicht möglich die Funktion thiswontgowrong() zu debuggen.

Referenzprogram
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include "nobughere.h"

int main()
{
	pid_t pid, awaited;
	int wstatus;

	switch (pid = fork()) {
	case -1:                   /* fehler */
		return EXIT_FAILURE;
	case 0:                    /* kind */
		thiswontgowrong();
		break;
	default:                   /* eltern */
		/* fehlerbehandlung hier vernachlässigt
		* nicht robust, nicht richtig! */
		waitpid(pid, &wstatus, 0);
	}

	return EXIT_SUCCESS;
}

Probleme mit Fäden

Im folgenden Beispiel wurde versucht ein Wort-Zähler zu implementieren, welcher in mehren Fäden versucht die Standardeingabe zu lesen, und die Häufigkeit jedes Wortes in einer geteilten Datenstruktur (assoziatives Feld) zu speichern. Dazu muss der Zugriff synchronisiert werden, hier mit einem pthread_mutex_t.

Es tritt aber ein Segmentation Fault auf! Wir können thread benutzen um nun den Zustand in verschiedenen Fäden zu analysieren:

(gdb) run
< war-and-peace.txt
Gestartet mit großen Eingabe. Starting program: /home/user/code/mwc < war-and-peace.txt [New Thread 0x7ffff7d6a700 (LWP 3216091)] [New Thread 0x7ffff7569700 (LWP 3216092)] [New Thread 0x7ffff6d68700 (LWP 3216093)] Thread 2 "mwc" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7ffff7d6a700 (LWP 3216091)] __strcmp_avx2 () at ../sysdeps/x86_64/multiarch/strcmp-avx2.S:102 (gdb) backtrace
Wir sind jetzt im Thread 2 (siehe oben). #0 __strcmp_avx2 Der Backtrace ist von oben zu lesen: Wer hat wen aufgerufen? #1 0x0000555555555300 in counter #2 0x00007ffff7f3bea7 in start_thread #3 0x00007ffff7e6bdef in clone (gdb) frame
1
#1 0x0000555555555300 in counter (arg=0x7fffffffe560) at mwc.c:45 45 if (0 == strcmp(word, data->list[i].word)) { (gdb) print
word
$1 = 0x7ffff7d67ecc "elite" Der lokale Speicher passt... (gdb) print
i
$2 = 109 Es wurden auch schon Daten verarbeitet... (gdb) print
data->list[i].word
$3 = 0x0 Aber der Listeneintrag ist NULL! (gdb) thread apply all
backtrace
Zur Übersicht führen wir backtrace auf jedem Faden aus. Thread 4 (Thread 0x7ffff6d68700 (LWP 3216093) "mwc"): #0 clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:78 #1 0x00007ffff7f3bdd0 in ?? #2 0x00007ffff6d68700 in ?? #3 0x0000000000000000 in ?? Dieser thread wurde scheinbar noch nicht initialisiert. Thread 3 (Thread 0x7ffff7569700 (LWP 3216092) "mwc"): #0 __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:50 #1 0x00007ffff7d93537 in __GI_abort Hier muss wohl ein ungültiger Zustand aufgetreten sein... #2 0x00007ffff7dec768 in __libc_message #3 0x00007ffff7df3a5a in malloc_printerr #4 0x00007ffff7df79bc in _int_realloc #5 0x00007ffff7df8c56 in __GI___libc_realloc Aufgrund von realloc(3)! #6 0x000055555555536e in counter #7 0x00007ffff7f3bea7 in start_thread #8 0x00007ffff7e6bdef in clone Thread 2 (Thread 0x7ffff7d6a700 (LWP 3216091) "mwc"): #0 __strcmp_avx2 () at ../sysdeps/x86_64/multiarch/strcmp-avx2.S:102 #1 0x0000555555555300 in counter Den wir dann hier erlebt haben! #2 0x00007ffff7f3bea7 in start_thread #3 0x00007ffff7e6bdef in clone Thread 1 (Thread 0x7ffff7d6b740 (LWP 3216069) "mwc"): #0 clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:78 #1 0x00007ffff7f3ad7c in create_thread #2 0x00007ffff7f3c68e in __pthread_create_2_1 #3 0x000055555555553c in main

Betrachtet man das Referenzprogram sieht man, dass das Problem ein zu kleiner kritischer Abschnitt war. Vergrößern wir diese auf die gesamte strtok(3)-Schleife, tritt der Fehler nicht mehr auf.

Wir können auch anschauen wieso Thread 4 so merkwürdig im Backtrace aussieht. Dazu kann man in die main Routine springen, und anschauen wo wir unterbrochen wurden:

(gdb) thread
2
[Switching to thread 1 (Thread 0x7ffff7d6b740 (LWP 3216069))] #0 clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:78 #1 0x00007ffff7f3ad7c in create_thread Mit dem backtrace Argument werden nur zwei Frames ausgegeben. (gdb) frame
function main
#3 0x000055555555553c in main () at mwc.c:87 Wir sind noch in der ersten Schleife! 87 errno = pthread_create(&threads[i], NULL, counter, &data); (gdb) print
i
$4 = 2 Es wurden auch nur zwei Fäden erstellt.

Es ist gegebenenfalls interessant anzumerken, dass der Thread 1 unser Hauptprogramm ist, auch wenn dieser nicht mit pthread_create(3) erstellt wurde.

Referenzprogram
/* Nicht -pthread in den CFLAGS vergessen! */

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <errno.h>

#define LEN(a) (sizeof(a)/sizeof(*a))
#define CAS atomic_compare_exchange_strong
#define DELIM " \t\n\r.:;?!,()*-=_\'\""

struct data {
     struct count {
          char *word;
          unsigned occ;
     } *list;
     unsigned len;
     pthread_mutex_t lock;
};

void die(char *msg)
{
     perror(msg);
     exit(EXIT_SUCCESS);
}

void *counter(void *arg)
{
     struct data *data = (struct data*) arg;
     char line[BUFSIZ], *word = NULL;
     bool ok;
     char *p;
     int i;

     for (;;) {
          ok = NULL != fgets(line, sizeof(line), stdin);
          if (!ok) {
               if (feof(stdin)) return NULL;
               die("fgets");
          }

          while ((word = strtok_r(word ? NULL : line, DELIM, &p))) {
               ok = false;
               for (i = 0; i < data->len; i++) {
                    if (0 == strcmp(word, data->list[i].word)) {
                         ok = true;
                         break;
                    }
               }

               pthread_mutex_unlock(&data->lock);
               if (!ok) {
                    data->list = realloc(
                         data->list,
                         ++data->len * sizeof(struct count));
                    if (NULL == data->list) {
                         die("realloc");
                    }
                    data->list[i].occ = 1;
                    data->list[i].word = strdup(word);
                    if (NULL == data->list[i].word) {
                         die("strdup");
                    }
               }
               data->list[i].occ++;
               pthread_mutex_unlock(&data->lock);
          }
     }
}

int comp(const void *a, const void *b)
{
     return ((struct count*) a)->occ < ((struct count*) b)->occ;
}

int main()
{
     int i;
     pthread_t threads[4]; /* <-- Hier wird die Anzahl der Threads festgelegt. */
     struct data data = { 0 };

     errno = pthread_mutex_init(&data.lock, NULL);
     if (errno) die("pthread_mutex_init");

     /* Starte Zähler in 4 Fäden  */
     for (i = 0; i < LEN(threads); i++) {
          errno = pthread_create(&threads[i], NULL, counter, &data);
          if (errno) die("pthread_create");
     }

     /* Warte das die Zähler terminieren (ie. EOF erreichen) */
     for (i = 0; i < LEN(threads); i++) {
          errno = pthread_join(threads[i], NULL);
          if (errno) die("pthread_join");
     }

     /* Gebe die absteigende de Häufigkeit jedes Wortes. */
     qsort(data.list, data.len, sizeof(struct count), comp);
     for (i = 0; i < data.len; i++) {
          printf("%8d\t%s\n", data.list[i].occ, data.list[i].word);
     }

     pthread_mutex_destroy(&data.lock);

     return 0;
}

3. Antipatterns

Viele empfinden GDB als anstrengend und unpraktisch. Man meidet es zu lernen, weil printf doch schneller geht!. Dieses ändert sich aber oft, wenn man die kleinen Tipps und Tricks lernen, welche das Leben mit der primär Befehlsorientierte Oberfläche (CLI) erleichterten können.

Hier also eine Liste von lästigen Sachen, die man oft bei GDB Nutzern sehen kann, und was man besser machen kann:


4. Ausgewählte Befehle Gruppiert nach Absteigender Wichtigkeit

Gewisse Befehle muss man wissen, um GDB zu nutzen, andere sich hilfreich oder zeigen ihr Potential nur in besonderen Randfällen. Welche welche sind ist anfangs schwer zu unterscheiden. Oft will man auch nur ein Bug finden, und nicht lernen wie man ein obskures Programm benutzt!

Der Sinn dieses Abschnittes ist eine opinionated Ordnung von Befehlen nach ihrer (von mir wahrgenommenen) Wichtigkeit. Die Absicht hierbei ist nicht die tatsächlich, objektive Bedeutung zu erfassen, sondern hinzuweisen wo man anfangen soll zu lernen, und was man sich erst später aneignen sollte.

GDB erlaubt es befehle Abzukürzen: Ich schreibe immer die ganzen Namen hin, aber deute die kürzere Version durch Unterstriche an. Benutzt was euch mehr gefällt — vor allem wenn ihr Befehle nicht vertauschen wollt.

Zentral

run
Starte Ausführung, und laufe bis zum ersten Breakpoint (siehe break) oder man selbst die Ausführung unterbricht (C-c). Argumente welche an das Programm gegeben werden sollen, können nach run folgen.
break
Setze einen Punkt im Code, wo die Ausführung anhalten soll zur Inspektion. Das Argument kann dabei eine Zeilennummer, eine Datei + Zeilennummer (bspw. code.c:32) oder eine Funktion sein.
next
Führe nur eine "Zeile" im Quelltext aus, und springe dabei nicht in Funktionen. Eine optionale Zahl kann dabei angeben wie oft ge-next’t werden soll.
step
Führe nur eine "Zeile" im Quelltext aus, und springe dabei doch in Funktionen. Eine optionale Zahl kann dabei angeben wie oft ge-step’t werden soll.
print
Wertet das Argument aus, und gibt das Resultat aus. Das Argument kann dabei eine Variable, oder sogar ein Funktionsaufruf sein.
quit
Beendet eine GDB Session. Äquivalent zü C-d.

Hillfreich

backtrace
Gibt Stacktrace am jetzigen Punkt an. Jeder Funktionsaufruf ist dabei mit einer Zahl versehen, welches mit frame benutzt werden kann.
frame
Wechsele (ohne das der Zustand sich ändert) in ein anderen Frame. Dabei wird das Argument als Framenummer interpretiert (siehe backtrace).
list
Gebe Quelltext an, in der nähe von der jetzt "aktiven" Zeile. Mit C-x C-a oder layout src kann Quelltext auch immer angezeigt werden.
set var
Verändere Wert einer Variable. Argument hat die Form variable=value.
continue
Führe das Programm weiter fort, bist zum nächstem Breakpoint oder Fehler.
display
Wie print, nur soll bei jedem Halten der Wert ausgegeben werden.
until
Führe aus bis zu einem Punkt. Gleiche Art von Argumenten wie break.

Nützlich

start
Starte das Programm wie mit run, aber setze ein einmailigen Breakpoint auf die main Funktion, d.h. fange sofort an zu Debuggen.
kill
Beende das laufende Programm. Kann ggf. danach wieder ausgeführt werden.
finish
Führe jetzige Funktion bis zum Ende aus, und warte dann wieder.
return
Kehre aus der jetzigen Funktion zurück, mit dem Argument als Rückgabe wert.
break … if COND
Halte nur dann die Ausführung an (wie bei break), wenn COND im Kontext erfüllt wird.
watch
Falle in den Debugger, wenn die Variable im Argument verändert wird. Variationen hiervon sind rwatch für Lesen, und awatch für beides. Kann genau wie break auch ein if ... benutzen.
delete
Lösche ein Break- oder Watchpoint insgesammt. Aus Performanzgründen Sinnvoll bei zu vielen Break- und Watchpoints. Argument ist eine Break- oder Watchpoint Nummer.
disable
Ignoriere ein Break- oder Watchpoint, ohne es zu löschen, bis zu einem enable. Argumente wie bei delete.
enable
Reaktiviere ein Break- oder Watchpoint nach einem enable. Argumente wie bei delete.
ignore
Die zwei Argumente, ein Break- oder Watchpoint und eine Zahl, geben an wie oft diese bei dieser Stelle nicht gehalten werden soll.
thread
Wechsele GDB Fokus auf Thread mit Argument als Nummer. Gebe Thread Nummern mit info threads aus.
thread apply
Führe Argument als Befehl auf anderen Threads aus, ohne manuell hin und zurück wechseln zu müssen.
set follow-fork-mode
Sage GDB ob nach einem fork(2) weiter den Elternprozess Debuggen soll (mit Argument parent, default) oder das Kind (mit Argument child).
advance
Führe Programm aus bis zum durch das Argument angedeuteten Punkt (eg. Zeile). Vergleichbar zu until, nur muss das Argument nicht in der jetzigen Funktion sein.
skip
Auch mit einem step soll nicht in diese Funktion hineingeschritten werden.
attach
Verbindet GDB mit einem bereits laufendem Prozess. Dieser wird durch dessen Prozess ID als Argument spezifiziert (benutzt pidof(1) um die PID eines Prozesses zu ermitteln.). Alternativ kann auch gdb mit dem -p Flag aufgerufen werden, um das gleiche zu erreichen.
make
Will man sein Programm neu bauen, verlassen viele Leute GDB und kompilieren das Programm neu. Dieses ist aber meist nicht notwendig, da mit dem make Befehl, make(1) wie in der Shell aufgerufen werden kann.

Wunderlich

handle
Verändere GDB’s Signalbehandlung. Siehe Manual.
reverse-continue
Laufe rückwärts durch ein Programm, bis man zum letztem Breakpoint ankommt. Funktioniert nicht immer, mit record full kann GDB helfen bei fehlender Hardware Unterstützung.
Existiert auch in reverse-step, reverse-next, reverse-finish, … Varianten. Siehe Manual.
macro expand
macro expand-once
Wertet ein Makro-Ausdruck aus, entweder vollständig, oder nur ein Schritt. Resultate werden jedoch nicht in der Umgebung ausgewertet. Siehe Manual.
trace
Vergleichbar zu break, nur wird automatisch gehalten um Daten zu sammeln, ohne das der Nutzer diese Manuell abfragen muss. Diese können dann im Nachhinein abgefragt werden. Siehe Manual.
dprintf
Vergleichbar zu break, nur wird automatisch gehalten um Daten zu sammeln, ohne das der Nutzer diese Manuell abfragen muss. Diese können dann im Nachhinein abgefragt werden. Siehe Manual.
catch
Fange gewisse Ereignisse ab (wie syscall, fork, signal, exec, …) und falle zurück in den Debugger (wie bei break). Siehe Manual.

Sofortige Informationen zu einem Befehl sind immer abrufbar mit help COMMAND.

Allgemein kann man ohne GDB zuzumachen, ein Shell Befehl ausführen, indem man ein ! an den Anfang einer Zeile mit dem Befehl fügt.