GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Ein praktisches Tutorial zur Verwendung des GNU Project Debuggers

Wenn Sie ein Programmierer sind und eine bestimmte Funktionalität in Ihre Software einbauen möchten, denken Sie zunächst über Möglichkeiten nach, sie zu implementieren – wie das Schreiben einer Methode, das Definieren einer Klasse oder das Erstellen neuer Datentypen. Dann schreiben Sie die Implementierung in einer Sprache, die der Compiler oder Interpreter verstehen kann. Aber was ist, wenn der Compiler oder Interpreter die Anweisungen nicht so versteht, wie Sie sie sich vorgestellt haben, obwohl Sie sicher sind, alles richtig gemacht zu haben? Was ist, wenn die Software die meiste Zeit gut funktioniert, aber unter bestimmten Umständen Fehler verursacht? In diesen Fällen müssen Sie wissen, wie Sie einen Debugger richtig verwenden, um die Ursache Ihrer Probleme zu finden.

Der GNU Project Debugger (GDB) ist ein mächtiges Werkzeug zum Auffinden von Fehlern in Programmen. Es hilft Ihnen, den Grund für einen Fehler oder Absturz aufzudecken, indem es verfolgt, was während der Ausführung im Programm vor sich geht.

Dieser Artikel ist ein praktisches Tutorial zur grundlegenden Verwendung von GDB. Um den Beispielen zu folgen, öffnen Sie die Befehlszeile und klonen Sie dieses Repository:

git clone https://github.com/hANSIc99/core_dump_example.git

Kurzbefehle

Weitere Linux-Ressourcen

  • Spickzettel für Linux-Befehle
  • Spickzettel für fortgeschrittene Linux-Befehle
  • Kostenloser Online-Kurs:RHEL Technical Overview
  • Spickzettel für Linux-Netzwerke
  • SELinux-Spickzettel
  • Spickzettel für allgemeine Linux-Befehle
  • Was sind Linux-Container?
  • Unsere neuesten Linux-Artikel

Jeder Befehl in GDB kann abgekürzt werden. Beispiel:info break , das die gesetzten Breakpoints anzeigt, kann zu i break abgekürzt werden . Sie werden diese Abkürzungen vielleicht an anderer Stelle sehen, aber in diesem Artikel werde ich den gesamten Befehl ausschreiben, damit klar ist, welche Funktion verwendet wird.

Befehlszeilenparameter

