GDB beschädigter Stack-Frame - Wie debugge ich?

113

Ich habe die folgende Stapelverfolgung. Ist es möglich, daraus etwas Nützliches für das Debuggen zu erkennen?

Program received signal SIGSEGV, Segmentation fault.
0x00000002 in ?? ()
(gdb) bt
#0  0x00000002 in ?? ()
#1  0x00000001 in ?? ()
#2  0xbffff284 in ?? ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)
(gdb) 

Wo soll ich anfangen Segmentation fault, den Code zu betrachten, wenn wir einen bekommen , und der Stack-Trace ist nicht so nützlich?

HINWEIS: Wenn ich den Code poste, geben mir die SO-Experten die Antwort. Ich möchte die Anleitung von SO übernehmen und die Antwort selbst finden, daher poste ich den Code hier nicht. Entschuldigung.

Sangeeth Saravanaraj
quelle
Wahrscheinlich ist Ihr Programm ins Unkraut gesprungen - können Sie etwas vom Stapelzeiger wiederherstellen?
Carl Norum
1
Eine andere zu berücksichtigende Sache ist, wenn der Rahmenzeiger richtig gesetzt ist. Bauen Sie ohne Optimierungen oder übergeben Sie eine Flagge wie -fno-omit-frame-pointer? Bei Speicherbeschädigungen ist dies valgrindmöglicherweise ein geeigneteres Tool, wenn dies für Sie eine Option ist.
FatalError

Antworten:

155

Diese gefälschten Adressen (0x00000002 und dergleichen) sind tatsächlich PC-Werte, keine SP-Werte. Wenn Sie diese Art von SEGV mit einer gefälschten (sehr kleinen) PC-Adresse erhalten, ist dies in 99% der Fälle auf den Aufruf eines gefälschten Funktionszeigers zurückzuführen. Beachten Sie, dass virtuelle Aufrufe in C ++ über Funktionszeiger implementiert werden, sodass sich jedes Problem mit einem virtuellen Aufruf auf dieselbe Weise manifestieren kann.

Ein indirekter Aufruf - Befehl schiebt nur den PC nach dem Anruf auf den Stapel und legt dann den PC auf den Zielwert (Schein- in diesem Fall), so dass , wenn dies ist , was passiert ist , können Sie diese leicht rückgängig machen , indem Sie manuell den PC aus dem Stapel knallt . In 32-Bit-x86-Code tun Sie einfach:

(gdb) set $pc = *(void **)$esp
(gdb) set $esp = $esp + 4

Mit 64-Bit-x86-Code benötigen Sie

(gdb) set $pc = *(void **)$rsp
(gdb) set $rsp = $rsp + 8

Dann sollten Sie in der Lage sein, a btauszuführen und herauszufinden, wo sich der Code wirklich befindet.

In den anderen 1% der Fälle ist der Fehler auf das Überschreiben des Stapels zurückzuführen, normalerweise durch Überlaufen eines auf dem Stapel gespeicherten Arrays. In diesem Fall können Sie möglicherweise mit einem Tool wie valgrind mehr Klarheit über die Situation gewinnen

Chris Dodd
quelle
5
@ George: gdb executable corefileöffnet gdb mit der ausführbaren Datei und der Kerndatei, an welchem ​​Punkt Sie tun können bt(oder die obigen Befehle gefolgt von bt) ...
Chris Dodd
2
@mk .. ARM verwendet den Stack nicht für Rücksprungadressen, sondern stattdessen das Linkregister. Daher tritt dieses Problem im Allgemeinen nicht auf, oder wenn dies der Fall ist, liegt dies normalerweise an einer anderen Stapelbeschädigung.
Chris Dodd
2
Selbst in ARM, denke ich, werden alle Allzweckregister und LR im Stapel gespeichert, bevor die aufgerufene Funktion ausgeführt wird. Sobald die Funktion beendet ist, wird der Wert von LR in den PC eingefügt und daher kehrt die Funktion zurück. Wenn also der Stack beschädigt ist, können wir sehen, dass ein falscher Wert für den PC richtig ist. In diesem Fall führt das Anpassen des Stapelzeigers möglicherweise zu einem geeigneten Stapel und hilft beim Debuggen des Problems. Was denken Sie? Bitte lassen Sie mich Ihre Gedanken wissen. Danke dir.
mk ..
1
Was bedeutet Schwindel?
Danny Lo
5
ARM ist nicht x86 - sein Stapelzeiger wird aufgerufen sp, nicht espoder rsp, und sein Aufrufbefehl speichert die Rücksprungadresse im lrRegister, nicht auf dem Stapel. Also für ARM, alles , was Sie wirklich brauchen , ist der Anruf rückgängig zu machen set $pc = $lr. Wenn $lres ungültig ist, haben Sie ein viel schwierigeres Problem beim Abwickeln.
Chris Dodd
44

Wenn die Situation ziemlich einfach ist, ist Chris Dodds Antwort die beste. Es sieht so aus, als wäre es durch einen NULL-Zeiger gesprungen.

Es ist jedoch möglich, dass sich das Programm vor dem Absturz selbst in Fuß, Knie, Nacken und Auge schoss - überschrieb den Stapel, brachte den Rahmenzeiger durcheinander und andere Übel. Wenn ja, dann zeigt das Enträtseln des Haschischs wahrscheinlich nicht Kartoffeln und Fleisch.

