Backtrace en utilisant les informations DWARF et pyelftools

TL;DR

Extension python Gdb et module pyelftools Je l'ai utilisé pour [implémenter] le backtrace (https://gist.github.com/sumomoneko/d8415b14f8eddf74a3eb9bd6d521fab3). Cela peut être utile dans les environnements où la commande gdb backtrace ne fonctionne pas telle quelle.

Motivation

Lors du développement chez Kumiko Michiho,

N'est-ce pas la situation qui est rarement courante? Quand j'ai eu des ennuis parce que j'étais jeté dans la savane avec juste ces séries et LED, grâce à Comment parcourir les informations de débogage, je suis devenu un ami qui sait lire Maintenant que j'y suis habitué, j'aimerais revenir en arrière.

Préparation

Environnement de confirmation

Par souci de simplicité du retest, je l'ai confirmé dans l'environnement de x64 ubuntu 16.04 gcc5.4 / python3.5 au lieu de l'environnement embarqué [^ 1].

[^ 1]: En tant que module intégré, nous l'avons mis dans une véritable bataille avec RX.

Dans l'explication

Les bases de DWARF sont expliquées en détail dans Walking Debug Information, veuillez donc vous y référer. Aussi, [ici](https://ja.osdn.net/projects/drdeamon64/wiki/DWARF%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3% L'article 83% 95% E3% 82% A9% E3% 83% BC% E3% 83% 9E% E3% 83% 83% E3% 83% 88) était également très utile.

De plus, je n'entrerai pas dans les détails sur gdb Python-API. Je ne fais pas de choses compliquées en utilisant le module python gdb, donc je pense que vous pouvez le comprendre en regardant l'exemple.

Installez pyelftools

Il est difficile de décoder DWARF à mains nues, alors installez le module python pyelftools qui décode une partie considérable. Je veux isoler l'environnement avec virtualenv etc., mais quand j'appelle python via gdb, il ne voit pas le chemin du module dans virtualenv, donc je le mets directement dans le système.

$ sudo pip3 install pyelftools

Sinon, développez le module quelque part et placez-le au début du script python

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

Vous pouvez entrer le chemin comme [^ 2].

[^ 2]: Veuillez indiquer s'il existe une solution brillante qui peut convaincre gdb.

Cloner et créer le code de vérification

Utilisez un code abordable pour voir comment cela fonctionne. Ici, nous utiliserons la bibliothèque de compression / décompression Mach lz4 et son application de test simpleBuffer. Par exemple, ici explique ce qu'est «lz4», alors veuillez vous y référer.

Maintenant, clonez et construisez comme suit.

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

En complément de l'indicateur de construction, dans gcc (Ubuntu 5.4.1-2ubuntu1 ~ 16.04) 5.4.1 20160904 j'ai essayé ici, directive gas CFI /as/CFI-directives.html) semble omettre la section .debug_frame où les informations de cadre de pile sont stockées, et seule sa version simplifiée (?) .Eh_frame est sortie. pyelftools prend également en charge le décodage .eh_frame, mais son utilisation prend un certain temps. Donc, cette fois, j'ai demandé au compilateur de sortir .debug_frame option -fno-dwarf2-cfi-asm. Je l'ai demandé avec .html). Cela peut avoir changé avec ubuntu / gcc récent.

Exécutez backterace avec gdb, voyez la réponse

Tout d'abord, exécutez-le avec gdb, coupez à une partie appropriée et effectuez un retour arrière.

$ 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

Reproduisons ceci.

la mise en oeuvre

J'ai préparé celui qui est terminé dans gist, donc je voudrais l'expliquer en conséquence.

Séparez les pièces spécifiques à la plate-forme

J'aimerais commencer tout de suite à analyser DWARF, mais avant cela, organisons l'environnement d'exécution.

