Philipp Trommlers Blog

GDB Breakpoint Kommandos

Breakpoint command lists sind ein nützliches Feature des GDB und dennoch lese ich kaum von ihnen und sehe sie nur selten in Benutzung, selbst bei erfahrenen Anwendern. Daher möchte ich hier ein paar Nutzungsbeispiele geben.

Veröffentlicht am von Philipp Trommler. Dieser Beitrag wurde außerdem übersetzt nach: en.

Die Grundlagen sind ganz einfach: Nach man mindestens einen Breakpoint erstellt hat, kann mit commands eine Befehlsliste erstellt werden, die bei jedem Treffer des Breakpoints abgearbeitet wird. Standardmäßig legt commands diese Liste für den zuletzt erstellten Breakpoint an, durch die Angabe einer Nummer hinter commands kann aber auch ein ein bestimmter Breakpoint ausgewählt werden. Ist man einmal in der commands-Umgebung, können dort alle Kommandos aufgelistet werden, die auch im gewöhnlichen GDB-Prompt zur Verfügung stehen. Verlassen wird die Umgebung durch die Eingabe von end.

Innerhalb der commands-Umgebung hat man Zugriff auf alle Variablen, die auch im GDB-Prompt verfügbar wären, hätte der Breakpoint normal getroffen, also globale Variablen, Funktionsargumente und lokale Variablen. Soll der aktuelle Treffer des Breakpoints übersprungen werden, kann wie üblich continue aufgerufen werden, bevor das abschließende end erreicht wird. Dementsprechend muss das finale end erreicht werden, ohne ein continue zu treffen, wenn man in einen GDB-Prompt für den aktuellen Treffer des Breakpoints wechseln möchte. Das selbe gilt analog auch für next und step.

Meiner Erfahrung nach kombiniert man commands am besten mit silent, wodurch die typischen Ausgaben des GDB beim Erreichen eines Breakpoints unterbunden werden. Insbesondere bei häufig getroffenen Breakpoints macht dies die Ausgaben deutlich lesbarer. Außerdem kann man innerhalb der commands-Umgebung die selben Kurznamen für Befehle verwenden wie im Prompt, also zum Beispiel c für continue. Um die Einstiegshürde niedrig zu halten habe ich darauf aber in diesem Beitrag verzichtet.

Nun aber zu den Beispielen. Angenommen folgendes Problem: In einem langlaufenden Programm kommt es von Zeit zu Zeit dazu, dass der Parameter einer Funktion nicht wie erwartet ist und die Herkunft dieses ungewöhnlichen Wertes ist unklar. Zudem kann das Problem nicht reproduziert werden. Eine solche Situation ist im unten dargestellten Programm abstrahiert in der Funktion foo dargestellt. Immer mal wieder erhält diese Funktion statt eines gültigen Strings einen NULL-Pointer. Natürlich würden solche Fehler oft einfach einen SEGFAULT hervorrufen und benötigten daher gar keinen Breakpoint, aber nehmen wir einfach mal im Sinne des Beispiels an, dem wäre nicht so.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stddef.h>

void foo(const char * str)
{
    /* TODO: Do something with str */
    (void) str;
}

int main(void)
{
    const char * arr[5] = {
        "This",
        "is",
        "a",
        NULL,
        "test."
    };

    for (long unsigned i = 0; i < sizeof(arr) / sizeof(char *); ++i)
    {
        foo(arr[i]);
    }

    return 0;
}

Das Programm kann mit gcc -Wall -Wextra -Werror -std=c18 -g -o main main.c kompiliert und sogar ausgeführt werden. Um das eigentliche Problem zu untersuchen, starten wir es im GDB und geben folgendes ein:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
(gdb) break foo
Breakpoint 1 at 0x1141: file main.c, line 7.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>if (0x0 == str)
 >backtrace
 >else
 >continue
 >end
>end
(gdb)

Dies erzeugt einen Breakpoint für foo und führt bei jedem Treffer dieses Breakpoints die folgenden Befehle aus:

  1. Die üblichen GDB-Ausgaben beim Treffen eines Breakpoints werden unterbunden.
  2. Wenn das Argument str der Funktion foo ein NULL-Pointer ist, wird der aktuelle Backtrace ausgegeben und ein GDB-Prompt geöffnet.
  3. Sonst wird das Programm weiter ausgeführt.