Die effizientere Lösung besteht darin, das Programm unter dem Debugger auszuführen und die Funktionen zu überschreiten, bis das Programm abstürzt. Sobald eine Absturzfunktion identifiziert wurde, starten Sie erneut und rufen Sie diese Funktion auf und bestimmen Sie, welche aufgerufene Funktion den Absturz verursacht. Wiederholen Sie diesen Vorgang, bis Sie die einzelne fehlerhafte Codezeile gefunden haben. In 75% der Fälle ist die Korrektur dann offensichtlich.

In den anderen 25% der Situationen ist die sogenannte beleidigende Codezeile ein roter Hering. Es wird auf (ungültige) Bedingungen reagieren, die viele Zeilen zuvor eingerichtet wurden - vielleicht Tausende von Zeilen zuvor. Wenn dies der Fall ist, hängt der beste gewählte Kurs von vielen Faktoren ab: hauptsächlich von Ihrem Verständnis des Codes und Ihrer Erfahrung damit:

  • Vielleicht führt das Setzen eines Debugger-Überwachungspunkts oder das Einfügen von Diagnosen printffür kritische Variablen zu den erforderlichen A ha!
  • Vielleicht bietet das Ändern der Testbedingungen mit unterschiedlichen Eingaben mehr Einblick als das Debuggen.
  • Vielleicht zwingt Sie ein zweites Paar Augen dazu, Ihre Annahmen zu überprüfen oder übersehene Beweise zu sammeln.
  • Manchmal reicht es aus, zum Abendessen zu gehen und über die gesammelten Beweise nachzudenken.

Viel Glück!

Wallyk
quelle
13
Wenn ein zweites Paar Augen nicht verfügbar ist, haben sich Gummienten als Alternativen bewährt.
Matt
2
Das Abschreiben des Endes eines Puffers kann dies auch. Es stürzt möglicherweise nicht ab, wenn Sie das Ende des Puffers abschreiben, aber wenn Sie die Funktion verlassen, stirbt es.
Phyatt
Kann
user202729
28

Angenommen, der Stapelzeiger ist gültig ...

Es kann unmöglich sein, genau zu wissen, wo das SEGV vom Backtrace auftritt - ich denke, die ersten beiden Stapelrahmen werden vollständig überschrieben. 0xbffff284 scheint eine gültige Adresse zu sein, die nächsten beiden jedoch nicht. Für einen genaueren Blick auf den Stapel können Sie Folgendes versuchen:

gdb $ x / 32ga $ rsp

oder eine Variante (ersetzen Sie die 32 durch eine andere Nummer). Dadurch wird eine bestimmte Anzahl von Wörtern (32) ausgehend vom Stapelzeiger von Riesengröße (g) ausgedruckt, der als Adressen (a) formatiert ist. Geben Sie 'help x' ein, um weitere Informationen zum Format zu erhalten.

In diesem Fall ist es möglicherweise keine schlechte Idee, Ihren Code mit einigen Sentinel-Drucken zu instrumentieren.

Manabear
quelle
Unglaublich hilfreich, danke - ich hatte einen Stapel, der nur drei Frames zurückging und dann auf "Backtrace gestoppt: vorheriger Frame identisch mit diesem Frame (beschädigter Stack?)" Drückte; Ich habe so etwas in Code in einem CPU-Ausnahmehandler schon einmal gemacht, konnte mich aber nicht erinnern, info symbolwie dies in gdb gemacht wurde.
Leander
22
FWIW auf 32-Bit-ARM-Geräten: x/256wa $sp =)
Leander
2
@leander Kannst du mir sagen, was X / 256wa ist? Ich brauche es für 64-Bit-ARM. Im Allgemeinen ist es hilfreich, wenn Sie erklären können, was es ist.
mk ..
5
Gemäß der Antwort ist 'x' = Speicherort untersuchen; Es druckt eine Anzahl von 'w' = Wörtern (in diesem Fall 256) aus und interpretiert sie als 'a' = Adressen. Weitere Informationen finden Sie im GDB-Handbuch unter sourceware.org/gdb/current/onlinedocs/gdb/Memory.html#Memory .
Leander
7

Sehen Sie sich einige Ihrer anderen Register an, um festzustellen, ob in einem von ihnen der Stapelzeiger zwischengespeichert ist. Von dort aus können Sie möglicherweise einen Stapel abrufen. Wenn dies eingebettet ist, wird der Stapel häufig an einer bestimmten Adresse definiert. Damit kann man manchmal auch einen anständigen Stack bekommen. Dies alles setzt voraus, dass Ihr Programm beim Springen in den Hyperraum nicht den gesamten Speicher gekotzt hat ...

Michael Dorgan
quelle
3

Wenn es sich um ein Stapelüberschreiben handelt, entsprechen die Werte möglicherweise etwas, das aus dem Programm erkennbar ist.

Zum Beispiel habe ich mir gerade den Stapel angesehen

(gdb) bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000000342d in ?? ()
#2  0x0000000000000000 in ?? ()

und 0x342dist 13357, was sich als Knoten-ID herausstellte, als ich die Anwendungsprotokolle danach durchsuchte. Dies half sofort dabei, Kandidatenstellen einzugrenzen, an denen das Überschreiben des Stapels aufgetreten sein könnte.

Craig Ringer
quelle