Cette fois, nous allons créer un script python qui s'exécute sur gdb x64, mais il n'est pas indispensable de l'exécuter sur gdb séparément. De plus, en raison de la nature de DWARF, cela ne signifie pas qu'il doit être x64. Si les fonctions suivantes sont préparées, il peut être exécuté en tant que script python unique indépendamment de gdb et de l'architecture.

Commençons par séparer ces fonctions en fonctions afin de pouvoir facilement modifier la cible ultérieurement. La mise en œuvre de chaque fonction est expliquée ci-dessous.

Obtenez le chemin du fichier contenant le DWARF

Obtenez le chemin du fichier exécutable qui n'est pas supprimé. Nous récupérons le nom de l'objet fonctionnant sous gdb (appelé "inférieur" dans gdb). S'il est intégré, ce sera un fichier binaire de pré-décapage qui a été enregistré avant la gravure sur ROM.

def get_elf_file_name() -> str:
    """
Obtenez le nom du fichier contenant les informations de débogage.
Dans l'environnement gdb, gdb.objfiles()[0].Trouvé dans le nom du fichier
    :return:Chemin de fichier avec informations de débogage
    """
    import gdb
    return gdb.objfiles()[0].filename

Lire la mémoire à partir de l'adresse spécifiée

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

Voici une image de cette opération avec gdb-python. Il est utilisé pour récupérer la mémoire pointée par le registre. Comme la zone de pile est requise comme zone de mémoire, il est bon de définir au moins la zone de pile comme cible de vidage lors de la création de votre propre vidage.

def read_uintptr_t(addr: int) -> int:
    """
    uintptr_Lire les données de taille t de la mémoire
    uintptr_t ret = *reinterpret_cast<uintptr_t*>(addr);
    :param addr:adresse
    :return:Les données
    """
    import gdb
    uintptr_t = gdb.lookup_type('uintptr_t')
    return int(gdb.Value(addr).reinterpret_cast(uintptr_t.pointer()).dereference())

