Heim >System-Tutorial >LINUX >Entdecken Sie Techniken zur Variablenverarbeitung in Linux-Debuggern!

Entdecken Sie Techniken zur Variablenverarbeitung in Linux-Debuggern!

PHPz
PHPznach vorne
2024-01-15 23:09:05716Durchsuche
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.
Serienartikelindex
  1. Bereiten Sie die Umgebung vor
  2. Haltepunkt
  3. Register und Speicher
  4. ELF und ZWERG
  5. Quellcode und Signale
  6. Schritt-für-Schritt-Ausführung auf Quellcode-Ebene
  7. Haltepunkte auf Quellenebene
  8. Stack-Erweiterung
  9. Variablen verarbeiten
  10. 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-Standort

Der 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
  • 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-Ausdruck

Verwenden 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
  • 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
  • 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
  • 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
  • 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
  • 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
  • Sondereinsätze
    • DW_OP_nop
      • Nichts tun!
ZWERG-Typ

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 Variablenlesers

Wie 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, &regs);
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();
}
Testen Sie es

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!

Stellungnahme:
Dieser Artikel ist reproduziert unter:linuxprobe.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen