Debuggen mit GDB: Tiefer graben

0
162
Shutterstock/Nicescene

Der leistungsstarke GNU Debugger GDB kehrt an die Front zurück. Wir tauchen tiefer in Stacks, Backtraces, Variablen, Core-Dumps, Frames und Debugging ein als je zuvor. Begleiten Sie uns für eine völlig neue, erweiterte Einführung in GDB.

Was ist GDB?

Wenn Sie neu im Debugging im Allgemeinen oder im Besonderen mit GDB—dem GNU-Debugger—im Allgemeinen sind, sollten Sie zuerst unseren Artikel Debugging mit GDB: Erste Schritte lesen und dann zu diesem zurückkehren. Dieser Artikel baut weiter auf den dort präsentierten Informationen auf.

Installieren von GDB

Zur Installation GDB auf Ihrer Debian/Apt-basierten Linux-Distribution (wie Ubuntu und Mint), führen Sie den folgenden Befehl in Ihrem Terminal aus:

sudo apt install gdb

So installieren Sie GDBFühren Sie auf Ihrer RedHat/Yum-basierten Linux-Distribution (wie RHEL, Centos und Fedora) den folgenden Befehl in Ihrem Terminal aus:

sudo yum install gdb

Stacks, Backtraces und Frames!

Es klingt wie Äpfel, Kuchen und Pfannkuchen! (Und bis zu einem gewissen Grad ist es das auch.) Genau wie Äpfel und Pfannkuchen uns ernähren, sind Stapel, Backtraces und Frames das Brot und Butter aller Entwickler, die in GDB debuggen, und die darin enthaltenen Informationen nähren einen Entwickler, der hungrig darauf ist, seine zu entdecken oder ihr Fehler im Quellcode.

RELATEDDebugging mit GDB: Erste Schritte

Der Befehl bt GDB erzeugt nacheinander einen Backtrace aller aufgerufenen Funktionen und präsentiert uns die Frames(die Funktionen) nacheinander aufgelistet. Ein Stack ist einem Backtrace insofern ziemlich ähnlich, als ein Stack eine Übersicht oder eine Liste von Funktionen ist, die zu einem Absturz, einer Situation oder einem Problem geführt haben, während ein Backtrace der Befehl ist, den wir ausgeben, um einen Stack zu erhalten.

Davon abgesehen werden die Begriffe oft synonym verwendet und man könnte sagen “Können Sie mir einen Stapel besorgen?” oder “Sehen wir uns die Rückverfolgung an,” was die Bedeutung beider Wörter in jedem Satz jeweils etwas umkehrt.

Und als Auffrischung von unserem vorherigen Artikel über GDB ein Rahmenist im Grunde eine einzelne Funktion, die in einem Backtrace aller verschachtelten Funktionsaufrufe aufgeführt ist—zum Beispiel die main()-Funktion, die zuerst beginnt (aufgelistet am Ende eines Backtrace) und dann main() namens math_function(), die wiederum namens do_the_maths() etc.

Wenn das etwas kompliziert klingt, sehen Sie sich zuerst Debugging mit GDB: Erste Schritte an.

Bei Single-Thread-Programmen wird GDB so gut wie immer (wenn nicht immer) den abstürzenden (und einzigen) Thread korrekt erkennen, wenn wir unser Debugging-Abenteuer beginnen. Dies macht es einfach, den bt-Befehl sofort auszuführen, wenn wir gdb eingeben und uns am (gdb)-Prompt befinden, da GDB uns sofort den für den beobachteten Absturz relevanten Backtrace anzeigt.

Single-Threaded oder Multithreaded?

Eine sehr wichtige Sache, die beim Debuggen von Core-Dumps zu beachten (und zu wissen) ist, ist, ob das zu debuggende Programm (oder genauer gesagt, < i>war) Singlethread oder Multithread?

In unserem vorherigen Beispiel/Artikel haben wir uns einen einfachen Backtrace angesehen, bei dem ein Satz von Frames aus einem selbstgeschriebenen Programm angezeigt wird. Das Programm war Single-Threaded: Es wurden keine anderen Ausführungs-Threads abgezweigt aus dem Code heraus.

Sobald wir jedoch mehrere Threads haben, wird ein einzelner bt (Backtrace)-Befehl Erzeuge nur den Backtrace für den aktuell in GDB ausgewählten Thread.

GDB wählt automatisch den abstürzenden Absturz aus, und selbst bei Multithread-Programmen ist dies in mehr als 99% der Fälle korrekt. Es gibt nur gelegentlich Fälle, in denen GDB den abstürzenden Thread mit einem anderen verwechselt. Dies kann beispielsweise passieren, wenn das Programm in zwei Threads gleichzeitig abgestürzt ist. In den letzten 10 Jahren habe ich dies nur wenige Male beim Umgang mit Tausenden von Core-Dumps beobachtet.

Um den Unterschied zwischen dem in unserem letzten Artikel verwendeten Beispiel und einer echten Multithread-Anwendung zu demonstrieren, habe ich den MySQL-Server 8.0.25 im Debug-Modus (mit anderen Worten mit hinzugefügten Debug-Symbolen/Instrumentierung) mit dem Build-Skript im MariaDB-a GitHub erstellt repo und ließ die SQL-Daten des pquery-Frameworks für eine Weile laufen, was bald den MySQL-Debug-Server zum Absturz brachte.

Wie Sie sich vielleicht aus unserem vorherigen Artikel erinnern, ist ein Core-Dump eine Datei, die vom Betriebssystem oder in einigen Fällen von der Anwendung selbst erstellt wird (wenn sie über integrierte Crash-Handling-/Core-Dumping-Vorkehrungen verfügt), die dann mit GDB analysiert werden. Eine Core-Datei wird normalerweise als Datei mit eingeschränkten Rechten geschrieben (um vertrauliche Informationen im Speicher zu schützen), und Sie müssen wahrscheinlich Ihr Superuser-Konto (dh root) verwenden, um darauf zuzugreifen.

Lassen Sie es gehen ;s tauchen direkt in den mit gdb bin/mysqld erzeugten Core-Dump ein $(ls data/*core*):

Und ein paar Sekunden später beendet GDB das Laden und bringt uns zum GDB-Prompt:

Die verschiedenen Neuen LWPMeldungen (die in der vollständigen Ausgabe noch zahlreicher waren) geben einen guten Hinweis darauf, dass dieses Programm multithreaded war. Der Begriff LWP steht für Light Weight Process. Sie können es sich als äquivalent zu jeweils einem einzelnen Thread vorstellen, die zusammen eine Liste aller Threads erstellen, die GDB bei der Analyse des Kerns entdeckt hat. Beachten Sie, dass GDB dies im Voraus tun muss, damit der abstürzende Thread wie zuvor beschrieben gefunden wird.

Wie wir in der letzten Zeile des ersten GDB-Startbilds oben lesen können, hat GDB außerdem eine Aktion zum Lesen von Symbolen aus bin/mysqld initiiert. Ohne die in die Binärdatei eingebauten/kompilierten Debug-Symbole hätten wir einige oder die meisten Frames mit einem Funktionsnamen ?? markiert gesehen. Außerdem würden für diese Funktionsnamen keine Variablenanzeigen angezeigt.

Dieses Problem (unauflösbare Frames, die beim Debuggen bestimmter optimierter/entfernter Binärdateien auftreten, deren Debug-Symbol entfernt/entfernt wurde) lässt sich nicht leicht beheben. Wenn Sie dies beispielsweise auf einer Datenbankserver-Binärdatei auf Produktionsebene sehen würden (bei der die Debug-Symbole entfernt/entfernt wurden, um die Laufzeit zu optimieren usw.), müssten Sie im Allgemeinen komplexere Verfahren befolgen, z. B Erstellen Sie einen vollständigen Stack-Trace für mysqld.

Backtraces!

Da wir den MySQL-Server mit Debug-Symbolen kompiliert haben, ist ein Backtrace wird in unserem Fall alle Funktionsnamen korrekt anzeigen. Wir geben einen bt-Befehl an der (gdb)-Eingabeaufforderung aus und unsere Backtrace-Ausgabe sieht wie folgt aus:

Wie sehen wir also einen Backtrace für alle Threads oder einen anderen Thread? Dies kann mit den Befehlen thread apply all bt oder thread 2 erreicht werden; bt bzw. Wir können die 2 im letzten Befehl austauschen, um auf einen anderen Thread zuzugreifen usw. Während die Ausgabe des Threads apply all bt hier etwas ausführlicher ist, ist hier die Ausgabe beim Wechseln zu einem anderen Thread und Erhalten eines Backtrace für diesen Thread:

Ein sorgfältiges Lesen von Computerfehlerprotokollen oder -spuren wird wie immer mehr Details preisgeben leicht übersehen, wenn man nur auf Informationen schaut. Dies ist eine echte Fähigkeit. Einer meiner früheren IT-Manager hat mich auf die große Notwendigkeit aufmerksam gemacht, und ich gebe hiermit die gleichen Informationen an alle begeisterten Leser dieses Artikels weiter. Um diese Aussage mit einigen Beweisen zu untermauern, sehen Sie sich die erstellte Rückverfolgung genau an, und Sie werden die Begriffe . bemerkenlisten_for_connection_event, Umfrage, Mysqld_socket_listener und connection_event_loop für Mysqld_socket_listener. Es ist ganz klar: Dieser Thread wartet auf Eingaben.

Dies ist nur ein Leerlauf-Thread, der wahrscheinlich darauf wartete, dass ein MySQL-Client eine Verbindung herstellt oder einen neuen Befehl oder ähnliches eingibt. Mit anderen Worten, es wäre so gut wie null wert, wenn man diesen Thread weiter debuggt.

Dies bringt uns auch zurück zu der Tatsache, wie praktisch es ist, dass GDB uns den abstürzenden Thread beim Start automatisch präsentiert. Alles, was wir tun müssen, um unser Debugging-Abenteuer zu starten, ist eine Rückverfolgung. Bei der Analyse mehrerer Threads und deren Interaktion ist es dann sinnvoll, mit dem Thread-Befehl zwischen den Threads zu springen. Beachten Sie, dass dies zu t abgekürzt werden kann:

Interessanterweise haben wir hier Thread 3, der sich auch in einer Polling-Schleife befindet und wie folgt aussieht (LinuxAIOHandler::poll), obwohl er sich in diesem Fall auf der Betriebssystem-/Festplattenebene befindet (wie durch die Begriffe Linux , AIO und Handler), und ein genauerer Blick zeigt, dass es anscheinend darauf wartet, dass AIO abgeschlossen ist: fil_aio_wait.

Wie Sie sehen, gibt es viele Informationen über den Zustand eines Programms in dem Moment, in dem es abstürzt, was man aus den Protokollen ablesen kann, wenn man genau hinschaut.

Hier ein Tipp: Sie können den Befehl set log on in GDB verwenden, wenn Sie alle Informationen auf der Festplatte speichern möchten, damit Sie die Ausgabe später einfach durchsuchen können, und Sie können set log off verwenden, um den Vorgang zu beenden Ausgabe-Trace. Die Informationen werden standardmäßig in gdb.txt gespeichert.

In Frames springen

Wie wir gesehen haben, ist es möglich, zwischen Threads zu wechseln und sogar einen Backtrace für alle Protokolle auf einmal zu erhalten, und es ist genauso möglich, in einzelne Frames zu springen! Wir können sogar—vorausgesetzt, dass der Quellcode auf der Festplatte verfügbar ist und am ursprünglichen Speicherort der Festplatte gespeichert ist (dh im selben Quellcodeverzeichnis, das beim Erstellen des Produkts verwendet wurde)—siehe Quellcode für a bestimmten Rahmen, in dem wir uns befinden.

Hier ist etwas Vorsicht geboten. Es ist ziemlich einfach, Binärdateien, Code und Core-Dumps nicht zuzuordnen. Beispielsweise ist der Versuch, einen mit Version v1.0 eines bestimmten Programms erstellten Core-Dump zu analysieren, wahrscheinlich nicht kompatibel mit der Binärdatei der Version v1.01, die etwas später mit v1.01-Code kompiliert wurde. Außerdem konnte man nicht [immer] den Quellcode von v1.01 verwenden, um einen Core-Dump zu debuggen, der mit der Version v1.0 eines Programms geschrieben wurde, selbst wenn auch die v1.0-Binärdatei verfügbar ist.

< p>Das Wort immer wurde als optional eingefügt, wie manchmal—wenn sich der Code in diesem Abschnitt des Codes und Programms, das debuggt wird, seit der letzten Version nicht geändert hat—es könnte möglich sein, älteren Quellcode zu verwenden.

Diese Praxis ist vielleicht verpönt, da ein paar einfache Änderungen im Code dazu führen können, dass die Codezeilen nicht mehr dem Binär- und/oder Core-Dump entsprechen. Es ist am besten, entweder niemals verschiedene Versionen des Quellcodes, der Binärdateien und der Core-Dumps zu mischen oder sich nur auf den Core-Dump und die Binärdatei zu verlassen, beide derselben Version, ohne den Quellcode oder mit dem Quellcode, auf den nur manuell verwiesen wird.

Wenn Sie jedoch viele Kerne analysieren, die Kunden gesendet haben, oft mit begrenzten Informationen, kann man manchmal eine etwas andere Version des Quellcodes verwenden und vielleicht sogar eine etwas andere Binärdatei (weniger wahrscheinlich). Beachten Sie jedoch immer, dass die von GDB bereitgestellten Informationen sehr wahrscheinlich teilweise oder wahrscheinlich vollständig ungültig sein werden. GDB warnt Sie auch beim Start, wenn es eine Diskrepanz zwischen Kern und Binärdatei erkennt.

In unserem Beispiel werden der Quellcode, die Binärdatei und der Kernauszug alle mit derselben Version erstellt des Quellcodes und miteinander, und wir können GDB daher glücklich vertrauen, wenn es Ausgaben wie Backtraces erzeugt.

Es gibt hier noch eine weitere kleine Ausnahme, und das ist das Zertrümmern von Stapeln. In einem solchen Fall werden Sie entweder Fehlermeldungen in der GDB beobachten, siehe ?? Frame-Namen—ähnlich der oben beschriebenen Situation (dieses Mal jedoch aufgrund der Unlesbarkeit eines Core-Dumps in Kombination mit der Binärdatei)—oder der Stack sieht wirklich seltsam und falsch aus. Meistens wird es ganz klar sein. Manchmal kann ein wirklich schlimmer Fehler zu einem Stack-Smashing führen.

Lassen Sie uns nun in einen Frame springen und sehen, wie einige unserer Variablen und unser Code aussehen:

Variablen

< img src="http://www.cloudsavvyit.com/pagespeed_static/1.JiBnMqyl6S.gif" />

t 1 bt f 7 Frame 8 p *thd p thd

Hier haben wir verschiedene Befehle eingegeben, um zum richtigen Thread zu navigieren und einen Backtrace auszuführen (t 1 führte uns zum ersten Thread, in unserem Beispiel zum abstürzenden Thread, gefolgt vom Backtrace-Befehl bt) und sprangen anschließend zu Frame 7 und dann Frame 8 mit den Befehlen f7 bzw. Frame 8. Sie können sehen, wie man, ähnlich wie beim Thread-Befehl, den Frame-Befehl auf den ersten Buchstaben f abkürzen kann.

Schließlich haben wir versucht, auf die Variable thd zuzugreifen, obwohl dies aus dem Trace heraus optimiert wurde/Core-Dump für diesen speziellen Frame. Die Informationen sind jedoch verfügbar, wenn wir einfach in den richtigen Frame springen, der die Variable zur Verfügung hat und nicht optimiert wurde (ein bisschen Trial-and-Error ist möglicherweise erforderlich):

In den letzten beiden Screenshots oben habe ich zwei verschiedene Arten der Eingabe des Befehls print (wieder abgekürzt wie nur p) gezeigt, die erste mit einem führenden * für den Variablennamen, die zweite ohne).

Das Interessante daran ist, dass die zweite häufiger verwendet wird, aber im Allgemeinen nur eine Speicheradresse für die fragliche Variable liefert, was nicht sehr praktisch ist. Die *-Version des Befehls (p *thd) löst stattdessen die Variable in ihren vollständigen Inhalt auf. Außerdem kennt GDB den Variablentyp, sodass keine Typumwandlung erforderlich ist (den Wert in einen anderen Variablentyp umwandeln).

Zusammenfassung

In diesem ausführlicheren GDB-Leitfaden haben wir uns mit Stacks, Backtraces, Variablen, Core-Dumps, Frames und Debugging befasst. Wir haben einige GBD-Beispiele studiert und einige wichtige Tipps für den begeisterten Leser gegeben, wie man gut und erfolgreich debuggt. Wenn Ihnen das Lesen dieses Artikels gefallen hat, lesen Sie unseren Artikel How Linux Signals Work: SIGINT, SIGTERM und SIGKILL.