Entdecken Sie Techniken zur Variablenverarbeitung in Linux-Debuggern!
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. |
- Bereiten Sie die Umgebung vor
- Haltepunkt
- Register und Speicher
- ELF und ZWERG
- Quellcode und Signale
- Schritt-für-Schritt-Ausführung auf Quellcode-Ebene
- Haltepunkte auf Quellenebene
- Stack-Erweiterung
- Variablen verarbeiten
- Fortgeschrittene Themen
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.
- Einfache Positionsbeschreibung: Beschreibt die Position eines zusammenhängenden Teils (normalerweise aller Teile) eines Objekts. Eine einfache Ortsbeschreibung kann einen Ort im adressierbaren Speicher oder einem Register oder dessen Fehlen (mit oder ohne bekannten Wert) beschreiben. Zum Beispiel DW_OP_fbreg -32: Eine gesamte gespeicherte Variable – 32 Bytes beginnend mit der Stapelrahmenbasis.
- Zusammengesetzte Ortsbeschreibung: Bei der Beschreibung von Objekten in Form von Fragmenten kann jedes Objekt in einem Teil eines Registers enthalten sein oder unabhängig von anderen Fragmenten an einem Speicherort gespeichert werden. Beispiel: DW_OP_reg3 DW_OP_piece 4 DW_OP_reg10 DW_OP_piece 2: Die ersten vier Bytes befinden sich in Register 3 und die letzten beiden Bytes befinden sich in einer Variablen in Register 10.
- Positionsliste: Beschreibt Objekte, die eine begrenzte Lebensdauer haben oder während ihrer Lebensdauer ihren Standort ändern. Zum Beispiel:
- [ 0]
DW_OP_reg0 - [ 1]
DW_OP_reg3 - [ 2]
DW_OP_reg2
- [ 0]
- Eine Variable, deren Speicherort basierend auf dem aktuellen Wert des Programmzählers zwischen den Registern verschoben wird.
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.
- Literale Kodierung
- DW_OP_lit0, DW_OP_lit1...DW_OP_lit31
- Literale auf den Stapel schieben
- DW_OP_addr
- Schieben Sie den Adressoperanden auf den Stapel
- DW_OP_constu
- Vorzeichenlosen Wert auf den Stapel verschieben
- DW_OP_lit0, DW_OP_lit1...DW_OP_lit31
- Wert registrieren
- DW_OP_fbreg
- Übertragen Sie den an der Basis des Stapelrahmens gefundenen Wert, versetzt um den angegebenen Wert
- DW_OP_breg0, DW_OP_breg1... DW_OP_breg31
- Schiebe den Inhalt des angegebenen Registers plus den angegebenen Offset auf den Stapel
- DW_OP_fbreg
- Stack-Operationen
- DW_OP_dup
- Kopieren Sie den Wert oben im Stapel
- DW_OP_deref
- Behandeln Sie den oberen Teil des Stapels als Speicheradresse und ersetzen Sie ihn durch den Inhalt dieser Adresse
- DW_OP_dup
- Arithmetische und logische Operationen
- DW_OP_and
- Legen Sie die beiden Werte oben auf den Stapel und schieben Sie ihr logisches UND zurück
- DW_OP_plus
- Wie DW_OP_and, aber mit Mehrwert
- DW_OP_and
- Kontrollflussvorgänge
- DW_OP_le, DW_OP_eq, DW_OP_gt usw.
- Erfassen Sie die ersten beiden Werte, vergleichen Sie sie und drücken Sie 1, wenn die Bedingung wahr ist, andernfalls 0
- DW_OP_bra
- Bedingter Zweig: Wenn die Oberseite des Stapels nicht 0 ist, springen Sie im Ausdruck über den Offset vorwärts oder rückwärts
- DW_OP_le, DW_OP_eq, DW_OP_gt usw.
- Konvertierung eingeben
- DW_OP_convert
- Konvertieren Sie den Wert oben im Stapel in einen anderen Typ, der durch einen DWARF-Infoeintrag am angegebenen Offset beschrieben wird
- DW_OP_convert
- Sondereinsätze
- DW_OP_nop
- Nichts tun!
- DW_OP_nop
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!

