[PYTHON] Backtrace mit DWARF-Informationen und Pyelftools

TL;DR

Gdb-Python-Erweiterung und pyelftools-Modul Ich habe es verwendet, um Backtrace zu implementieren. Dies kann in Umgebungen nützlich sein, in denen der Befehl gdb backtrace nicht so funktioniert, wie er ist.

Motivation

Bei der Entwicklung bei Kumiko Michiho,

Ist das nicht die Situation, die selten vorkommt? Als ich in Schwierigkeiten geriet, weil ich dank solcher Debug-Informationen (https://qiita.com/mhiramat/items/8df17f5113434e93ff0c) mit solchen Serien und LEDs in die Savanne geworfen wurde, wurde ich ein Freund, der Bagujoho las. Jetzt, wo ich daran gewöhnt bin, möchte ich es zurückverfolgen.

Vorbereitung

Bestätigungsumgebung

Der Einfachheit halber habe ich den Retest in der Umgebung von x64 Ubuntu 16.04 gcc5.4 / python3.5 anstelle der eingebetteten Umgebung [^ 1] bestätigt.

[^ 1]: Als eingebautes Gerät haben wir es in einen tatsächlichen Kampf mit RX gebracht.

In der Erklärung

Die Grundlagen von DWARF werden ausführlich in So gehen Sie Debug-Informationen durchgehen erläutert. Auch [hier](https://ja.osdn.net/projects/drdeamon64/wiki/DWARF%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3% Der Artikel 83% 95% E3% 82% A9% E3% 83% BC% E3% 83% 9E% E3% 83% 83% E3% 83% 88) war ebenfalls sehr hilfreich.

Außerdem werde ich nicht zu sehr auf gdb Python-API eingehen. Ich mache keine komplizierten Dinge mit dem Python-GDB-Modul, daher denke ich, dass Sie es verstehen können, wenn Sie sich das Beispiel ansehen.

Installieren Sie "Pyelftools"

Es ist schwierig, DWARF mit bloßen Händen zu dekodieren. Installieren Sie daher das Python-Modul pyelftools, das einen erheblichen Teil dekodiert. Ich möchte die Umgebung mit virtualenv usw. isolieren, aber als ich Python über gdb aufrief, wurde der Modulpfad in virtualenv nicht angezeigt, sodass ich ihn direkt in das System einfügte.

$ sudo pip3 install pyelftools

Alternativ können Sie das Modul irgendwo erweitern und am Anfang des Python-Skripts platzieren

import sys
sys.path.append("/home/oreore/virtualenv/python/site-packages")

Sie können den Pfad wie folgt eingeben [^ 2].

[^ 2]: Bitte weisen Sie darauf hin, ob es eine brillante Lösung gibt, die gdb überzeugen kann.

Klonen und machen Sie den Bestätigungscode

Verwenden Sie erschwinglichen Code, um zu sehen, wie es funktioniert. Hier verwenden wir die Mach-Komprimierungs- / Dekomprimierungsbibliothek lz4 und ihre Testanwendung simpleBuffer. Zum Beispiel erklärt hier, was "lz4" ist.

Klonen und erstellen Sie nun wie folgt.

$ git clone https://github.com/lz4/lz4.git
$ cd lz4
$ CFLAGS="-g3 -fno-dwarf2-cfi-asm" make all

Dies ist eine kleine Ergänzung zum Build-Flag, aber in gcc (Ubuntu 5.4.1-2ubuntu1 ~ 16.04) 5.4.1 20160904 habe ich hier versucht, Gas-CFI-Direktive /as/CFI-directives.html) scheint den Abschnitt ".debug_frame" wegzulassen, in dem die Stapelrahmeninformationen gespeichert sind, und nur die vereinfachte Version (?) ".Eh_frame" wurde veröffentlicht. pyelftools unterstützt auch das Decodieren von .eh_frame, die Verwendung dauert jedoch einige Zeit. Deshalb habe ich diesmal den Compiler gebeten, ".debug_frame" (Option "-fno-dwarf2-cfi-asm") auszugeben (https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options). Ich habe es mit .html angefordert). Dies hat sich möglicherweise mit Ubuntu / gcc geändert.

Führen Sie backterace mit gdb aus, siehe Antwort

Führen Sie es zuerst mit gdb aus, brechen Sie an einer geeigneten Stelle ab und verfolgen Sie es zurück.

$ cd examples
$ gdb simpleBuffer
 :
 :
(gdb) b lz4.c:578
Breakpoint 1 at 0x4010a4: lz4.c:578. (11 locations)

(gdb) r
Starting program: /home/oreore/lz4/examples/simpleBuffer 

