Einführung | Manchmal ist die wichtigste Information, die Sie wissen müssen, wie Ihr aktueller Programmstand dorthin gelangt ist. Es gibt einen Backtrace-Befehl, der Ihnen die aktuelle Funktionsaufrufkette Ihres Programms liefert. In diesem Beitrag erfahren Sie, wie Sie das Stack-Unwinding auf x86_64 implementieren, um einen solchen Traceback zu generieren. |
Diese Links werden online geschaltet, sobald andere Beiträge veröffentlicht werden.
Nehmen Sie das folgende Programm als Beispiel:
void a() { //stopped here } void b() { a(); } void c() { a(); } int main() { b(); c(); }
Wenn der Debugger an der Zeile //stopped here' stoppt, gibt es zwei Möglichkeiten, dorthin zu gelangen: main->b->a oder main->c->a`. Wenn wir mit LLDB einen Breakpoint setzen, die Ausführung fortsetzen und einen Traceback anfordern, dann erhalten wir Folgendes:
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3 frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6 frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14 frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291 frame #4: 0x0000000000400409 a.out`_start + 41
Das bedeutet, wir befinden uns derzeit in Funktion a, a springt von Funktion b, b springt von der Hauptfunktion usw. In den letzten beiden Frames führt der Compiler das Bootstrapping der Hauptfunktion durch.
Die Frage ist nun, wie wir es auf x86_64 umsetzen. Der robusteste Ansatz wäre, den .eh_frame-Teil der ELF-Datei zu analysieren und herauszufinden, wie der Stapel von dort abgewickelt werden kann, aber das wäre mühsam. Sie könnten es mit libunwind oder ähnlichem machen, aber das ist langweilig. Stattdessen gehen wir davon aus, dass der Compiler den Stapel auf irgendeine Weise eingerichtet hat, und durchlaufen ihn manuell. Dazu müssen wir zunächst das Layout des Stapels verstehen.
High | ... | +---------+ +24| Arg 1 | +---------+ +16| Arg 2 | +---------+ + 8| Return | +---------+ EBP+--> |Saved EBP| +---------+ - 8| Var 1 | +---------+ ESP+--> | Var 2 | +---------+ | ... | Low
Wie Sie sehen können, wird der Frame-Zeiger des letzten Stack-Frames am Anfang des aktuellen Stack-Frames gespeichert, wodurch eine verknüpfte Liste von Zeigern erstellt wird. Der Stapel wird basierend auf dieser verknüpften Liste abgewickelt. Wir können die Funktion für den nächsten Frame in der Liste finden, indem wir in der DWARF-Nachricht nach der Rücksprungadresse suchen. Einige Compiler ignorieren die Verfolgung der Frame-Basisadresse von EBP, da diese als Offset von ESP ausgedrückt werden kann und ein zusätzliches Register freigibt. Selbst wenn Optimierungen aktiviert sind, wird die Übergabe von -fno-omit-frame-pointer an GCC oder Clang dazu führen, dass die Konventionen eingehalten werden, auf die wir angewiesen sind.
Wir erledigen die ganze Arbeit in der Funktion print_backtrace:
void debugger::print_backtrace() {
Als Erstes müssen Sie entscheiden, welches Format zum Ausdrucken der Rahmeninformationen verwendet werden soll. Ich habe ein Lambda verwendet, um diese Methode einzuführen:
auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; };
Der erste gedruckte Frame ist der aktuell ausgeführte Frame. Informationen zu diesem Frame erhalten wir, indem wir den aktuellen Programmzähler in DWARF nachschlagen:
auto current_func = get_function_from_pc(get_pc()); output_frame(current_func);
Als nächstes müssen wir den Frame-Zeiger und die Rückgabeadresse der aktuellen Funktion abrufen. Der Frame-Zeiger wird im RBP-Register gespeichert und die Rücksprungadresse beträgt 8 Bytes, gestapelt vom Frame-Zeiger.
auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8);
Jetzt haben wir alle Informationen, die wir brauchen, um den Stack zu erweitern. Ich spule einfach weiter ab, bis der Debugger die Hauptfunktion erreicht, Sie können aber auch anhalten, wenn der Frame-Zeiger 0x0 ist. Dies sind die Funktionen, die Sie aufrufen, bevor Sie die Hauptfunktion aufrufen. Wir erfassen den Frame-Zeiger und die Rücksprungadresse von jedem Frame und drucken die Informationen aus.
while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
Das ist es! Hier ist die gesamte Funktion:
void debugger::print_backtrace() { auto output_frame = [frame_number = 0] (auto&& func) mutable { std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func) << ' ' << dwarf::at_name(func) << std::endl; }; auto current_func = get_function_from_pc(get_pc()); output_frame(current_func); auto frame_pointer = get_register_value(m_pid, reg::rbp); auto return_address = read_memory(frame_pointer+8); while (dwarf::at_name(current_func) != "main") { current_func = get_function_from_pc(return_address); output_frame(current_func); frame_pointer = read_memory(frame_pointer); return_address = read_memory(frame_pointer+8); } }
Natürlich müssen wir diesen Befehl dem Benutzer zugänglich machen.
else if(is_prefix(command, "backtrace")) { print_backtrace(); }
Eine Möglichkeit, diese Funktionalität zu testen, besteht darin, ein Testprogramm mit einer Reihe kleiner Funktionen zu schreiben, die sich gegenseitig aufrufen. Legen Sie ein paar Haltepunkte fest, springen Sie durch den Code und stellen Sie sicher, dass Ihr Traceback korrekt ist.
Wir haben einen langen Weg von einem Programm zurückgelegt, das nur spawnen und sich an andere Programme anhängen konnte. Der vorletzte Artikel dieser Reihe vervollständigt die Debugger-Implementierung durch die Unterstützung des Lesens und Schreibens von Variablen. Bis dahin finden Sie den Code für diesen Beitrag hier.
Das obige ist der detaillierte Inhalt vonLinux-Debugger-Stack-Erweiterung!. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!