Einführung | Variablen sind hinterlistig. Manchmal sitzen sie gerne in der Kasse und landen dann auf dem Stapel, sobald sie sich umdrehen. Zu Optimierungszwecken kann der Compiler sie vollständig aus dem Fenster werfen. Ganz gleich, wie sich Variablen durch den Speicher bewegen, wir brauchen eine Möglichkeit, sie im Debugger zu verfolgen und zu manipulieren. In diesem Artikel lernen Sie den Umgang mit Variablen im Debugger und demonstrieren eine einfache Implementierung mit libelfin. |
Bevor Sie beginnen, stellen Sie bitte sicher, dass Sie die Version von libelfin fbreg in meinem Zweig verwenden. Dies enthält einige Hacks, um das Abrufen der Basisadresse des aktuellen Stack-Frames und das Auswerten einer Liste von Positionen zu unterstützen, die von nativem libelfin nicht bereitgestellt werden. Möglicherweise müssen Sie den Parameter -gdwarf-2 an GCC übergeben, um kompatible DWARF-Nachrichten zu generieren. Aber bevor ich das umsetze, werde ich detailliert beschreiben, wie die Positionskodierung in der neuesten DWARF 5-Spezifikation funktioniert. Wenn Sie mehr wissen möchten, können Sie den Standard hier abrufen.
ZWERGEN-StandortDer Speicherort einer Variablen im Speicher zu einem bestimmten Zeitpunkt wird in der DWARF-Nachricht mithilfe des Attributs DW_AT_location codiert. Eine Standortbeschreibung kann eine einzelne Standortbeschreibung, eine zusammengesetzte Standortbeschreibung oder eine Liste von Standorten sein.
DW_AT_location wird abhängig von der Art der Standortbeschreibung auf drei verschiedene Arten kodiert. exprloc kodiert einfache und zusammengesetzte Positionsbeschreibungen. Sie bestehen aus einer Bytelänge, gefolgt von einem DWARF-Ausdruck oder einer Ortsbeschreibung. Kodierte Standortlisten für loclist und loclistptr, die den Index oder Offset im Abschnitt .debug_loclists bereitstellen, der die tatsächliche Standortliste beschreibt.
Zwerg-AusdruckVerwenden Sie DWARF-Ausdrücke, um die tatsächliche Position einer Variablen zu berechnen. Dazu gehört eine Reihe von Operationen, die Stapelwerte manipulieren. Da viele DWARF-Operationen verfügbar sind, werde ich sie nicht im Detail erklären. Stattdessen gebe ich einige Beispiele für jeden Ausdruck, um Ihnen etwas zu geben, mit dem Sie arbeiten können. Haben Sie auch keine Angst davor; libelfin übernimmt die ganze Komplexität für uns.
DWARF-Typdarstellungen müssen leistungsstark genug sein, um Debugger-Benutzern nützliche Variablendarstellungen bereitzustellen. Benutzer möchten oft in der Lage sein, auf Anwendungsebene und nicht auf Maschinenebene zu debuggen, und sie müssen verstehen, was ihre Variablen tun.
Der DWARF-Typ wird zusammen mit den meisten anderen Debugging-Informationen in DIE codiert. Sie können Eigenschaften haben, die ihren Namen, ihre Kodierung, Größe, Bytes usw. angeben. Es stehen unzählige Typ-Tags zur Darstellung von Zeigern, Arrays, Strukturen, Typdefinitionen und allem anderen zur Verfügung, das Sie in einem C- oder C++-Programm sehen könnten.
Nehmen Sie diese einfache Struktur als Beispiel:
struct test{ int i; float j; int k[42]; test* next; };
Der übergeordnete DIE dieser Struktur sieht folgendermaßen aus:
< 1><0x0000002a> DW_TAG_structure_type DW_AT_name "test" DW_AT_byte_size 0x000000b8 DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000001
Was oben steht, ist, dass wir eine Struktur namens test mit der Größe 0xb8 haben, die in Zeile 1 von test.cpp deklariert ist. Als nächstes gibt es eine Reihe von Sub-DIEs, die die Mitglieder beschreiben.
< 2><0x00000032> DW_TAG_member DW_AT_name "i" DW_AT_type <0x00000063> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000002 DW_AT_data_member_location 0 < 2><0x0000003e> DW_TAG_member DW_AT_name "j" DW_AT_type <0x0000006a> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000003 DW_AT_data_member_location 4 < 2><0x0000004a> DW_TAG_member DW_AT_name "k" DW_AT_type <0x00000071> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000004 DW_AT_data_member_location 8 < 2><0x00000056> DW_TAG_member DW_AT_name "next" DW_AT_type <0x00000084> DW_AT_decl_file 0x00000001 test.cpp DW_AT_decl_line 0x00000005 DW_AT_data_member_location 176(as signed = -80)
Jedes Mitglied hat einen Namen, einen Typ (der ein DIE-Offset ist), eine Deklarationsdatei und -zeile sowie einen Byte-Offset zur Struktur, in der sich sein Mitglied befindet. Seine Typpunkte sind wie folgt.
< 1><0x00000063> DW_TAG_base_type DW_AT_name "int" DW_AT_encoding DW_ATE_signed DW_AT_byte_size 0x00000004 < 1><0x0000006a> DW_TAG_base_type DW_AT_name "float" DW_AT_encoding DW_ATE_float DW_AT_byte_size 0x00000004 < 1><0x00000071> DW_TAG_array_type DW_AT_type <0x00000063> < 2><0x00000076> DW_TAG_subrange_type DW_AT_type <0x0000007d> DW_AT_count 0x0000002a < 1><0x0000007d> DW_TAG_base_type DW_AT_name "sizetype" DW_AT_byte_size 0x00000008 DW_AT_encoding DW_ATE_unsigned < 1><0x00000084> DW_TAG_pointer_type DW_AT_type <0x0000002a>
Wie Sie sehen können, ist int auf meinem Laptop ein 4-Byte-Ganzzahltyp mit Vorzeichen und float eine 4-Byte-Gleitkommazahl. Der Integer-Array-Typ hat 2a Elemente, indem er auf einen int-Typ als Elementtyp und auf sizetype (stellen Sie sich das als size_t vor) als Indextyp zeigt. Der Test *-Typ ist DW_TAG_pointer_type, der sich auf den Test DIE bezieht.
Implementierung eines einfachen VariablenlesersWie oben erwähnt, übernimmt Libelfin den Großteil der Komplexität für uns. Es implementiert jedoch nicht alle Methoden zur Darstellung variabler Positionen und die Handhabung dieser in unserem Code wird sehr komplex. Daher entscheide ich mich jetzt dafür, nur exprloc zu unterstützen. Bitte fügen Sie bei Bedarf Unterstützung für weitere Ausdruckstypen hinzu. Wenn Sie wirklich mutig sind, senden Sie bitte einen Patch an libelfin, um den erforderlichen Support zu vervollständigen!
Der Umgang mit Variablen umfasst hauptsächlich das Auffinden verschiedener Teile im Speicher oder in Registern, und das Lesen oder Schreiben ist dasselbe wie zuvor. Der Einfachheit halber erkläre ich Ihnen nur, wie Sie das Lesen umsetzen.
Zuerst müssen wir libelfin mitteilen, wie es Register aus unserem Prozess lesen soll. Wir erstellen eine Klasse, die von expr_context erbt, und verwenden ptrace, um alles zu verarbeiten:
class ptrace_expr_context : public dwarf::expr_context { public: ptrace_expr_context (pid_t pid) : m_pid{pid} {} dwarf::taddr reg (unsigned regnum) override { return get_register_value_from_dwarf_register(m_pid, regnum); } dwarf::taddr pc() override { struct user_regs_struct regs; ptrace(PTRACE_GETREGS, m_pid, nullptr, ®s); return regs.rip; } dwarf::taddr deref_size (dwarf::taddr address, unsigned size) override { //TODO take into account size return ptrace(PTRACE_PEEKDATA, m_pid, address, nullptr); } private: pid_t m_pid; };
Das Lesen wird von der Funktion read_variables in unserer Debugger-Klasse übernommen:
void debugger::read_variables() { using namespace dwarf; auto func = get_function_from_pc(get_pc()); //... }
Als erstes haben wir oben die Funktion gefunden, in der wir uns gerade befinden. Anschließend müssen wir die Einträge in dieser Funktion durchlaufen, um die Variablen zu finden:
for (const auto& die : func) { if (die.tag == DW_TAG::variable) { //... } }
Wir erhalten die Standortinformationen, indem wir nach dem DW_AT_location-Eintrag in DIE suchen:
auto loc_val = die[DW_AT::location];
Dann stellen wir sicher, dass es sich um einen Ausdruck handelt, und bitten libelfin, unseren Ausdruck zu bewerten:
if (loc_val.get_type() == value::type::exprloc) { ptrace_expr_context context {m_pid}; auto result = loc_val.as_exprloc().evaluate(&context);
Nachdem wir den Ausdruck ausgewertet haben, müssen wir den Inhalt der Variablen lesen. Es kann im Speicher oder in Registern liegen, daher behandeln wir beide Fälle:
switch (result.location_type) { case expr_result::type::address: { auto value = read_memory(result.value); std::cout << at_name(die) << " (0x" << std::hex << result.value << ") = " << value << std::endl; break; } case expr_result::type::reg: { auto value = get_register_value_from_dwarf_register(m_pid, result.value); std::cout << at_name(die) << " (reg " << result.value << ") = " << value << std::endl; break; } default: throw std::runtime_error{"Unhandled variable location"}; }
Sie können sehen, dass ich den Wert anhand des Variablentyps ohne Erklärung ausgedruckt habe. Hoffentlich können Sie mit diesem Code sehen, wie das Schreiben von Variablen oder die Suche nach Variablen mit einem bestimmten Namen unterstützt wird.
Endlich können wir dies zu unserem Befehlsparser hinzufügen:
else if(is_prefix(command, "variables")) { read_variables(); }
Schreiben Sie eine kleine Funktion mit einigen Variablen, kompilieren Sie sie ohne Optimierung und mit Debug-Informationen und prüfen Sie dann, ob Sie den Wert der Variablen lesen können. Versuchen Sie, an die Speicheradresse zu schreiben, an der die Variable gespeichert ist, und beobachten Sie, wie das Programm sein Verhalten ändert.
Es gibt bereits neun Artikel und der letzte ist noch übrig! Beim nächsten Mal werde ich einige fortgeschrittenere Konzepte besprechen, die für Sie von Interesse sein könnten. Den Code für diesen Beitrag finden Sie nun hier.
Das obige ist der detaillierte Inhalt vonEntdecken Sie Techniken zur Variablenverarbeitung in Linux-Debuggern!. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!