Mapper le compteur de programme sur le nom de la fonction dans votre code


  • Français


  • Les compilateurs sont couramment utilisés pour convertir le code source lisible par l’homme en une série d’instructions directement exécutées par l’ordinateur. Une question courante est “Comment les débogueurs et les gestionnaires d’erreurs signalent-ils l’endroit dans le code source où se trouve actuellement le processeur ?” Il existe différentes méthodes pour mapper les instructions vers des emplacements dans le code source. Avec le compilateur optimisant le code, il y a aussi quelques complications dans le mappage des instructions vers le code source. Ce premier article de la série décrit comment les outils mappent le compteur de programme (également appelé pointeur d’instruction) au nom de la fonction. Les articles suivants de cette série couvriront le mappage du compteur de programme sur la ligne spécifique d’un fichier source. Il fournit également une trace décrivant la série d’appels qui ont abouti à ce que le processeur se trouve dans la fonction actuelle.

    Les symboles d’entrée de fonction

    Lors de la traduction du code source en un binaire exécutable, le compilateur conserve les informations de symbole sur l’entrée de chaque fonction. Garder ces informations disponibles permet à l’éditeur de liens d’assembler plusieurs fichiers objets (.o suffixe) dans un seul fichier exécutable. Avant que les fichiers objets ne soient traités par l’éditeur de liens, les adresses finales des fonctions et des variables ne sont pas connues. Les fichiers d’objets ont des espaces réservés pour les symboles qui n’ont pas encore d’adresses. L’éditeur de liens résoudra les adresses lors de la création de l’exécutable. En supposant que l’exécutable résultant n’est pas supprimé, ces symboles décrivant l’entrée de chaque fonction sont toujours disponibles. Vous pouvez en voir un exemple en compilant le programme simple suivant :

    #include <stdlib.h>
    #include <stdio.h>
    
    int a;
    double b;
    
    int
    main(int argc, char* argv[])
    {
    	a = atoi(argv[1]);
    	b = atof(argv[2]);
    	a = a + 1;
    	b = b / 42.0;
    	printf ("a = %d, b = %f\n", a, b);
    	return 0;
    }

    Compilez le code :

    $ gcc -O2 example.c -o example

    Vous pouvez voir où la fonction principale commence à 0x401060 avec la commande suivante :

    $ nm example |grep main
    U __libc_start_main@GLIBC_2.34
    0000000000401060 T main

    Même si le code est compilé sans informations de débogage (-g option), gdb pouvez toujours trouver le début de la fonction principale avec les informations de symbole :

    $ gdb example
    
    GNU gdb (GDB) Fedora 12.1-1.fc36
    …
    (No debugging symbols found in example)
    (gdb) break main
    Breakpoint 1 at 0x401060

    Parcourir le code avec le GDB nexti commande. GDB rapporte l’adresse à laquelle il se trouve actuellement dans le main fonction:

    (gdb) nexti
    0x0000000000401061 in main ()
    (gdb) nexti
    0x0000000000401065 in main ()
    (gdb) where
    #0 0x0000000000401065 in main ()
    

    Cette information minimale est utile mais n’est pas idéale. Le compilateur peut optimiser les fonctions et diviser les fonctions en sections non contiguës pour que le code associé à la fonction ne soit pas manifestement lié à l’entrée de fonction répertoriée. Une partie des instructions de la fonction peut être séparée de l’entrée de la fonction par les entrées d’autres fonctions. En outre, le compilateur peut générer des noms alternatifs qui ne correspondent pas directement aux noms de fonction d’origine. Cela rend plus difficile de déterminer à quelle fonction dans le code source les instructions sont associées.

    Informations sur le nain

    Code compilé avec le -g L’option inclut des informations supplémentaires à mapper entre le code source et l’exécutable binaire. Par défaut, les RPM avec du code compilé sur Fedora ont la génération d’informations de débogage activée. Ensuite, ces informations sont placées dans un fichier séparé debuginfo RPM, qui peut être installé en complément des RPM contenant les binaires. Cela facilite l’analyse des vidages sur incident et le débogage des programmes. Avec debuginfo, vous pouvez obtenir des plages d’adresses qui correspondent à des noms de fonction particuliers. Il fournit également le numéro de ligne et le nom de fichier auquel chaque instruction correspond. Les informations de mappage sont encodées dans le Norme naine.

    La description de la fonction DWARF

    Pour chaque fonction avec une entrée de fonction, il existe une entrée d’informations de débogage DWARF (DIE) la décrivant. Ces informations sont dans un format lisible par machine, mais il existe un certain nombre d’outils, notamment llvm-dwarfdump et eu-readelf qui produisent une sortie lisible par l’homme des informations de débogage DWARF. Ci-dessous le llvm-dwarfdump sortie de l’exemple de fonction principale DIE décrivant la fonction principale de la précédente example.c programme.

    Le DIE commence par le DW_TAG_subprogram pour indiquer qu’il décrit une fonction. Il existe d’autres types de balises DWARF utilisées pour décrire d’autres composants de programmes tels que des types et des variables.

    Le DIE de la fonction a plusieurs attributs commençant chacun par DW_AT_ qui décrivent les caractéristiques de la fonction. Ces attributs fournissent des informations sur la fonction, telles que l’emplacement de la fonction dans le binaire exécutable. Il indique également où le trouver dans le code source d’origine.

    A quelques lignes de la DW_TAG_subprogram est le DW_AT_name attribut qui décrit le nom de la fonction du code source comme main. Le DW_AT_decl_file et DW_AT_decl_line Les attributs DWARF décrivent respectivement le numéro de fichier et de ligne d’où provient la fonction. Cela permet au débogueur de trouver l’emplacement approprié dans un fichier pour vous montrer le code source associé à la fonction. Les informations de colonne sont également incluses avec le DW_AT_decl_column.

    Les autres informations clés pour le mappage entre les instructions binaires et le code source sont les DW_AT_low_pc et DW_AT_high_pc les attributs. L’utilisation de la DW_AT_low_pc et DW_AT_high_pc indique que le code de cette fonction est contigu, allant de 0x401060 (la même valeur que celle fournie par nm commande antérieure) jusqu’à 0x4010b7 non compris. Le DW_AT_ranges L’attribut est utilisé pour décrire des fonctions si la fonction couvre des régions non contiguës.

    Avec le compteur de programme, vous pouvez mapper le compteur de programme du processeur sur le nom de la fonction et trouver le fichier et le numéro de ligne où se trouve la fonction :

    $ llvm-dwarfdump example --name=main
    example:	file format elf64-x86-64
    
    0x00000113: DW_TAG_subprogram
                  DW_AT_external	(true)
                  DW_AT_name	("main")
                  DW_AT_decl_file	("/home/wcohen/present/202207youarehere/example.c")
                  DW_AT_decl_line	(8)
                  DW_AT_decl_column	(0x01)
                  DW_AT_prototyped	(true)
                  DW_AT_type	(0x00000031 "int")
                  DW_AT_low_pc	(0x0000000000401060)
                  DW_AT_high_pc	(0x00000000004010b7)
                  DW_AT_frame_base	(DW_OP_call_frame_cfa)
                  DW_AT_call_all_calls	(true)
                  DW_AT_sibling	(0x000001ea)

    Fonctions en ligne

    Un compilateur peut optimiser le code en remplaçant un appel à une autre fonction par des instructions qui implémentent les opérations de cette fonction appelée. Une fonction intégrée élimine les changements de flux de contrôle causés par l’appel de fonction et les instructions de retour pour implémenter les appels de fonction traditionnels. Pour une fonction en ligne, il n’est pas nécessaire d’exécuter des instructions de prologue et d’épilogue de fonction supplémentaires requises pour se conformer à l’interface binaire d’application (ABI) des appels de fonction traditionnels.

    Les fonctions en ligne offrent également des opportunités supplémentaires d’optimisation car le compilateur peut mélanger les instructions entre l’appelant et la fonction invoquée en ligne. Cela fournit une image complète du code qui peut être éliminé en toute sécurité. Cependant, si vous venez d’utiliser les plages d’adresses des fonctions réelles décrites par le DW_TAG_subprogram, vous pouvez alors attribuer par erreur une instruction à la fonction qui a appelé la fonction inline, plutôt qu’à la fonction inline réelle qui la contient. Pour cette raison, DWARF a le DW_TAG_inlined_subroutine pour fournir des informations sur les fonctions en ligne.

    Étonnamment, même example.cl’exemple simple fourni dans cet article, a des fonctions en ligne dans le code généré, atoi et atof. Le bloc de code ci-dessous montre la sortie de llvm-dwarfdump pour atoi. Il y a deux parties, une DW_TAG_inlined_subroutine pour décrire chaque lieu atoi était en fait en ligne et un DW_TAG_subprogram décrivant les informations génériques qui ne changent pas entre les multiples copies en ligne.

    Le DW_AT_abstract_origin dans le DW_TAG_inlined_subroutine pointe vers l’associé DW_TAG_subprogram qui décrit le fichier avec DW_AT_decl_file et DW_AT_decl_line tout comme un DWARF DIE décrivant une fonction régulière. Dans ce cas, vous voyez que cette fonction en ligne provient de la ligne 362 du fichier système /usr/include/stdlib.h.

    La plage réelle d’adresses associées à atof est non contigu et décrit par DW_AT_ranges, [0x401060,0x401060), [0x401061, 0x401065), [0x401068,0x401074)et [0x40107a,0x41080). Le DW_TAG_inlined_subroutine a un DW_AT_entry_pc pour indiquer quel emplacement est considéré comme le début de la fonction en ligne. Avec les instructions de réorganisation du compilateur, il n’est peut-être pas évident de savoir ce qui serait considéré comme la première instruction d’une fonction en ligne :

    $ llvm-dwarfdump example --name=atoi
    example:	file format elf64-x86-64
    
    0x00000159: DW_TAG_inlined_subroutine
                  DW_AT_abstract_origin	(0x00000208 "atoi")
                  DW_AT_entry_pc	(0x0000000000401060)
                  DW_AT_GNU_entry_view	(0x02)
                  DW_AT_ranges	(0x0000000c
                     [0x0000000000401060, 0x0000000000401060)
                     [0x0000000000401061, 0x0000000000401065)
                     [0x0000000000401068, 0x0000000000401074)
                     [0x000000000040107a, 0x0000000000401080))
                  DW_AT_call_file	("/home/wcohen/present/202207youarehere/example.c")
                  DW_AT_call_line	(10)
                  DW_AT_call_column	(6)
                  DW_AT_sibling	(0x00000196)
    
    0x00000208: DW_TAG_subprogram
                  DW_AT_external	(true)
                  DW_AT_name	("atoi")
                  DW_AT_decl_file	("/usr/include/stdlib.h")
                  DW_AT_decl_line	(362)
                  DW_AT_decl_column	(0x01)
                  DW_AT_prototyped	(true)
                  DW_AT_type	(0x00000031 "int")
                  DW_AT_inline	(DW_INL_declared_inlined)

    Programmation et développement

    Implications des fonctions en ligne

    La plupart des programmeurs pensent que le processeur exécute complètement une ligne dans le code source avant de passer à la suivante. De même, avec la fonction attendue call ABI les programmeurs pensent où caller la fonction termine les opérations dans les instructions avant l’appel et les instructions après l’appel ne sont pas lancées tant que callee le retour de la fonction peut ne pas tenir. Avec la fonction en ligne, les frontières entre les fonctions deviennent floues. Consignes de la caller La fonction peut être planifiée avant ou après les instructions des fonctions en ligne, quel que soit leur emplacement dans le code source, si le compilateur détermine que le résultat final sera le même. Cela peut conduire à des valeurs inattendues lors de l’inspection des variables avant et après les fonctions en ligne.

    Lectures complémentaires

    Cet article couvre une très petite partie de la façon dont le mappage entre le code source et le binaire exécutable est implémenté avec DWARF. Comme point de départ pour en savoir plus sur DWARF, vous pouvez lire Introduction au format de débogage DWARF par Michael J. Désireux. Recherchez les articles à venir sur la manière dont les instructions des fonctions sont mappées au code source et sur la manière dont les backtraces sont générées.

    Source

    Houssen Moshinaly

    Pour contacter personnellement le taulier :

    Laisser un commentaire

    Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

    Copy code