Sie können GDB an jede ausführbare Datei anhängen. Navigieren Sie zum geklonten Repository und kompilieren Sie es, indem Sie make ausführen . Sie sollten jetzt eine ausführbare Datei namens coredump haben . (Siehe meinen Artikel über Erstellen und Debuggen von Linux-Dump-Dateien für weitere Informationen..

Um GDB an die ausführbare Datei anzuhängen, geben Sie Folgendes ein:gdb coredump .

Ihre Ausgabe sollte wie folgt aussehen:

Es heißt, es wurden keine Debugging-Symbole gefunden.

Debugging-Informationen sind Teil der Objektdatei (der ausführbaren Datei) und umfassen Datentypen, Funktionssignaturen und die Beziehung zwischen dem Quellcode und dem Opcode. An dieser Stelle haben Sie zwei Möglichkeiten:

  • Fahren Sie mit dem Debuggen der Assembly fort (siehe "Debug ohne Symbole" weiter unten)
  • Kompilieren Sie mit Debug-Informationen unter Verwendung der Informationen im nächsten Abschnitt

Mit Debug-Informationen kompilieren

Um Debug-Informationen in die Binärdatei aufzunehmen, müssen Sie sie neu kompilieren. Öffnen Sie das Makefile und entfernen Sie das Hashtag (# ) ab Zeile 9:

CFLAGS =-Wall -Werror -std=c++11 -g

Das g Option weist den Compiler an, die Debug-Informationen einzuschließen. Führen Sie make clean aus gefolgt von make und rufe GDB erneut auf. Sie sollten diese Ausgabe erhalten und können mit dem Debuggen des Codes beginnen:

Die zusätzlichen Debugging-Informationen erhöhen die Größe der ausführbaren Datei. In diesem Fall wird die ausführbare Datei um das 2,5-fache erhöht (von 26.088 Byte auf 65.480 Byte).

Starten Sie das Programm mit dem -c1 wechseln Sie, indem Sie run -c1 eingeben . Das Programm startet und stürzt ab, wenn es State_4 erreicht :

Sie können weitere Informationen zum Programm abrufen. Der Befehl info source liefert Informationen über die aktuelle Datei:

  • 101 Zeilen
  • Sprache:C++
  • Compiler (Version, Tuning, Architektur, Debug-Flag, Sprachstandard)
  • Debugging-Format:DWARF 2
  • Keine Präprozessor-Makroinformationen verfügbar (bei Kompilierung mit GCC sind Makros nur verfügbar, wenn sie mit -g3 kompiliert wurden Flagge).

Der Befehl info shared gibt eine Liste dynamischer Bibliotheken mit ihren Adressen im virtuellen Adressraum aus, der beim Start geladen wurde, damit das Programm ausgeführt wird:

Wenn Sie mehr über die Handhabung von Bibliotheken unter Linux erfahren möchten, lesen Sie meinen Artikel Wie man mit dynamischen und statischen Bibliotheken unter Linux umgeht .

Programm debuggen

Sie haben vielleicht bemerkt, dass Sie das Programm in GDB mit run starten können Befehl. Der run Der Befehl akzeptiert Befehlszeilenargumente, wie Sie sie verwenden würden, um das Programm von der Konsole aus zu starten. Das -c1 switch bewirkt, dass das Programm auf Stufe 4 abstürzt. Um das Programm von Anfang an auszuführen, müssen Sie GDB nicht beenden; Verwenden Sie einfach den run erneut befehlen. Ohne -c1 Schalter, führt das Programm eine Endlosschleife aus. Sie müssten es mit Strg+C stoppen .

Sie können ein Programm auch Schritt für Schritt ausführen. In C/C++ ist der Einstiegspunkt main Funktion. Verwenden Sie den Befehl list main um den Teil des Quellcodes zu öffnen, der den main zeigt Funktion:

Die main Die Funktion befindet sich in Zeile 33, also fügen Sie dort einen Haltepunkt hinzu, indem Sie break 33 eingeben :

Führen Sie das Programm aus, indem Sie run eingeben . Wie erwartet stoppt das Programm bei main Funktion. Geben Sie layout src ein um den Quellcode parallel anzuzeigen:

Sie befinden sich jetzt im TUI-Modus (Text User Interface) von GDB. Verwenden Sie die Aufwärts- und Abwärtspfeiltasten, um durch den Quellcode zu blättern.

GDB hebt die auszuführende Zeile hervor. Indem Sie next eingeben (n) können Sie die Befehle zeilenweise ausführen. GBD führt den letzten Befehl aus, wenn Sie keinen neuen angeben. Um den Code schrittweise durchzugehen, drücken Sie einfach die Eingabetaste Schlüssel.

Von Zeit zu Zeit werden Sie feststellen, dass die Ausgabe von TUI etwas beschädigt wird:

Drücken Sie in diesem Fall Strg+L um den Bildschirm zurückzusetzen.

Verwenden Sie Strg+X+A um den TUI-Modus nach Belieben aufzurufen und zu verlassen. Weitere Tastenbelegungen finden Sie im Handbuch.

Um GDB zu beenden, geben Sie einfach quit ein .

Überwachungspunkte

Das Herz dieses Beispielprogramms besteht aus einer Zustandsmaschine, die in einer Endlosschleife läuft. Die Variable n_state ist eine einfache Aufzählung, die den aktuellen Zustand bestimmt:

while(true){
        switch(n_state){
        case State_1:
                std::cout << "State_1 reached" << std::flush;
                n_state = State_2;
                break;
        case State_2:
                std::cout << "State_2 reached" << std::flush;
                n_state = State_3;
                break;
       
        (.....)
       
        }
}

Sie möchten das Programm stoppen, wenn n_state wird auf den Wert State_5 gesetzt . Stoppen Sie dazu das Programm am main Funktion und setze einen Watchpoint für n_state :

watch n_state == State_5

Das Setzen von Watchpoints mit dem Variablennamen funktioniert nur, wenn die gewünschte Variable im aktuellen Kontext verfügbar ist.

Wenn Sie die Ausführung des Programms fortsetzen, indem Sie continue eingeben , sollten Sie eine Ausgabe erhalten wie:

Wenn Sie die Ausführung fortsetzen, stoppt GDB, wenn der Watchpoint-Ausdruck zu false ausgewertet wird :

Sie können Watchpoints für allgemeine Wertänderungen, spezifische Werte und Lese- oder Schreibzugriff angeben.

Ändern von Breakpoints und Watchpoints

Geben Sie info watchpoints ein um eine Liste der zuvor gesetzten Watchpoints auszudrucken:

Haltepunkte und Überwachungspunkte löschen

Wie Sie sehen können, sind Watchpoints Zahlen. Um einen bestimmten Watchpoint zu löschen, geben Sie delete ein gefolgt von der Nummer des Watchpoints. Beispielsweise hat mein Watchpoint die Nummer 2; Um diesen Überwachungspunkt zu entfernen, geben Sie delete 2 ein .

Achtung: Wenn Sie delete verwenden ohne Angabe einer Zahl, alle Watchpoints und Breakpoints werden gelöscht.

Gleiches gilt für Haltepunkte. Im Screenshot unten habe ich mehrere Breakpoints hinzugefügt und eine Liste davon gedruckt, indem ich info breakpoint eingetippt habe :

Um einen einzelnen Haltepunkt zu entfernen, geben Sie delete ein gefolgt von seiner Nummer. Alternativ können Sie einen Haltepunkt entfernen, indem Sie seine Zeilennummer angeben. Zum Beispiel der Befehl clear 78 entfernt Breakpoint Nummer 7, der in Zeile 78 gesetzt ist.

Haltepunkte und Überwachungspunkte deaktivieren oder aktivieren

Anstatt einen Breakpoint oder Watchpoint zu entfernen, können Sie ihn deaktivieren, indem Sie disable eingeben gefolgt von seiner Nummer. Im Folgenden sind die Haltepunkte 3 und 4 deaktiviert und im Codefenster mit einem Minuszeichen gekennzeichnet:

Es ist auch möglich, eine Reihe von Breakpoints oder Watchpoints zu ändern, indem Sie etwas wie disable 2 - 4 eingeben . Wenn Sie die Punkte wieder aktivieren möchten, geben Sie enable ein gefolgt von ihren Nummern.

Bedingte Haltepunkte

Entfernen Sie zuerst alle Breakpoints und Watchpoints, indem Sie delete eingeben . Sie wollen immer noch, dass das Programm am main stoppt Funktion, aber anstatt eine Zeilennummer anzugeben, fügen Sie einen Haltepunkt hinzu, indem Sie die Funktion direkt benennen. Geben Sie break main ein um einen Haltepunkt am main hinzuzufügen Funktion.

Geben Sie run ein um die Ausführung von Anfang an zu beginnen, und das Programm stoppt bei main Funktion.

Die main Funktion enthält die Variable n_state_3_count , die erhöht wird, wenn die Zustandsmaschine Zustand 3 erreicht.

Um einen bedingten Haltepunkt basierend auf dem Wert von n_state_3_count hinzuzufügen Typ:

break 54 if n_state_3_count == 3

Setzen Sie die Ausführung fort. Das Programm führt die Zustandsmaschine dreimal aus, bevor es in Zeile 54 stoppt. Um den Wert von n_state_3_count zu prüfen , geben Sie Folgendes ein:

print n_state_3_count

Haltepunkte bedingt machen

Es ist auch möglich, einen bestehenden Haltepunkt an Bedingungen zu knüpfen. Entfernen Sie den kürzlich hinzugefügten Haltepunkt mit clear 54 , und fügen Sie einen einfachen Haltepunkt hinzu, indem Sie break 54 eingeben . Sie können diesen Haltepunkt bedingt machen, indem Sie Folgendes eingeben:

condition 3 n_state_3_count == 9

Die 3 bezieht sich auf die Haltepunktnummer.

Haltepunkte in anderen Quelldateien setzen

Wenn Sie ein Programm haben, das aus mehreren Quelldateien besteht, können Sie Haltepunkte setzen, indem Sie den Dateinamen vor der Zeilennummer angeben, z. B. break main.cpp:54 .

Fangpunkte

Neben Breakpoints und Watchpoints können Sie auch Catchpoints setzen. Catchpoints gelten für Programmereignisse wie das Ausführen von Systemaufrufen, das Laden gemeinsam genutzter Bibliotheken oder das Auslösen von Ausnahmen.

Um den write abzufangen syscall, der verwendet wird, um in STDOUT zu schreiben, geben Sie ein:

catch syscall write

Jedes Mal, wenn das Programm in die Konsolenausgabe schreibt, unterbricht GDB die Ausführung.

Im Handbuch finden Sie ein ganzes Kapitel über Break-, Watch- und Catchpoints.

Evaluieren und manipulieren Sie Symbole

Das Drucken der Werte von Variablen erfolgt mit dem print Befehl. Die allgemeine Syntax ist print <expression> <value> . Der Wert einer Variablen kann geändert werden, indem Sie Folgendes eingeben:

set variable <variable-name> <new-value>.

Im Screenshot unten habe ich die Variable n_state_3_count angegeben den Wert 123 .

Der /x expression gibt den Wert hexadezimal aus; mit & Operator können Sie die Adresse innerhalb des virtuellen Adressraums drucken.

Wenn Sie sich über den Datentyp eines bestimmten Symbols nicht sicher sind, können Sie ihn mit whatis finden :

Wenn Sie alle Variablen auflisten möchten, die im Bereich main verfügbar sind geben Sie info scope main ein :

Der DW_OP_fbreg Werte beziehen sich auf den Stack-Offset basierend auf dem aktuellen Unterprogramm.

Wenn Sie sich bereits in einer Funktion befinden und alle Variablen im aktuellen Stapelrahmen auflisten möchten, können Sie alternativ info locals verwenden :

Schlagen Sie im Handbuch nach, um mehr über das Untersuchen von Symbolen zu erfahren.

An einen laufenden Prozess anhängen

Der Befehl gdb attach <process-id> ermöglicht das Anhängen an einen bereits laufenden Prozess durch Angabe der Prozess-ID (PID). Zum Glück der coredump Das Programm gibt seine aktuelle PID auf dem Bildschirm aus, sodass Sie sie nicht manuell mit ps oder top suchen müssen.

Starten Sie eine Instanz der Coredump-Anwendung:

./coredump

Das Betriebssystem gibt die PID 2849 an . Öffnen Sie ein separates Konsolenfenster, wechseln Sie in das Quellverzeichnis der Coredump-Anwendung und hängen Sie GDB:

an
gdb attach 2849

GDB stoppt sofort die Ausführung, wenn Sie es anhängen. Geben Sie layout src ein und backtrace um die Aufrufliste zu untersuchen:

Die Ausgabe zeigt den Prozess, der während der Ausführung von std::this_thread::sleep_for<...>(...) unterbrochen wurde Funktion, die in Zeile 92 von main.cpp aufgerufen wurde .

Sobald Sie GDB beenden, läuft der Prozess weiter.

Weitere Informationen zum Anhängen an einen laufenden Prozess finden Sie im GDB-Handbuch.

Durch den Stapel bewegen

Kehren Sie mit up zum Programm zurück zweimal, um im Stack nach oben zu main.cpp zu gelangen :

Normalerweise erstellt der Compiler für jede Funktion oder Methode eine Subroutine. Jede Subroutine hat ihren eigenen Stapelrahmen, also bedeutet eine Bewegung im Stapelrahmen nach oben eine Bewegung im Aufrufstapel nach oben.

Weitere Informationen zur Stapelauswertung finden Sie im Handbuch.

Geben Sie die Quelldateien an

Beim Anhängen an einen bereits laufenden Prozess sucht GDB nach den Quelldateien im aktuellen Arbeitsverzeichnis. Alternativ können Sie die Quellverzeichnisse manuell mit directory angeben Befehl.

Dump-Dateien auswerten

Lesen Sie Linux-Dump-Dateien erstellen und debuggen für Informationen zu diesem Thema.

TL;DR:

  1. Ich nehme an, Sie arbeiten mit einer neueren Version von Fedora
  2. Rufen Sie Coredump mit dem Schalter c1 auf:coredump -c1

  3. Laden Sie die neueste Dumpdatei mit GDB:coredumpctl debug
  4. Öffnen Sie den TUI-Modus und geben Sie layout src ein

Die Ausgabe von backtrace zeigt, dass der Absturz fünf Stack-Frames entfernt von main.cpp passiert ist . Geben Sie ein, um direkt zur fehlerhaften Codezeile in main.cpp zu springen :

Ein Blick in den Quellcode zeigt, dass das Programm versucht hat, einen Zeiger freizugeben, der nicht von einer Speicherverwaltungsfunktion zurückgegeben wurde. Dies führt zu undefiniertem Verhalten und verursachte den SIGABRT .

Debugging ohne Symbole

Wenn keine Quellen verfügbar sind, wird es sehr schwierig. Ich hatte meine ersten Erfahrungen damit, als ich versuchte, Reverse-Engineering-Herausforderungen zu lösen. Es ist auch nützlich, einige Kenntnisse der Assemblersprache zu haben.

Sehen Sie sich anhand dieses Beispiels an, wie es funktioniert.

Gehen Sie in das Quellverzeichnis, öffnen Sie das Makefile , und bearbeiten Sie Zeile 9 wie folgt:

CFLAGS =-Wall -Werror -std=c++11 #-g

Um das Programm neu zu kompilieren, führen Sie make clean aus gefolgt von make und GDB starten. Das Programm hat keine Debugging-Symbole mehr, die durch den Quellcode führen.

Der Befehl info file zeigt die Speicherbereiche und den Einstiegspunkt der Binärdatei:

Der Einstiegspunkt entspricht dem Anfang des .text Bereich, der den eigentlichen Opcode enthält. Geben Sie zum Hinzufügen eines Haltepunkts am Einstiegspunkt break *0x401110 ein Starten Sie dann die Ausführung, indem Sie run eingeben :

Um einen Haltepunkt an einer bestimmten Adresse einzurichten, geben Sie diese mit dem Dereferenzierungsoperator * an .

Wählen Sie die Disassembler-Variante

Bevor Sie sich eingehender mit der Assembly befassen, können Sie auswählen, welche Assembly-Variante verwendet werden soll. Der Standardwert von GDB ist AT&T, aber ich bevorzuge die Intel-Syntax. Ändern Sie es mit:

set disassembly-flavor intel

Öffnen Sie nun die Assembly und registrieren Sie das Fenster, indem Sie layout asm eingeben und layout reg . Sie sollten jetzt eine Ausgabe wie diese sehen:

Konfigurationsdateien speichern

Obwohl Sie bereits viele Befehle eingegeben haben, haben Sie noch nicht mit dem Debuggen begonnen. Wenn Sie eine Anwendung intensiv debuggen oder versuchen, eine Reverse-Engineering-Herausforderung zu lösen, kann es hilfreich sein, Ihre GDB-spezifischen Einstellungen in einer Datei zu speichern.

Die Konfigurationsdatei gdbinit im GitHub-Repository dieses Projekts enthält die zuletzt verwendeten Befehle:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

Das set write on Mit dem Befehl können Sie die Binärdatei während der Ausführung ändern.

Beenden Sie GDB und öffnen Sie es erneut mit der Konfigurationsdatei: gdb -x gdbinit coredump .

Anweisungen lesen

Mit dem c2 Schalter betätigt, stürzt das Programm ab. Das Programm stoppt an der Eingabefunktion, also müssen Sie continue schreiben um mit der Ausführung fortzufahren:

Die idiv Anweisung führt eine ganzzahlige Division mit dem Dividenden im RAX durch register und der als Argument angegebene Divisor. Der Quotient wird in den RAX geladen registrieren, und der Rest wird in RDX geladen .

Aus der Registerübersicht können Sie den RAX sehen enthält 5 , also müssen Sie herausfinden, welcher Wert auf dem Stack an Position RBP-0x4 gespeichert ist .

Speicher lesen

Zum Lesen von Rohspeicherinhalten müssen Sie einige Parameter mehr angeben als zum Lesen von Symbolen. Wenn Sie in der Assembly-Ausgabe etwas nach oben scrollen, können Sie die Aufteilung des Stapels sehen:

Sie interessieren sich am meisten für den Wert von rbp-0x4 denn an dieser Stelle steht das Argument für idiv wird gelagert. Auf dem Screenshot können Sie sehen, dass sich die nächste Variable bei rbp-0x8 befindet , also die Variable bei rbp-0x4 ist 4 Bytes breit.

In GDB können Sie den x verwenden Befehl zum Untersuchen beliebiger Speicherinhalt:

x/ n f u> addr>

Optionale Parameter:

  • n :Wiederholungszahl (Standard:1) bezieht sich auf die Einheitsgröße
  • f :Formatbezeichner, wie in printf
  • u :Einheitsgröße
    • b :Bytes
    • h :Halbwörter (2 Bytes)
    • w :Wort (4 Byte) (Standard)
    • g :Riesenwort (8 Bytes)

Um den Wert bei rbp-0x4 auszudrucken , geben Sie x/u $rbp-4 ein :

Wenn Sie dieses Muster im Hinterkopf behalten, ist es einfach, den Speicher zu untersuchen. Überprüfen Sie den Abschnitt zur Untersuchung des Gedächtnisses im Handbuch.

Manipulieren Sie die Baugruppe

Die arithmetische Ausnahme ist in der Subroutine zeroDivide() aufgetreten . Wenn Sie mit der Pfeiltaste nach oben etwas nach oben scrollen, finden Sie dieses Muster:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp  

Dies wird als Funktionsprolog bezeichnet:

  1. Der Basiszeiger (rbp ) der aufrufenden Funktion wird auf dem Stack abgelegt
  2. Der Wert des Stapelzeigers (rsp ) wird in den Basiszeiger geladen (rbp )

Überspringen Sie dieses Unterprogramm vollständig. Sie können den Aufrufstapel mit backtrace überprüfen . Sie sind Ihrem main nur einen Stapelrahmen voraus Funktion, damit Sie zu main zurückkehren können mit einem einzigen up :

In Ihrem main Funktion finden Sie dieses Muster:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

Die Unterroutine zeroDivide() wird nur bei jump equal (je) eingetragen wird zu true ausgewertet . Sie können dies einfach durch jump-not-equal (jne) ersetzen Anweisung, die den Opcode 0x75 hat (vorausgesetzt, Sie arbeiten auf einer x86/64-Architektur; die Opcodes sind auf anderen Architekturen unterschiedlich). Starten Sie das Programm neu, indem Sie run eingeben . Wenn das Programm an der Eingabefunktion stoppt, manipulieren Sie den Opcode, indem Sie Folgendes eingeben:

set *(unsigned char*)0x401435 = 0x75

Geben Sie schließlich continue ein . Das Programm überspringt die Subroutine zeroDivide() und stürzt nicht mehr ab.

Schlussfolgerung

GDB arbeitet im Hintergrund in vielen integrierten Entwicklungsumgebungen (IDEs), einschließlich Qt Creator und der nativen Debug-Erweiterung für VSCodium.

Es ist hilfreich zu wissen, wie man die Funktionalität von GDB nutzt. Normalerweise können nicht alle Funktionen von GDB von der IDE aus verwendet werden, daher profitieren Sie von Erfahrung mit der Verwendung von GDB von der Befehlszeile aus.


Linux
  1. 7 praktische Tricks zur Verwendung des Linux-wget-Befehls

  2. Linux-Tipps zur Verwendung von GNU Screen

  3. 8 Tipps für die Linux-Kommandozeile

  4. Fehlerbehebung mit dem proc-Dateisystem unter Linux

  5. 5 Tipps für den GNU-Debugger

Linux ss Command Tutorial für Anfänger (8 Beispiele)

GalliumOS:Die Linux-Distribution für Chromebooks

Das vollständige Handbuch zur Verwendung von ffmpeg unter Linux

Tutorial zur Verwendung des Timeout-Befehls unter Linux

Tutorial zur Verwendung des letzten Befehls im Linux-Terminal

Die 20 besten Linux-Debugger für moderne Softwareentwickler