Breakpoint 1, LZ4_compress_generic (acceleration=1, dictIssue=<optimized out>, dict=<optimized out>, tableType=<optimized out>, 
    outputLimited=<optimized out>, maxOutputSize=<optimized out>, inputSize=57, dest=0x61e010 "", 
    source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", cctx=0x7fffffff9810) at lz4.c:578
578	    LZ4_putPosition(ip, cctx->hashTable, tableType, base);

(gdb) bt
#0  LZ4_compress_generic (acceleration=1, dictIssue=<optimized out>, dict=<optimized out>, 
    tableType=<optimized out>, outputLimited=<optimized out>, maxOutputSize=<optimized out>, 
    inputSize=57, dest=0x61e010 "", 
    source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", cctx=0x7fffffff9570)
    at lz4.c:578
#1  LZ4_compress_fast_extState (state=0x7fffffff9570, 
    sourpppce=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73, acceleration=1) at lz4.c:739
#2  0x0000000000404722 in LZ4_compress_fast (
    source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73, acceleration=1) at lz4.c:760
#3  0x0000000000404776 in LZ4_compress_default (
    source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73) at lz4.c:771
#4  0x0000000000400957 in main () at simple_buffer.c:54

Lassen Sie uns dies reproduzieren.

Implementierung

Ich habe die fertige in [gist] vorbereitet (https://gist.github.com/sumomoneko/d8415b14f8eddf74a3eb9bd6d521fab3), daher möchte ich sie entsprechend erläutern.

Plattformspezifische Teile getrennt halten

Ich möchte sofort mit der Analyse von DWARF beginnen, aber vorher organisieren wir die Ausführungsumgebung.

Dieses Mal erstellen wir ein Python-Skript, das auf x64-GDB ausgeführt wird. Es ist jedoch nicht unbedingt erforderlich, es separat auf GDB auszuführen. Aufgrund der Natur von DWARF bedeutet dies nicht, dass es x64 sein muss. Wenn die folgenden Funktionen vorbereitet sind, kann sie unabhängig von GDB und Architektur als einzelnes Python-Skript ausgeführt werden.

Lassen Sie uns diese Funktionen zunächst als Funktionen trennen, damit wir das Ziel später problemlos ändern können. Die Implementierung jeder Funktion wird unten erläutert.

Rufen Sie den Dateipfad ab, der den DWARF enthält

Ruft den Pfad der ausführbaren Datei ab, die nicht entfernt wurde. Wir rufen den Namen des Objekts ab, das unter gdb ausgeführt wird (in gdb als "minderwertig" bezeichnet). Wenn es integriert ist, handelt es sich um eine Pre-Strip-Binärdatei, die vor dem Brennen in das ROM gespeichert wurde.

def get_elf_file_name() -> str:
    """
Rufen Sie den Namen der Datei ab, die die Debug-Informationen enthält.
In der GDB-Umgebung wird GDB.objfiles()[0].Gefunden in Dateiname
    :return:Dateipfad mit Debug-Informationen
    """
    import gdb
    return gdb.objfiles()[0].filename

Lesen Sie den Speicher von der angegebenen Adresse

uintptr_t ret = *reinterpret_cast<uintptr_t*>(addr);

Hier ist ein Bild davon mit gdb-python. Es wird verwendet, um den Speicher abzurufen, auf den das Register zeigt. Da der Stapelbereich als Speicherbereich benötigt wird, empfiehlt es sich, den Stapelbereich zumindest als Speicherauszugsziel festzulegen, wenn Sie einen eigenen Speicherauszug erstellen.

def read_uintptr_t(addr: int) -> int:
    """
    uintptr_Lesen Sie Daten der Größe t aus dem Speicher
    uintptr_t ret = *reinterpret_cast<uintptr_t*>(addr);
    :param addr:Adresse
    :return:Daten
    """
    import gdb
    uintptr_t = gdb.lookup_type('uintptr_t')
    return int(gdb.Value(addr).reinterpret_cast(uintptr_t.pointer()).dereference())

Holen Sie sich den Programmzähler des Startpunkts der Rückverfolgung (Programmstopport).

Ich habe gerade (gdb) p $ pc in python-gdb geschrieben. Wenn Sie einen eigenen Speicherauszug erstellen, ist dies wie das Speichern der Quelladresse, die zur Ausnahmeverarbeitung gesprungen ist?

def get_pc() -> int:
    """
Holen Sie sich den Programmzähler
    :return:PC-Wert
    """
    import gdb
    return int(gdb.parse_and_eval("$pc"))

Holen Sie sich den Registerwert zum Zeitpunkt des Stopps

DWARF wurde entwickelt, um eine Vielzahl von Architekturen zu unterstützen. Daher werden die Register nicht mit architekturspezifischen Namen behandelt, sondern nummeriert und verwaltet. Welches Register ist dann welche Nummer? Schnell [Quellcode von gdb](https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=gdb Werfen wir einen Blick auf /amd64-tdep.c;h=b589d93940f1f498177ba91273190dc9b0714370;hb=HEAD#l156).

Hier ist also das fertige Produkt. Ich habe die Register "stN" und "mmN" auskommentiert, aber dies ist ein Durchhang, weil die breiten Register ein Ärger waren [^ 3]. Wenn Sie nach einer Registernummer für eine andere Architektur suchen, sollten Sie sich nach einer ähnlichen Datei mit einem CPU-Namen an einer ähnlichen Stelle im GDB-Quellbaum umsehen.

[^ 3]: Ursprünglich habe ich es zum Debuggen der eingebauten CPU geschrieben, daher habe ich kein solches Register.

def get_registers() -> List[int]:
    """
Sammeln Sie Registerwerte von gdb gemäß der DWARF-Registernummer
    https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=gdb/amd64-tdep.c;h=b589d93940f1f498177ba91273190dc9b0714370;hb=HEAD#l156
    :return:Liste der Registerwerte in der Reihenfolge der DWARF-Registernummer
    """
    import gdb
    reg_names = ["rax", "rdx", "rcx", "rbx", "rsi", "rdi", "rbp", "rsp",
                 "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15",
                 "rip",
                 # "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
                 # "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
                 None, None, None, None, None, None, None, None,
                 None, None, None, None, None, None, None, None,
                 # "st0", "st1", "st3", "st4", "st5", "st6", "st7",
                 None, None, None, None, None, None, None, None,
                 # "mm0", "mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm7",
                 None, None, None, None, None, None, None, None,
                 "eflags",
                 "es", "cs", "ss", "ds", "fs", "gs", None, None,

                 None, None, None, None,
                 None, None,
                 "mxcsr", "fctrl", "fstat"]

    ret = []
    for name in reg_names:
        if name is not None:
            val = int(gdb.parse_and_eval("${}".format(name)))
            ret.append(val)
        else:
            ret.append(-1)
    return ret

Legen Sie einen Wert für den Stapelzeiger fest

Wenn Sie in den Aufrufbaum zurückkehren, müssen Sie den Stapel zurückspulen. Da das Register, das den Stapel enthält, architekturabhängig ist, wird es in Funktionen als Rückspulverarbeitung unterteilt.

def unwind_stack(regs: List[int], cfa: int) -> None:
    """
Stellen Sie die Adresse in das Register ein, in dem sich der Stapelzeiger befindet.
Für x64$Für rsp. Die DWARF-Registernummer ist 7, stellen Sie also die Adresse dort ein.
    :param regs:Array von Registern(DWARF-Registernummernreihenfolge)
    :param cfa:Adresse, die als Stapelzeiger festgelegt werden soll
    :return: None
    """
    regs[7] = cfa

Das war's für das Vertrauen in GDB und Architektur.

Gesamtdurchfluss

Der Gesamtfluss der Backtrace-Verarbeitung ist wie folgt.

  1. Holen Sie sich die Stoppadresse und registrieren Sie sich (einschließlich Stapelzeiger).
  2. Funktionsinformationen von der Adresse abrufen
  3. Rufen Sie die Anruferadresse und den Registerstatus unmittelbar vor dem Aufruf aus den Stapelrahmeninformationen ab.
  4. Gehen Sie zurück zu 2 und wiederholen Sie den Vorgang

Dies ist fest mit main () geschrieben.

def main() -> None:
    """
Rückverfolgung anzeigen
    """
    with open(get_elf_file_name(), 'rb') as f:
        elf_file = ELFFile(f)
        if not elf_file.has_dwarf_info():
            print('file has no DWARF info')
            return

        dwarf_info = elf_file.get_dwarf_info()

        #Zum ersten Mal von der aktuellen Stopppositionsinformation
        pc = get_pc()
        regs = get_registers()
        i = 0
        while True:
            #Funktionsinformationen der Stoppposition abrufen
            fi = get_func_info(dwarf_info, pc)
            if fi is None:
                break

            print("#{:<3}0x{:016x} in {}() at {}:{}".format(i,
                                                            fi["addr"],
                                                            fi["func_name"],
                                                            fi["path"],
                                                            fi["line"]))
            i += 1

            #Schauen Sie sich den Stapelrahmen an und folgen Sie dem Aufrufer
            pc, regs = get_prev_frame(fi["cu"], pc, regs, read_uintptr_t)
            if pc is None:
                break

"Abrufen der Stoppadresse, des Registers (und des Stapelzeigers)" ruft nur die im vorherigen Abschnitt erläuterte Funktion auf. Wir werden sie daher in der Reihenfolge "Abrufen der Funktionsinformationen einschließlich der Adresse" betrachten.

Funktionsinformationen einschließlich Adresse abrufen

Als DWARF-Erfassungsroute, um eine Funktion zu erhalten, die sie von einer bestimmten Adresse enthält,

--Lick alle CU / DIE und finde einen Treffer-Adressbereich --Subtrahiere DIE von der Adresse

Es gibt zwei Möglichkeiten. Letzteres ist genau die Methode für diesen Zweck, [DWARFv4 6.1.2 Suche nach Adresse](http://www.dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A347%2C] % 22gen% 22% 3A0% 7D% 2C% 7B% 22name% 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D) Entsprechende Tabelle zum Abschnitt ".debug_aranges" Ist geschrieben. Leider unterstützt "pyelftools" nicht ".debug_aranges" (https://github.com/eliben/pyelftools/wiki/User's-guide#dwarf). Also werde ich offen nach CU / DIE suchen.

def get_func_info(dwarf_info: DWARFInfo, addr: int) -> Optional[Dict]:
    """
Funktionsinformationen abrufen, einschließlich der durch addr angegebenen Adresse
    :param dwarf_info:DWARF-Informationen
    :param addr:Programm zähler
    :return:Funktionsinformationen wie Funktionsname und Adresse
    """

    #Von jeder Zusammenstellungseinheit
    for cu in dwarf_info.iter_CUs():
        #Während der Wiederholung von DIE
        for die in cu.iter_DIEs():
            try:
                #Auf der Suche nach der Funktion DIE,
                if die.tag == 'DW_TAG_subprogram':
                    #Suchen Sie den von der Funktion belegten Adressbereich
                    low_pc = die.attributes['DW_AT_low_pc'].value
                    high_pc_attr = die.attributes['DW_AT_high_pc']
                    high_pc_attr_class = describe_form_class(high_pc_attr.form)
                    if high_pc_attr_class == 'address':
                        high_pc = high_pc_attr.value
                    elif high_pc_attr_class == 'constant':
                        high_pc = low_pc + high_pc_attr.value
                    else:
                        print('Error: invalid DW_AT_high_pc class:{}\n'.format(high_pc_attr_class))
                        continue

                    #Bingo, wenn die angegebene Adresse diesem Funktionsbereich entspricht
                    if low_pc <= addr < high_pc:
                        ret = dict()
                        ret["addr"] = addr
                        ret["cu"] = cu
                        ret["func_name"] = die.attributes['DW_AT_name'].value.decode("utf-8")
                        ret["func_addr"] = low_pc
                        ret["offset_from_func"] = addr - low_pc
                        ret.update(get_file_line_from_address(cu, addr))
                        return ret
            except KeyError:
                continue
    return None

In allen CompileUnits suchen wir nach DIE und iterieren DIE DW_TAG_subprogram, das die Funktion darstellt. Der von der Funktion belegte Adressbereich ist [2.17 Codeadressen und -bereiche](http://dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A137%2C%22gen%22%3A0% Die Spezifikationen sind in 7D% 2C% 7B% 22name% 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D) geschrieben. Wenn das Attribut [DW_AT_low_pc, DW_AT_high_pc), das das Intervall angibt, einen einzelnen Adressbereich angibt oder wenn aufgrund von Optimierung usw. mehrere diskontinuierliche Bereiche vorhanden sind, wenn der Bereich durch DW_AT_ranges angegeben wird Es gibt. Lassen Sie uns zunächst nur einen einzigen Adressbereich behandeln. Wenn "DW_AT_ranges" angezeigt wird, implementieren Sie es zu diesem Zeitpunkt.

Von hier aus werden domänenspezifische Wörter wie Attribute und Klassen verstreut, sodass ich sie einmal organisieren werde.

CompileUnit(CU)
Zu kompilierende Einheit. Für C ist es eine Quelldateieinheit.
DIE
Eine Struktur von Debug-Informationen, bei der es sich um einen Baum von Eltern-Kind-Beziehungen handelt. Für jede CU gibt es einen DIE-Baum. Es gibt verschiedene Typen, die sich jedoch grob durch DW_AT_TAG_ * unterscheiden.
-Attribut ( DW_AT _ * )
In DIE enthaltenes Informationselement. Name oder Adressbereich.
Klasse Die durch das Attribut
ausgedrückte Bedeutung. Adresse , Konstante , Zeichenfolge .
-Format ( DW_FORM_ * )
Zeigt an, wie es als Entität gehalten wird. DW_FORM_data2 ist ein 2-Byte-Wert, DW_FORM_data4 ist ein 4-Byte-Wert.

Wenn Sie diese Begriffe verwenden, um zu beschreiben, wie die Startadresse einer Funktion ermittelt wird,

Wenn Sie für die CU in main.c von der DIE oben folgen, erreichen Sie die DIE der Hauptfunktion. Das Tag für dieses DIE ist "DW_TAG_subprogram" und das Attribut "DW_AT_name" ist "main". Das Attribut DW_AT_low_pc hat einen Wert der Klasse address in der Form DW_FORM_addr. Das ist die Startadresse der Hauptfunktion.

Es wird sein.

Nachdem wir die Startadresse kennen, schauen wir uns die Endseite an, das Attribut DW_AT_high_pc. [2.17.2 Kontinuierlicher Adressbereich](http://dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A137%2C%22gen%22%3A0%7D%2C%7B%22name% Gemäß 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D) kann das Attribut "DW_AT_high_pc" "Adresse" oder "Konstante" sein. Wenn es sich um die Klasse "Adresse" handelt, handelt es sich um * die verschobene Adresse *, sodass Sie sich diese als geladene Speicheradresse vorstellen können [^ 4]. Wenn die Klasse "konstant" ist, bedeutet dies die Versatzadresse von "DW_AT_low_pc".

[^ 4]: Ich denke, ASLR ist deaktiviert, wenn gdb funktioniert, daher sollte es dem Preis zum Zeitpunkt der Verknüpfung entsprechen (ich bin nicht sicher, weil ich nicht richtig über gemeinsam genutzte Bibliotheken nachdenke).

Nachdem Sie die Start- und Endadressen dieser Funktion kennen, können Sie feststellen, ob dies die gesuchte Funktion ist.

Suchen Sie den Namen / die Zeilennummer der Quelldatei aus der Adresse

Was soll ich mit der Quelldatei tun, nachdem ich den Funktionsnamen und die Adresse kenne?

[6.2 Informationen zur Zeilennummer](http://www.dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A350%2C%22gen%22%3A0%7D%2C%7B%22name% Laut 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D) können Sie im Abschnitt .debug_line sehen, dass es eine Tabelle zur Konvertierung von Adresse in Quellcode gibt. Die Grundidee besteht darin, eine Referenztabelle von der Adresse des Objekts bis zum Namen, der Zeile und der Spaltennummer der Quellcodedatei zu haben. Wenn Sie jedoch einfach eine solche Tabelle erstellen, wird sie zu einer riesigen Tabelle, die um ein Vielfaches größer ist als das unten gezeigte Objekt.

Adresse Name der Quelldatei Anzahl der Zeilen Anzahl an Ziffern
0x00abcd main.c 10 8
0x00abce main.c 11 10
0x00abcf main.c 11 10
0x00abcg main.c 12 8
0x00abch main.c 12 8
0x00abci main.c 13 8

Daher reduziert DWARF die Speichergröße durch die folgenden zwei Methoden.

  • Lassen Sie einfach doppelte Zeilen weg
Adresse Name der Quelldatei Anzahl der Zeilen Anzahl an Ziffern
0x00abcd main.c 10 8
0x00abce main.c 11 10
0x00abcg main.c 12 8
0x00abci main.c 13 8

Spart Größe, indem doppelte Zeilen entfernt werden, wenn die Maschinenanweisungen mehrere Bytes umfassen.

  • Entwerfen Sie eine Stapelmaschine

Entwerfen Sie Ihre eigene Stapelmaschine ohne Aufzeichnungstabellen und speichern Sie damit die Aufzeichnungsgröße.

_Personen, Leute, Leute, Leute, Leute, Leute, Leute, Leute, Leute, Leute
> Entwerfen Sie eine Stapelmaschine <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

Es wurde plötzlich eine super Erweiterung, aber zum Glück verschiebt "Pyelftools" die Stapelmaschine und erweitert sie in die Adress- / Zeilennummertabelle, daher bin ich dankbar, sie zu verwenden. Eigentlich class Line Program Die Stack-Maschine läuft hier herum.

Die Funktion zum Abrufen des Quelldateinamens und der Anzahl der Zeilen von der Adresse lautet also wie folgt.

def get_file_line_from_address(cu: CompileUnit, addr: int) -> Dict:
    """
Suchen Sie in der Kompiliereinheit nach Informationen zum Namen der Quellcodedatei und zur Zeilenanzahl
    :param cu:Einheiteninformationen zusammenstellen
    :param addr:Adresse im Objekt
    :return:Name der Quellcodedatei und Anzahl der Zeilen
    """

    top_die = cu.get_top_DIE()  # type: DIE

    #Das Verzeichnis zur Kompilierungszeit.
    #Der Quellpfad wird relativ zu diesem Verzeichnis angezeigt
    if "DW_AT_comp_dir" in top_die.attributes.keys():
        comp_dir = top_die.attributes["DW_AT_comp_dir"].value.decode('utf-8')
    else:
        comp_dir = ""

    line_program = cu.dwarfinfo.line_program_for_CU(cu)  # type: LineProgram

    for entry in reversed(line_program.get_entries()):  # type: LineProgramEntry
        if entry.state is not None and not entry.state.end_sequence:
            if entry.state.address < addr:
                #Der Adressbereich des Eintrags enthielt die gesuchte Adresse

                #Suchen Sie den vollständigen Pfad der Datei
                fe = line_program["file_entry"][entry.state.file - 1]
                name = fe.name.decode('utf-8')
                if fe.dir_index != 0:
                    #Ein anderes Verzeichnis, nicht das Verzeichnis zur Kompilierungszeit(Als relativer Pfad notiert)Wenn es eine Quelle in gibt
                    d = line_program["include_directory"][fe.dir_index - 1].decode('utf-8')
                else:
                    d = ""

                path = posixpath.normpath(posixpath.join(comp_dir, d, name))

                ret = dict()
                ret["path"] = path
                ret["line"] = entry.state.line
                return ret

    return dict()

Rufen Sie die Anruferadresse und den Registerstatus unmittelbar vor dem Aufruf aus den Stapelrahmeninformationen ab.

Nun, hier ist die Produktion. Um zum Anruf zurückzukehren

  • Adresse des Anrufers
  • Stapelzeiger des Aufrufers
  • Der Speicherort des Registers, dessen Rückgabe bei Rückgabe gemäß der aufrufenden Konvention versprochen wird.

Ich brauche diese Informationen.

Die Grundidee ist wie immer, einen riesigen Tisch zu haben. Zum Beispiel

Adresse Stapelzeiger des Aufrufers Registrieren Sie r0, um zurückzukehren Registrieren Sie r1, um wiederhergestellt zu werden 呼び元Adresse
0x100 .. .. .. ..
0x101 .. .. .. ..
0x102 .. .. .. ..
0x103 .. .. .. ..

Durch Erstellen einer so großen Tabelle können die Informationen für die Rückkehr zum Anrufer an einer beliebigen Adresse wiederhergestellt werden, während sich der Verbrauchsstatus des Registerstapels mit fortschreitender Funktion jeden Moment ändert.

Hier ist eine kleine Definition des Begriffs, aber definieren Sie grob, dass der obere Rand des Stapelzeigers bei der Rückkehr zum Funktionsaufrufer CFA heißt. Um genau zu sein: 6.4 Call Frame Information

An area of memory that is allocated on a stack called a “call frame.” The call frame is identified by an address on the stack. We refer to this address as the Canonical Frame Address or CFA. Typically, the CFA is defined to be the value of the stack pointer at the call site in the previous frame (which may be different from its value on entry to the current frame).

Der auf dem Stapel zugewiesene Speicherbereich wird als "Aufrufrahmen" bezeichnet. Der Aufrufrahmen wird als Adresse auf dem Stapel identifiziert. Die Adresse für diese Identifikation heißt CFA (Referenzrahmenadresse). In der Regel ist CFA der Wert des Stapelzeigers unmittelbar vor dem Funktionsaufruf.

Also, * normalerweise * ist ein Punkt, aber es ist ein grobes Verständnis.

Nun, wie üblich komprimiert DWARF diese Tabelle mit einer Stapelmaschine, aber die eigentliche Arbeit wird von "Pyelftools" erledigt. Wenn Sie die Details der Erweiterungsmethode aufschreiben, können Sie das Ende nicht sehen, daher erkläre ich nur die Atmosphäre, in der Sie der erweiterten Tabelle nachjagen.

Zuerst habe ich versucht, die Informationen der Funktion LZ4_compress_fast () anzuzeigen, die im Abschnitt .debug_frame mit eu-readelf -w aufgezeichnet wurden. Der Teil mit Programm ist der Code der Stapelmaschine.

[    70] CIE length=20
   CIE_id:                   0
   version:                  3
   augmentation:             "zR"
   code_alignment_factor:    1
   data_alignment_factor:    -8
   return_address_register:  16
   Augmentation data:        0x3 (FDE-Adresscodierung udata4)

   Program:
     def_cfa r7 (rsp) at offset 8
     offset r16 (rip) at cfa-8
     nop
     nop
 :
 :
[   470] FDE length=44 cie=[    70]
   CIE_pointer:              1028
   initial_location:         0x00000000004046a3 <LZ4_compress_fast>
   address_range:            0xa1

   Program:
     advance_loc4 1 to 0x1
     def_cfa_offset 16
     offset r6 (rbp) at cfa-16
     advance_loc4 3 to 0x4
     def_cfa_register r6 (rbp)
     advance_loc4 156 to 0xa0
     def_cfa r7 (rsp) at offset 8
     nop
     nop
     nop
     nop
     nop
     nop
     nop

Sie werden die Adress- / Registerkorrespondenztabelle von hier aus wiederherstellen. Wenn Sie sie jedoch mit pyrlftools dekodieren, wird sie auf das folgende Formular erweitert.

# entries = cu.dwarfinfo.CFI_entries()
# entry.cie.header
Container({'version': 3,
           'code_alignment_factor': 1,
           'augmentation': b'',
           'length': 20,
           'data_alignment_factor': -8,
           'CIE_id': 4294967295,
           'return_address_register': 16})
# entry.decoded()
DecodedCallFrameTable(table=[{'pc':  0x4046A3,
                              'cfa': CFARule(reg=7, offset=8, expr=None)},
                              16:    RegisterRule(OFFSET, -8),
                             {'pc':  0x4046A4,
                              'cfa': CFARule(reg=7, offset=16, expr=None),
                              6:     RegisterRule(OFFSET, -16),
                              16:    RegisterRule(OFFSET, -8)},
                             {'pc':  0x4046A7,
                              'cfa': CFARule(reg=6, offset=16, expr=None),
                              6:     RegisterRule(OFFSET, -16)
                              16:    RegisterRule(OFFSET, -8),},
                             {'pc':  0x404743,
                              'cfa': CFARule(reg=7, offset=8, expr=None),
                              6:     RegisterRule(OFFSET, -16),
                              16:    RegisterRule(OFFSET, -8)}],
                      reg_order=[16, 6])

Die folgende Tabelle fasst dies in Form einer Tabelle zusammen.

Übrigens wird hier das Register return_address_register, in dem das Rückgabeziel gespeichert ist, Nr. 16, $ rip zugewiesen, aber je nach Architektur möglicherweise nicht dem tatsächlichen Register zugewiesen.

Adresse CFA r6 ($rbp) r16 ($rip, return_address_register)
0x4046A3 r7+8 - *(CFA-8)
0x4046A4 r7+16 *(CFA-16) *(CFA-8)
0x4046A7 r6+16 *(CFA-16) *(CFA-8)
0x404743 r7+8 *(CFA-16) *(CFA-8)

Verwenden wir diese Tabelle tatsächlich, um die Adresse des Aufrufers wiederherzustellen, der LZ4_compress_fast () aufgerufen hat.

(gdb) bt
#0  LZ4_compress_generic (acceleration=1, dictIssue=<optimized out>, dict=<optimized out>, tableType=<optimized out>, 
    outputLimited=<optimized out>, maxOutputSize=<optimized out>, inputSize=57, dest=0x61e010 "", 
    source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", cctx=0x7fffffff9530) at lz4.c:578
#1  LZ4_compress_fast_extState (state=0x7fffffff9530, source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73, acceleration=1) at lz4.c:739
#2  0x0000000000404722 in LZ4_compress_fast (source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73, acceleration=1) at lz4.c:760
#3  0x0000000000404776 in LZ4_compress_default (source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73) at lz4.c:771
#4  0x0000000000400957 in main () at simple_buffer.c:54

Wechseln Sie zunächst zu dem Stapelrahmen, in dem die Funktion LZ4_compress_fast () ausgeführt wird.

(gdb) frame 2
#2  0x0000000000404722 in LZ4_compress_fast (source=0x41b4b8 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", dest=0x61e010 "", 
    inputSize=57, maxOutputSize=73, acceleration=1) at lz4.c:760
760	    int const result = LZ4_compress_fast_extState(ctxPtr, source, dest, inputSize, maxOutputSize, acceleration);

Die Ausführungsadresse lautet zu diesem Zeitpunkt 0x404722. Wenn Sie also die obige Tabelle nach Adresse durchsuchen,

Adresse CFA r6 ($rbp) r16 ($rip, return_address)
0x4046A7 r6+16 *(CFA-16) *(CFA-8)

Diese Zeile stimmt überein. Der Speicherort des CFA ist "$ rbp + 16", weil "r6 == $ rbp". Und da r16`` CFA-8 ist, lautet ** return_address **:

(gdb) x $rbp+16-8
0x7fffffffd568:	0x00404776

Sollte sein. Da diese Adresse sicherlich mit der Ausführungsadresse von Frame 3 übereinstimmt, bedeutet dies, dass die Anruferadresse wiederhergestellt werden kann.

Hier gibt es einige Einschränkungen. Ich beziehe mich auf r6 als Speicherort des CFA, aber dies ist nicht das r6, das ich für den Rückruf von Anrufen wiederhergestellt habe, aber jetzt ist es r6 zum Zeitpunkt der Ausführung von 0x404722. Wenn nicht, wird es zirkulieren, da es auf CFA basiert, um r6 wiederherzustellen.

Mit anderen Worten:

  1. Ermitteln Sie den Standort des CFA anhand des aktuellen Registerwerts.
  2. Ermitteln Sie den Wert des Registers, das bei der Rückgabe von der aktuellen Funktion zurückgegeben werden soll, anhand des oben angegebenen CFA-Speicherorts.

Es ist notwendig, in der Reihenfolge zu berechnen.

Basierend auf dem oben Gesagten werden die folgenden Funktionen verarbeitet. Neben der Wiederherstellung der return_address und der Wiederherstellung des Registers wird hier auch der Stack zurückgespult.

def get_prev_frame(cu: CompileUnit,
                   addr: int,
                   regs: List[int],
                   read_mem: Callable[[int], int]) -> Tuple[Optional[int], Optional[List[int]]]:
    """
Ausführungsadressregister und Speicher(Hauptsächlich stapeln)Identifizieren Sie den Stapelrahmen.
Stellen Sie die Adresse des Anrufers und das Register zu diesem Zeitpunkt wieder her
    :param cu:CU-Informationen
    :param addr:Ausführungsadresse
    :param regs:Liste der nach DWARF-Registernummer indizierten Register
    :param read_mem:Eine Funktion, die Speicher liest. Lesen ist ein Zeiger(registrieren)Größe
    :return:Anruferadresse und Registrierung
    """

    if cu.dwarfinfo.has_CFI():
        entries = cu.dwarfinfo.CFI_entries()
    else:
        # .debug_Es gibt keinen Rahmen
        entries = []

    for entry in entries:
        if "initial_location" not in entry.header.keys():
            continue
        start = entry.header.initial_location
        end = start + entry.header.address_range
        if not (start <= addr < end):
            continue
        dec = entry.get_decoded()
        for row in reversed(dec.table):
            if row["pc"] <= addr:
                #Stellen Sie die Absenderadresse und ihre Register wieder her
                cfa_rule = row["cfa"]  # type: CFARule
                assert cfa_rule.expr is None, "DWARF-Ausdruck wird nicht unterstützt"
                cfa = regs[cfa_rule.reg] + cfa_rule.offset

                return_address_rule = row[entry.cie.header.return_address_register]  # type: RegisterRule
                assert return_address_rule.type == RegisterRule.OFFSET, "Wird nur OFFSET unterstützt"
                return_address = cfa + return_address_rule.arg

                prev_regs = regs[:]
                for key, reg in row.items():
                    if isinstance(reg, RegisterRule):
                        assert reg.type == RegisterRule.OFFSET, "Wird nur OFFSET unterstützt"
                        prev_regs[key] = read_mem(cfa + reg.arg)

                #Stapel zurückspulen
                unwind_stack(prev_regs, cfa)
                return read_mem(return_address), prev_regs
    return None, None

Funktionsprüfung

Jetzt, da es fertig ist, lassen Sie es uns laufen. Platzieren Sie bt.py im aktuellen Verzeichnis und versuchen Sie, das Python-Modul von gdb aus aufzurufen. Wenn Sie den Frame weiter verschieben, ändert sich der Registerwert. Vergessen Sie also nicht, ihn zurück zum ersten Frame zu verschieben!

(gdb) frame 0
(gdb) source bt.py 
#0  0x00000000004010a4 in LZ4_compress_fast_extState() at /home/oreore/lz4/lib/lz4.c:575
#1  0x0000000000404722 in LZ4_compress_fast() at /home/oreore/lz4/lib/lz4.c:760
#2  0x0000000000404776 in LZ4_compress_default() at /home/oreore/lz4/lib/lz4.c:771
#3  0x0000000000400957 in main() at /home/oreore/lz4/examples/simple_buffer.c:54

Es sieht ziemlich gut aus, aber wenn Sie genau hinschauen, werden Sie feststellen, dass ein Frame fehlt. Dies liegt daran, dass wir die Inline-Erweiterung nicht richtig analysiert haben. Die Funktion "LZ4_compress_generic ()" wird in der Funktion "LZ4_compress_fast_extState ()" inline erweitert, aber ich decodiere sie als "LZ4_compress_fast_extState ()", ohne es zu bemerken.

Um Inline zu interpretieren, müssen Sie DW_TAG_inlined_subroutine ernst nehmen, aber es gibt nicht genug Platz, um es zu schreiben, also werde ich hier aufhören.

Außerdem werden die Parameter nicht angezeigt. Auch dies ist eine Menge Arbeit, zum Beispiel das Betrachten des Abschnitts .debug_loc, um es zu schreiben (ry)

abschließend

Kemono Freunde, ich wollte die Fortsetzung sehen. .. ..