Ach du liebes Lieschen! Hätt ich meine Ohren früher gespitzt, wär mir das prächtige GDB nicht durch die Lappen gegangen? Das ist ja, als hätt ich bisher nur Wasser getrunken und nun den besten Wein entdeckt! — Jemand der GDB zu spät gelernt hat
Diese Seite gibt eine Einführung zu GDB, und hofft zu erklären, wie man es effizient und angenehm Programme debuggen kann.
Bei Fehlern, Fragen oder Vorschlägen mir gerne Schreiben.
Diese Seite ist Umlaufbestand
und ich freue mich immer über Rückmeldungen!
GDB ist ein Debugger, kein Compiler. Um es zu benutzen, muss das
Programm erst übersetzt und gebunden 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.
Achtung: Falls man Optimierungen aktiviert hat (-O1
,
-O2
, -O3
, -Os
, -Oz
, ...)
sollte man diese deaktivieren um das Verhalten der Programme in Ausführung besser
zu verstehen. Standardmäßig sind keine Optimierungen 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.
Erst später starten wir das Program!
Tipp: Wenn man ein Programm immer 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.
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.
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).
#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;
}
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, dassi
immernoch 1 ist $1 = 1 (gdb) display i Wir entscheiden uns immeri
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 wiederholtnext
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 seheni
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.
#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;
}
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.
#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;
}
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 istNULL
! (gdb) thread apply all backtrace Zur Übersicht führen wirbacktrace
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 vonrealloc(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.
/* 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;
}
Viele empfinden GDB als anstrengend und unpraktisch. Man meidet es zu
lernen, weil
. 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.
printf
doch schneller geht!
Hier also eine Liste von lästigen
Sachen, die man oft bei GDB Nutzern
sehen kann, und was man besser machen kann:
print
wieder und wieder
eingeben:
Wenn man ständig den Wert einer variable nachverfolgen will, eignet
sich display
, oder eine
fortgeschrittenere Frontend. Ggf. sollte man
überlegen, ob man das richtige macht und ob
ein watch
nicht schlauer wäre.
step
-in
rückgängig machen:
Wer nicht aufpasst, kann step
einmal zu viel ausführen, und in einer langen oder System Funktion (ohne
Quelltext) landen. Um da nicht den Debugger neu starten zu müssen, oder
ewig lange next
ausführen zu
müssen, kann finish
(bzw. return
, wenn man den
Rückgabewert verändern will benutzen).
.gdbinit
benutzen:
GDB sucht nach einer Datei im Home-Verzeichnis, mit
dem Namen .gdbinit
. Dieses enthält auf
jeder Zeile einen Befehl, welches vor dem Start
ausgeführt wird. Mein .gdbinit
sieht so
aus:
set history filename ~/.cache/gdb_history
set history save
set print pretty on
set print object on
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.
- Anstatt
p
schreiben.- Anstatt
backtrace
auch nurbt
.
break
) oder man
selbst die Ausführung unterbricht (C-c). Argumente welche an
das Programm gegeben werden sollen, können nach run
folgen.
code.c:32
) oder eine Funktion sein.
next
’t werden soll.
step
’t werden soll.
frame
benutzt werden
kann.
backtrace
).
layout src
kann Quelltext auch
immer angezeigt werden.
variable=value
.
print
, nur soll bei
jedem Halten der Wert ausgegeben werden.
break
.
run
, aber setze
ein einmailigen Breakpoint auf die main
Funktion,
d.h. fange sofort an zu Debuggen.
break
), wenn COND
im
Kontext erfüllt wird.
rwatch
für Lesen,
und awatch
für beides. Kann genau
wie break
auch
ein if ...
benutzen.
enable
.
Argumente wie bei delete
.
enable
. Argumente wie
bei delete
.
info threads
aus.
fork(2)
weiter den
Elternprozess Debuggen soll (mit Argument parent
,
default) oder das Kind (mit Argument child
).
until
, nur muss das
Argument nicht in der jetzigen Funktion sein.
step
soll nicht in diese Funktion hineingeschritten werden.
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
Befehl,
make(1)
wie in der Shell aufgerufen werden kann.
rückwärtsdurch ein Programm, bis man zum letztem Breakpoint ankommt. Funktioniert nicht immer, mit
record full
kann GDB
helfen bei fehlender Hardware Unterstützung.reverse-step
, reverse-next
, reverse-finish
,
… Varianten. Siehe Manual.
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.
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.
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.
$ info gdb
lokal aufrufbar)gdb
Debugger Tutorial (nicht Stallman, nur gleiches Akronym)M-x gdb
. Siehe auch
den realgudFork.
Give Me 15 Minutes and I’ll Change Your View of GDB
Advanced Programming in the UNIX Environment.
Pleasant debugging with GDB and DDD