Und tatsächlich, nachdem run aufgerufen worden ist, wird die Ausführung des Programms an der Stelle unterbrochen, an der ein NULL-Pointer in die Funktion foo übergeben wird:

1
2
3
4
5
(gdb) run
Starting program: /tmp/tmp.4UHFRrHldC/main
#0  foo (str=0x0) at main.c:7
#1  0x00005555555551aa in main () at main.c:21
(gdb)

Schlaue Köpfe mögen nun anmerken, dass ein nahezu gleiches Verhalten auch unter Benutzung von bedingten Breakpoints à la break foo if 0x0 == str hätte erreicht werden können und das stimmt auch. Das Folgende kann jedoch nicht so einfach repliziert werden: Angenommen, der Nutzer möchte bestimmte, eventuell fehlerhafte, Werte zur Laufzeit in das Programm injizieren, um ein bestimmtes Verhalten zu auszulösen, zum Beispiel während des Prototypings. Genau so, wie innerhalb der commands-Umgebung Werte abgefragt werden können, können diese nämlich auch verändert werden.

Um dies zu verdeutlichen, wird die Funktion foo aus dem vorigen Beispiel wie folgt geändert, sodass sie die Werte, die ihr übergeben werden, ausgibt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@@ -1,9 +1,9 @@
 #include <stddef.h>
+#include <stdio.h>

 void foo(const char * str)
 {
-    /* TODO: Do something with str */
-    (void) str;
+    puts(str);
 }

 int main(void)

Das resultierende Programm kann noch immer wie zuvor kompiliert werden, beim Versuch es auszuführen, kommt es jedoch zum SEGFAULT, wenn der NULL-Pointer ausgegeben werden soll. Aber, wieder zum Wohle des Beispiels, angenommen, es handele sich um ein weitaus komplexeres Programm, in dem zur Laufzeit Daten geändert werden sollen, um ein bestimmtes Verhalten zu provozieren. Unter Verwendung des set var-Befehls des GDB in der commands-Umgebung kann dieses Ziel erreicht werden. Um beispielsweise bei jedem Aufruf von foo den str-Parameter auf "bar" zu setzten, können die folgenden Befehle genutzt werden:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
(gdb) break foo
Breakpoint 1 at 0x1155: file main.c, line 6.
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>silent
>set var str="bar"
>continue
>end
(gdb) run
Starting program: /tmp/tmp.4UHFRrHldC/main
bar
bar
bar
bar
bar
[Inferior 1 (process 21664) exited normally]
(gdb)

Und wie zu erwarten, wird bar fünf Mal ausgegeben und es tritt kein SEGFAULT mehr auf.

Natürlich sind dies nur sehr triviale Beispiele für den Einstieg in die GDB Breakpoint Kommandos. Durch Verwendung sonstiger, aus der normalen Nutzung des GDB bekannter Tricks können diese aber zu einem sehr nützlichen Instrument in der Werkzeugkiste des Fehlersuchenden werden. Und tatsächlich: Seitdem ich die commands-Umgebung kenne, hat sich meine gesamte Nutzung des GDB dahingehend verändert, dass ich sie regelmäßig einsetze.

Abgelegt unter Debugging. Tags: programming, gdb.

Willst du diesen Beitrag kommentieren? Schreib mir an blog [at] philipp-trommler [dot] me!

Beiträge von Blogs, denen ich folge

One machine can go pretty far if you build things properly
via Writing - rachelbythebay, January 28, 2022

Okay, so yesterday I posted a picture of an old multi-line bulletin board system which had apparently taken over an entire bedroom with dozens of computers. I mentioned that I intended to come back to the topic to throw in my own two cents, and so here we…

Implementing a MIME database in XXXX
via Drew DeVault's blog, January 28, 2022

This is a (redacted) post from the internal blog of a new systems programming language we’re developing. The project is being kept under wraps until we’re done with it, so for this post I’ll be calling it XXXX. If you are interested in participating, send m…

LibrePCB Talk at FOSDEM 2022
via LibrePCB Blog, January 28, 2022

After the introduction to LibrePCB talk at FOSDEM 18 and the status update talk at FOSDEM 20, I’m happy to give one more update about the current project status at FOSDEM 22! The status update talk will take place on Saturday, February 5, at 12:00 CET. Due…

Generiert mit openring