Beim Debuggen von Speicherzugriffen ist es manchmal recht schwierig, die verursachende Stelle im Quelltext zu finden, wenn das Programm an einer scheinbar zufälligen Stelle crasht oder es durch Logikfehler unerwartete Ergebnisse liefert.
Hilfsmittel wie Valgrind oder Watchpoints (“help watch” und “help rwatch” in GDB) erleichtern hierbei die Fehlersuche, können aber nicht immer den Fehler finden. Gerade bei komplexeren Bugs wünscht man sich oft, in der Programmausführung zurückgehen zu können, um Variablen vor einem bestimmten Funktionsaufruf untersuchen zu können, oder um weitere Breakpoints zu setzen.
Seit Version 7.0 ist genau das mit GDB möglich. Mit Hilfe des “record”-Targets kann die Programmausführung aufgenommen werden und schrittweise zurück- und auch wieder vorgespult werden.
Um dieses Feature zu demonstrieren, kompilieren wir folgendes Beispiel-Programm mit “gcc -ggdb -o test test.c”:

int main(int argc, char **argv) {
    int a, b;
    a = 7;
    b = a;
    return 0;
}

Danach können wir wie gewohnt gdb aufrufen:

$ gdb ./test

Zunächst müssen wir einen Breakpoint setzen, an dem wir mit der Aufnahme der Programmausführung starten wollen. Im einfachsten Falle wäre das die “main”-Funktion. Bei komplexeren Programmen kann hier eine andere Stelle im Programm verwendet werden, wo der zu untersuchende Bug noch nicht aufgetreten ist:

(gdb) break main
Breakpoint 1 at 0x4004bf: file test.c, line 4.

Danach können wir noch einen zweiten Breakpoint setzen, wenn wir wissen, an welcher Stelle der Bug schon aufgetreten ist (z.B. beim Speichern einer Datei, wenn die zu speichernden Daten zu diesem Zeitpunkt bereits fehlerhaft sind) – dies ist jedoch optional:

(gdb) break test.c:8
Breakpoint 2 at 0x4004cc: file test.c, line 8.

Nachdem wir die Breakpoints gesetzt haben, können wir das Programm starten:

(gdb) run
Starting program: /home/gunnar/test
Breakpoint 1, main (argc=1, argv=0x7fffffffe288) at test.c:4
4		a = 7;

GDB stoppt unseren Prozess an dem ersten Breakpoint. Mit dem “record”-Befehl können wir nun GDB anweisen, die weitere Programmausführung aufzuzeichnen:

(gdb) record

Wenn wir den Prozess nun weiterlaufen lassen, sollte er den zweiten Breakpoint erreichen. (Falls wir keinen gesetzt haben, fragt GDB uns nach Ausführung des Programms, ob wir den Prozess anhalten möchten – dies sollte mit “y” beantwortet werden, da GDB sonst den Prozess beendet):

(gdb) continue
Continuing.
Breakpoint 2, main (argc=1, argv=0x7fffffffe288) at test.c:8
8		return 0;

Um nun in der Aufnahme zurückspringen zu können, bietet GDB zusätzlich zu den normalen Ausführungsbefehlen (continue, step, next, usw.) entsprechende Befehle, die in umgekehrter Richtung funktionieren:

  • reverse-continue
  • reverse-finish
  • reverse-next
  • reverse-nexti
  • reverse-step
  • reverse-stepi

Mit reverse-continue können wir z.B. zum Anfang der Aufnahme zurückspringen:

(gdb) reverse-continue
Continuing.
No more reverse-execution history.
main (argc=1, argv=0x7fffffffe288) at test.c:4
4		a = 7;

An dieser Stelle können wir z.B. weitere Breakpoints und Watchpoints setzen, oder mit “step” das Programm zeilenweise durchlaufen lassen und mit “print” Variablen untersuchen.
Abschließend wäre noch zu beachten, dass “record” das auszuführende Programm deutlich verlangsamt. Idealerweise sollten dabei die Breakpoints so gewählt werden, dass ein möglichst kleiner Teil des Prozesses aufgezeichnet werden muss.