Debugging von C++-Code: Wie Sie aufhören, Angst vor Segmentation Fault zu haben, und GDB lieben lernen
Haben Sie schon einmal drei Stunden damit verbracht, einen Fehler zu suchen, der sich am Ende als vergessenes break in einem switch oder ein überflüssiges ; nach einem if herausstellte? Wenn ja – willkommen im Club. Das Debugging von C++ ist eine Kunst, die an Masochismus grenzt. Aber wie jede Kunst kann man sie erlernen.
In diesem Artikel behandeln wir nicht nur „wie starte ich GDB“, sondern echte Techniken, die das Debugging von Kaffeesatzleserei in detektivische Arbeit à la Sherlock Holmes verwandeln. Wir sprechen über moderne Sanitizer, Reverse Debugging und sogar darüber, wie man einen Fehler aufspürt, der sich „nur freitags nachmittags“ reproduzieren lässt.
1. Der Klassiker: GDB und LLDB
Beginnen wir mit den Grundlagen. Der GNU Debugger (GDB) und sein Konkurrent LLDB sind die Schweizer Taschenmesser unter den Debuggern. Aber die meisten nutzen nur 10 % ihrer Möglichkeiten. Lassen Sie uns das ändern.
Haltepunkte mit Bedingungen
Sie setzen einen Haltepunkt in einer Schleife mit 10.000 Iterationen und drücken 9999 Mal continue? Hören Sie auf damit. Verwenden Sie bedingte Haltepunkte:
(gdb) break main.cpp:42 if i == 5000Das spart Stunden Ihres Lebens. In LLDB ist die Syntax ähnlich:
(lldb) breakpoint set --file main.cpp --line 42 --condition 'i == 5000'Watchpoints – Änderungen einer Variable abfangen
Stellen Sie sich vor: Sie haben eine Variable, die sich irgendwo unerkannt ändert. Setzen Sie einen Watchpoint:
(gdb) watch my_variable(gdb) rwatch my_variable # löst bei Lesezugriff aus(gdb) awatch my_variable # löst bei Lese- und Schreibzugriff ausJetzt stoppt das Programm genau in dem Moment, in dem jemand Ihre Variable berührt. Magie, oder?
2. Reverse Debugging: Zeitreise
Der schlimmste Albtraum eines Programmierers: Das Programm ist abgestürzt, aber Sie wissen nicht, wie es zu diesem Zustand gekommen ist. Reverse Debugging (umgekehrtes Debugging) ermöglicht es Ihnen, die Ausführung zurückzuspulen.
In GDB benötigen Sie dafür die Unterstützung von record:
(gdb) record(gdb) continue... Programm abgestürzt ...(gdb) reverse-step # einen Schritt zurück(gdb) reverse-continue # in die entgegengesetzte Richtung fortsetzenIn kommerziellen Werkzeugen (z. B. UndoDB oder rr von Mozilla) funktioniert das noch besser. rr (Record and Replay) ist ein kostenloses Werkzeug, das die Programmausführung aufzeichnet und es Ihnen dann erlaubt, sie immer wieder abzuspielen:
$ rr record ./my_program$ rr replay(gdb) continue... Absturzstelle gefunden ...(gdb) reverse-stepTipp: Verwenden Sie rr zum Auffinden von Datenrennen (Data Races) – es reproduziert jede Ausführung deterministisch.
3. Sanitizer: Impfung gegen undefiniertes Verhalten
Modernes C++ (ab Clang und GCC 4.8+) verfügt über integrierte Werkzeuge, von denen unsere Väter nicht einmal träumen konnten. AddressSanitizer (ASan), UndefinedBehaviorSanitizer (UBSan) und ThreadSanitizer (TSan) sind ein Must-have.
Beispiel: Speicherleck aufspüren
// Kompilieren mit: g++ -fsanitize=address -g main.cpp -o main#include <iostream>
void leak() { int* p = new int(42); // delete vergessen}
int main() { leak(); return 0;}Starten – und wir sehen:
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 4 byte(s) in 1 object(s) allocated from: #0 0x7f... in operator new(unsigned long) #1 0x400... in leak() main.cpp:5 #2 0x400... in main main.cpp:10ASan zeigt den genauen Aufrufstapel, einschließlich der Zeilennummer, in der die Speicherzuweisung stattfand. Schluss mit „irgendwo leckt Speicher“.
UBSan gegen undefiniertes Verhalten
// Kompilieren mit: g++ -fsanitize=undefined -g main.cppint main() { int x = INT_M