Heiße KI -Werkzeuge

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool
Ausziehbilder kostenlos

Clothoff.io
KI-Kleiderentferner

Video Face Swap
Tauschen Sie Gesichter in jedem Video mühelos mit unserem völlig kostenlosen KI-Gesichtstausch-Tool aus!

Heißer Artikel

Heiße Werkzeuge

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Heiße Themen











Die fünf grundlegenden Komponenten des Linux -Systems sind: 1. Kernel, 2. Systembibliothek, 3. System Utilities, 4. Grafische Benutzeroberfläche, 5. Anwendungen. Der Kernel verwaltet Hardware -Ressourcen, die Systembibliothek bietet vorkompilierte Funktionen, Systemversorgungsunternehmen werden für die Systemverwaltung verwendet, die GUI bietet visuelle Interaktion und Anwendungen verwenden diese Komponenten, um Funktionen zu implementieren.

Um die Git -Repository -Adresse anzuzeigen, führen Sie die folgenden Schritte aus: 1. Öffnen Sie die Befehlszeile und navigieren Sie zum Repository -Verzeichnis; 2. Führen Sie den Befehl "git remote -v" aus; 3.. Zeigen Sie den Repository -Namen in der Ausgabe und der entsprechenden Adresse an.

VS Code One-Step/Nächster Schritt Verknüpfungsschlüsselnutzung: Einschritt (rückwärts): Windows/Linux: Strg ←; macOS: CMD ← Nächster Schritt (vorwärts): Windows/Linux: Strg →; macos: cmd →

Zu den Hauptanwendungen von Linux gehören: 1. Server -Betriebssystem, 2. Eingebettes System, 3. Desktop -Betriebssystem, 4. Entwicklungs- und Testumgebung. Linux zeichnet sich in diesen Bereichen aus und bietet Stabilität, Sicherheits- und effiziente Entwicklungstools.

Es gibt sechs Möglichkeiten, Code in Sublime auszuführen: durch Hotkeys, Menüs, Build-Systeme, Befehlszeilen, Standard-Build-Systeme und benutzerdefinierte Build-Befehle und führen Sie einzelne Dateien/Projekte aus, indem Sie mit der rechten Maustaste auf Projekte/Dateien klicken. Die Verfügbarkeit des Build -Systems hängt von der Installation des erhabenen Textes ab.

Obwohl Notepad den Java -Code nicht direkt ausführen kann, kann er durch Verwendung anderer Tools erreicht werden: Verwenden des Befehlszeilencompilers (JAVAC), um eine Bytecode -Datei (Dateiname.class) zu generieren. Verwenden Sie den Java Interpreter (Java), um Bytecode zu interpretieren, den Code auszuführen und das Ergebnis auszugeben.

Um Laravel zu installieren, befolgen Sie die folgenden Schritte: Installieren Sie den Komponisten (für MacOS/Linux und Windows). Installieren Sie LaRavel Installer. Erstellen Sie eine neue Projektstart -Service -Access -Anwendung (URL: http://127.0.0.1:8000).

Visual Studio Code (VSCODE) ist ein plattformübergreifender, Open-Source-Editor und kostenloser Code-Editor, der von Microsoft entwickelt wurde. Es ist bekannt für seine leichte, Skalierbarkeit und Unterstützung für eine Vielzahl von Programmiersprachen. Um VSCODE zu installieren, besuchen Sie bitte die offizielle Website, um das Installateur herunterzuladen und auszuführen. Bei der Verwendung von VSCODE können Sie neue Projekte erstellen, Code bearbeiten, Code bearbeiten, Projekte navigieren, VSCODE erweitern und Einstellungen verwalten. VSCODE ist für Windows, MacOS und Linux verfügbar, unterstützt mehrere Programmiersprachen und bietet verschiedene Erweiterungen über den Marktplatz. Zu den Vorteilen zählen leicht, Skalierbarkeit, umfangreiche Sprachunterstützung, umfangreiche Funktionen und Versionen