Obtenir le compteur de programme du point de départ de la trace arrière (emplacement d'arrêt du programme)

Je viens d'écrire (gdb) p $ pc dans python-gdb. Si vous effectuez votre propre vidage, est-ce comme enregistrer l'adresse source qui est passée au traitement des exceptions?

def get_pc() -> int:
    """
Obtenez le compteur de programmes
    :return:Valeur PC
    """
    import gdb
    return int(gdb.parse_and_eval("$pc"))

Obtenez la valeur du registre au moment de l'arrêt

DWARF est conçu pour prendre en charge une variété d'architectures. Par conséquent, les registres ne sont pas traités avec des noms spécifiques à l'architecture, mais sont numérotés et gérés. Alors, quel registre est quel numéro? Rapidement [code source de gdb](https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=gdb Jetons un coup d'œil à /amd64-tdep.c;h=b589d93940f1f498177ba91273190dc9b0714370;hb=HEAD#l156).

Voici donc le produit fini. J'ai commenté les registres stN et mmN, mais c'est un jeu parce que les registres larges étaient un problème [^ 3]. Si vous recherchez un numéro de registre pour une architecture différente, vous devriez rechercher un fichier similaire avec un nom de processeur dans un emplacement similaire dans l'arborescence des sources gdb.

[^ 3]: À l'origine, je l'ai écrit pour déboguer le processeur intégré, donc je n'ai pas un tel registre.

def get_registers() -> List[int]:
    """
Collecter les valeurs de registre de gdb en fonction du numéro de registre DWARF
    https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;a=blob;f=gdb/amd64-tdep.c;h=b589d93940f1f498177ba91273190dc9b0714370;hb=HEAD#l156
    :return:Liste des valeurs de registre par ordre de numéro de registre DWARF
    """
    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

Définir une valeur pour le pointeur de pile

Lorsque vous revenez dans l'arborescence des appels, vous devez rembobiner la pile. Puisque le registre qui contient la pile dépend de l'architecture, il est séparé en fonctions comme traitement de rembobinage.

def unwind_stack(regs: List[int], cfa: int) -> None:
    """
Définissez l'adresse dans le registre qui contient le pointeur de pile.
Pour x64$Pour rsp. Le numéro de registre DWARF est 7, alors définissez l'adresse ici.
    :param regs:Tableau de registres(Ordre des numéros de registre DWARF)
    :param cfa:Adresse à définir comme pointeur de pile
    :return: None
    """
    regs[7] = cfa

C'est tout pour la dépendance de gdb et d'architecture.

Flux global

Le flux global du traitement des traces est le suivant.

  1. Obtenez l'adresse d'arrêt et le registre (y compris le pointeur de pile)
  2. Obtenir des informations de fonction à partir de l'adresse
  3. Obtenez l'adresse de l'appelant et l'état du registre immédiatement avant l'appel à partir des informations de trame de pile.
  4. Revenez à 2 et répétez

Ceci est écrit solidement avec main ().

def main() -> None:
    """
Afficher la trace
    """
    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()

        #Pour la première fois à partir des informations de position d'arrêt actuelle
        pc = get_pc()
        regs = get_registers()
        i = 0
        while True:
            #Obtenir des informations sur la fonction de la position d'arrêt
            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

            #Regardez le cadre de la pile et suivez l'appelant
            pc, regs = get_prev_frame(fi["cu"], pc, regs, read_uintptr_t)
            if pc is None:
                break

"Obtenir l'adresse d'arrêt, registre (& pointeur de pile)" n'appelle que la fonction expliquée dans la section précédente, nous allons donc l'examiner dans l'ordre à partir de "Obtenir les informations de la fonction, y compris l'adresse".

Obtenir des informations sur la fonction, y compris l'adresse

En tant que route de capture DWARF pour obtenir une fonction qui l'inclut à partir d'une certaine adresse,

--Lick all CU / DIE et trouver une plage d'adresses de hit

Il y a deux manières. Cette dernière est exactement la méthode à cet effet, [DWARFv4 6.1.2 Recherche par 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) Tableau correspondant à la section .debug_aranges Est écrit. Malheureusement, pyelftools ne prend pas en charge .debug_aranges (https://github.com/eliben/pyelftools/wiki/User's-guide#dwarf). Donc, je vais franchement chercher CU / DIE.

def get_func_info(dwarf_info: DWARFInfo, addr: int) -> Optional[Dict]:
    """
Obtenir des informations sur la fonction, y compris l'adresse indiquée par addr
    :param dwarf_info:Informations DWARF
    :param addr:Compteur de programme
    :return:Informations de fonction telles que le nom et l'adresse de la fonction
    """

    #De chaque unité de compilation
    for cu in dwarf_info.iter_CUs():
        #Lors de l'itération de DIE
        for die in cu.iter_DIEs():
            try:
                #À la recherche de la fonction DIE,
                if die.tag == 'DW_TAG_subprogram':
                    #Trouvez la plage d'adresses occupée par la fonction
                    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 si l'adresse spécifiée correspond à cette plage de fonctions
                    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

À partir de toutes les CompileUnits, nous rechercherons DIE et itérerons DIE DW_TAG_subprogram qui représente la fonction. La plage d'adresses occupée par la fonction est [2.17 Adresses et plages de code](http://dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A137%2C%22gen%22%3A0% Les spécifications sont écrites en 7D% 2C% 7B% 22name% 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D). Si l'attribut [DW_AT_low_pc, DW_AT_high_pc) indiquant l'intervalle indique une seule plage d'adresses, ou s'il existe plusieurs plages discontinues en raison de l'optimisation, etc., si la plage est spécifiée par DW_AT_ranges Il y a. Pour l'instant, ne traitons qu'une seule plage d'adresses. Si DW_AT_ranges apparaît, implémentez-le à ce moment-là.

À partir de là, les mots spécifiques au domaine, tels que les attributs et les classes, seront dispersés, donc je les organiserai une fois.

CompileUnit(CU)
Unité à compiler. Pour C, il s'agit d'une unité de fichier source.
DIE
Une structure d'informations de débogage, qui est une arborescence de relations parent-enfant. Il existe un arbre DIE pour chaque UC. Il existe différents types, mais ils se distinguent grossièrement par DW_AT_TAG_ * . Attribut
( DW_AT _ * )
Élément d'information contenu dans DIE. Nom ou plage d'adresses.
classe La signification exprimée par l'attribut
. adresse , constante , chaîne .
format ( DW_FORM_ * )
Montre comment il est tenu en tant qu'entité. DW_FORM_data2 est une valeur sur 2 octets, DW_FORM_data4 est une valeur sur 4 octets.

Si vous utilisez ces termes pour décrire comment trouver l'adresse de début d'une fonction,

Pour la CU dans main.c, si vous suivez du DIE en haut, vous atteindrez le DIE de la fonction principale. La balise de ce DIE est DW_TAG_subprogram, et l'attribut DW_AT_name est" main ". L'attribut DW_AT_low_pc a une valeur de la classe ʻaddress sous la forme DW_FORM_addr`. C'est l'adresse de début de la fonction principale.

Ce sera.

Maintenant que nous connaissons l'adresse de début, regardons le côté de fin, l'attribut DW_AT_high_pc. [2.17.2 Plage d'adresses contiguës](http://dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A137%2C%22gen%22%3A0%7D%2C%7B%22name% Selon 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D), l'attribut DW_AT_high_pc peut être de classe ʻaddress ou constant. Si c'est la classe ʻaddress, c'est * l'adresse relocalisée *, donc vous pouvez la considérer comme l'adresse mémoire chargée [^ 4]. Si la classe est «constante», cela signifie l'adresse de décalage de «DW_AT_low_pc».

[^ 4]: Je pense que l'ASLR est désactivé lorsque gdb fonctionne, il devrait donc être le même que le prix demandé au moment de la liaison (je ne suis pas sûr car je ne pense pas correctement aux bibliothèques partagées)

Maintenant que vous connaissez les adresses de début et de fin de cette fonction, vous pouvez déterminer s'il s'agit de la fonction que vous recherchez.

Recherchez le nom du fichier source / numéro de ligne à partir de l'adresse

Maintenant que je connais le nom et l'adresse de la fonction, que dois-je faire avec le fichier source?

[6.2 Informations sur le numéro de ligne](http://www.dwarfstd.org/doc/DWARF4.pdf#%5B%7B%22num%22%3A350%2C%22gen%22%3A0%7D%2C%7B%22name% Selon 22% 3A% 22XYZ% 22% 7D% 2C0% 2C792% 2Cnull% 5D), vous pouvez voir qu'il y a une table de conversion adresse-code source dans la section .debug_line. L'idée de base est d'avoir une table de référence depuis l'adresse de l'objet jusqu'au nom du fichier de code source, la ligne et le numéro de colonne. Cependant, si vous créez simplement une telle table, elle deviendra une énorme table plusieurs fois plus grande que l'objet comme indiqué ci-dessous.

adresse Nom du fichier source Nombre de lignes Nombre de chiffres
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

Par conséquent, DWARF réduit la taille de stockage par les deux méthodes suivantes.

  • Omettez simplement les lignes en double
adresse Nom du fichier source Nombre de lignes Nombre de chiffres
0x00abcd main.c 10 8
0x00abce main.c 11 10
0x00abcg main.c 12 8
0x00abci main.c 13 8

Économise de la taille en supprimant les lignes en double lorsque les instructions de la machine comportent plusieurs octets.

  • Concevoir une machine à empiler

Concevez votre propre machine d'empilage sans enregistrer la table et utilisez-la pour économiser la taille d'enregistrement.

_Personnes, personnes, personnes, personnes, personnes, personnes, personnes, personnes, personnes, personnes
> Concevoir une machine à empiler <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

Cela est soudainement devenu une super extension, mais heureusement, pyelftools déplace la machine de pile et l'étend vers la table des numéros d'adresses / lignes, je suis donc reconnaissant de l'utiliser. En fait, programme de ligne de classe La machine de pile tourne ici.

Ainsi, la fonction pour obtenir le nom du fichier source et le nombre de lignes de l'adresse est la suivante.

def get_file_line_from_address(cu: CompileUnit, addr: int) -> Dict:
    """
Rechercher le nom du fichier de code source et les informations sur le nombre de lignes à partir de l'unité de compilation
    :param cu:compiler les informations sur les unités
    :param addr:Adresse dans l'objet
    :return:Nom du fichier de code source et nombre de lignes
    """

    top_die = cu.get_top_DIE()  # type: DIE

    #Le répertoire au moment de la compilation.
    #Le chemin source est affiché par rapport à ce répertoire
    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:
                #La plage d'adresses de l'entrée contenait l'adresse que vous recherchiez

                #Trouvez le chemin complet du fichier
                fe = line_program["file_entry"][entry.state.file - 1]
                name = fe.name.decode('utf-8')
                if fe.dir_index != 0:
                    #Un répertoire différent, pas le répertoire de compilation(Noté comme chemin relatif)S'il y a une source dans
                    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()

Obtenez l'adresse de l'appelant et l'état du registre immédiatement avant l'appel à partir des informations de trame de pile.

Eh bien, voici la production. Pour revenir à l'appel

  • Adresse de l'appelant
  • Pointeur de pile de l'appelant
  • L'emplacement du registre qui est promis d'être retourné au retour selon la convention d'appel

J'ai besoin de ces informations.

L'idée de base est, comme d'habitude, d'avoir une grande table. Par exemple

adresse Pointeur de pile de l'appelant Enregistrez r0 pour revenir Enregistrez R1 à restaurer 呼び元adresse
0x100 .. .. .. ..
0x101 .. .. .. ..
0x102 .. .. .. ..
0x103 .. .. .. ..

En créant une table aussi volumineuse, les informations de retour à l'appelant peuvent être restaurées à n'importe quelle adresse tandis que l'état de consommation de la pile de registres change à chaque instant au fur et à mesure que la fonction progresse.

Voici une petite définition du terme, mais définissons grossièrement que le haut du pointeur de pile lors du retour à l'appelant de la fonction est appelé CFA. Pour être exact: 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).

La zone mémoire allouée sur la pile est appelée "call frame". La trame d'appel est identifiée comme une adresse sur la pile. L'adresse pour cette identification est appelée CFA (adresse de trame de référence). En règle générale, CFA est la valeur du pointeur de pile juste avant l'appel de fonction.

Donc, * généralement * est un point, mais c'est une compréhension approximative.

Eh bien, comme d'habitude, DWARF compresse cette table en utilisant une machine à pile, mais le travail réel est effectué par pyelftools. Si vous notez les détails de la méthode d'expansion, vous ne pourrez pas voir la fin, donc je n'expliquerai que l'atmosphère de la poursuite du tableau développé.

Tout d'abord, j'ai essayé d'afficher les informations de la fonction LZ4_compress_fast () enregistrées dans la section .debug_frame avec ʻeu-readelf -w`. La partie avec «Programme» est le code de la machine d'empilage.

[    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 (Codage d'adresse FDE 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

Vous allez restaurer la table de correspondance adresse / registre à partir d'ici, mais si vous la décodez avec pyrlftools, elle sera développée sous la forme suivante.

# 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])

Le tableau ci-dessous résume cela sous la forme d'un tableau.

A propos, ici, le registre return_address_register qui stocke la destination de retour est affecté au n ° 16 et $ rip, mais il peut ne pas être affecté au registre réel en fonction de l'architecture.

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)

Utilisons en fait cette table pour restaurer l'adresse de l'appelant qui a appelé LZ4_compress_fast ().

(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

Commencez par aller dans le frame de pile où la fonction LZ4_compress_fast () est en cours d'exécution.

(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);

L'adresse d'exécution à ce moment est 0x404722, donc si vous recherchez le tableau ci-dessus par adresse,

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

Cette ligne correspond. Puisque r6 == $ rbp, l'emplacement du CFA est $ rbp + 16. Et puisque r16 est CFA-8, ** return_address ** est:

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

Devrait être. Puisque cette adresse correspond certainement à l'adresse d'exécution de la trame # 3, cela signifie que l'adresse de l'appelant peut être restaurée.

Il y a quelques mises en garde ici. Je fais référence à r6 comme étant l'emplacement du CFA, qui est maintenant r6 au moment de l'exécution de 0x404722, et non le r6 restauré pour le rappel d'appel. Sinon, il circulera car il est basé sur CFA pour restaurer r6.

En d'autres termes:

  1. Trouvez l'emplacement du CFA en utilisant la valeur de registre actuelle.
  2. Trouvez la valeur du registre à renvoyer à partir de la fonction en cours lors du retour, en utilisant l'emplacement CFA trouvé ci-dessus.

Il est nécessaire de calculer dans l'ordre.

Sur la base de ce qui précède, les fonctions suivantes sont en cours de traitement. En plus de restaurer l'adresse return_address et de restaurer le registre, la pile est également rembobinée ici.

def get_prev_frame(cu: CompileUnit,
                   addr: int,
                   regs: List[int],
                   read_mem: Callable[[int], int]) -> Tuple[Optional[int], Optional[List[int]]]:
    """
Registre et mémoire des adresses d'exécution(Principalement pile)À partir de, identifiez le cadre de la pile,
Restaurer l'adresse de l'appelant et le registre à ce moment
    :param cu:Informations CU
    :param addr:Adresse d'exécution
    :param regs:Liste des registres indexés par numéro de registre DWARF
    :param read_mem:Une fonction qui lit la mémoire. Lire est un pointeur(S'inscrire)Taille
    :return:Adresse de l'appelant et inscription
    """

    if cu.dwarfinfo.has_CFI():
        entries = cu.dwarfinfo.CFI_entries()
    else:
        # .debug_Il n'y a pas de cadre
        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:
                #Restaurer l'adresse de retour et ses registres
                cfa_rule = row["cfa"]  # type: CFARule
                assert cfa_rule.expr is None, "Expression DWARF non prise en charge"
                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, "Non pris en charge sauf OFFSET"
                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, "Non pris en charge sauf OFFSET"
                        prev_regs[key] = read_mem(cfa + reg.arg)

                #Rebobinage de la pile
                unwind_stack(prev_regs, cfa)
                return read_mem(return_address), prev_regs
    return None, None

Contrôle de fonctionnement

Maintenant qu'il est terminé, exécutons-le. Placez bt.py dans le répertoire courant et essayez d'appeler le module python depuis gdb. Si vous continuez à décaler l'image, la valeur du registre changera, alors n'oubliez pas de la ramener à la première image!

(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

Cela a l'air plutôt bien, mais si vous regardez de près, vous remarquerez qu'il manque un cadre. C'est parce que nous n'avons pas analysé correctement l'expansion en ligne. La fonction LZ4_compress_generic () est développée en ligne dans la fonction LZ4_compress_fast_extState (), mais je la décode comme la fonction LZ4_compress_fast_extState () sans m'en apercevoir.

Afin d'interpréter en ligne, vous devez prendre DW_TAG_inlined_subroutine au sérieux, mais il n'y a pas assez d'espace pour l'écrire, donc je vais m'arrêter ici.

De plus, les paramètres ne sont pas affichés. Encore une fois, c'est beaucoup de travail à faire, comme regarder la section .debug_loc, pour l'écrire (ry)

en conclusion

Amis Kemono, je voulais voir la suite. .. ..