Optimisation du compilateur et son effet sur les informations de ligne du débogueur


  • Français


  • Dans mon article précédent, j’ai décrit les informations DWARF utilisées pour mapper des fonctions régulières et en ligne entre un binaire exécutable et son code source. Les fonctions peuvent être des dizaines de lignes, vous aimeriez donc savoir précisément où se trouve le processeur dans votre code source. Le compilateur inclut un mappage d’informations entre les instructions et des lignes spécifiques dans le code source pour fournir un emplacement précis. Dans cet article, je décris les informations de mappage de ligne et certains des problèmes causés par les optimisations du compilateur.

    Commencez avec le même exemple de code de l’article précédent :

    #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;
    }
    

    Le compilateur n’inclut les informations de mappage de ligne que lorsque le code est compilé avec les informations de débogage activées (le -g option):

    $ gcc -O2 -g example.c -o example

    Examen des informations de numéro de ligne

    Les informations de ligne sont stockées dans un format lisible par machine, mais une sortie lisible par l’homme peut être générée avec llvm-objdump ou odjdump.

    $ llvm-objdump --line-numbers example

    Pour la fonction principale, vous obtenez une sortie répertoriant l’instruction de code assembleur avec le numéro de fichier et de ligne associé à l’instruction :

    0000000000401060 <main>:
    ; main():
    ; /home/wcohen/present/202207youarehere/example.c:9
      401060: 53                      		 pushq    %rbx
    ; /usr/include/stdlib.h:364
      401061: 48 8b 7e 08             		 movq    8(%rsi), %rdi
    ; /home/wcohen/present/202207youarehere/example.c:9
      401065: 48 89 f3                		 movq    %rsi, %rbx
    ; /usr/include/stdlib.h:364
      401068: ba 0a 00 00 00          		 movl    $10, %edx
      40106d: 31 f6                   		 xorl    %esi, %esi
      40106f: e8 dc ff ff ff          		 callq    0x401050 <strtol@plt>
    ; /usr/include/bits/stdlib-float.h:27
      401074: 48 8b 7b 10             		 movq    16(%rbx), %rdi
      401078: 31 f6                   		 xorl    %esi, %esi
    ; /usr/include/stdlib.h:364
      40107a: 89 05 c8 2f 00 00       		 movl    %eax, 12232(%rip)   	# 0x404048 <a>
    ; /usr/include/bits/stdlib-float.h:27
      401080: e8 ab ff ff ff          		 callq    0x401030 <strtod@plt>
    ; /home/wcohen/present/202207youarehere/example.c:12
      401085: 8b 05 bd 2f 00 00       		 movl    12221(%rip), %eax   	# 0x404048 <a>
    ; /home/wcohen/present/202207youarehere/example.c:14
      40108b: bf 10 20 40 00          		 movl    $4202512, %edi      	# imm = 0x402010
    ; /home/wcohen/present/202207youarehere/example.c:13
      401090: f2 0f 5e 05 88 0f 00 00 		 divsd    3976(%rip), %xmm0   	# 0x402020 <__dso_handle+0x18>
      401098: f2 0f 11 05 a0 2f 00 00 		 movsd    %xmm0, 12192(%rip)  	# 0x404040 <b>
    ; /home/wcohen/present/202207youarehere/example.c:12
      4010a0: 8d 70 01                		 leal    1(%rax), %esi
    ; /home/wcohen/present/202207youarehere/example.c:14
      4010a3: b8 01 00 00 00          		 movl    $1, %eax
    ; /home/wcohen/present/202207youarehere/example.c:12
      4010a8: 89 35 9a 2f 00 00       		 movl    %esi, 12186(%rip)   	# 0x404048 <a>
    ; /home/wcohen/present/202207youarehere/example.c:14
      4010ae: e8 8d ff ff ff          		 callq    0x401040 <printf@plt>
    ; /home/wcohen/present/202207youarehere/example.c:16
      4010b3: 31 c0                   		 xorl    %eax, %eax
      4010b5: 5b                      		 popq    %rbx
      4010b6: c3
    

    La première consigne à 0x401060 correspond au fichier de code source d’origine example.c ligne 9, l’ouverture { pour la fonction principale.

    La prochaine consigne 0x401061 correspond à la ligne 364 de stdlib.h ligne 364, l’inline atoi fonction. C’est la mise en place de l’un des arguments à la suite strtol appel.

    L’instruction 0x401065 est également associé à l’ouverture { de la fonction principale.

    Instructions 0x401068 et 0x40106d définir les arguments restants pour le strtol appel qui a lieu à 0x40106f. Dans ce cas, vous pouvez voir que le compilateur a réorganisé les instructions et provoque des rebonds entre la ligne 9 de example.c et la ligne 364, ou le stdlib.h include, au fur et à mesure que vous parcourez les instructions du débogueur.

    Vous pouvez également voir un mélange d’instructions pour les lignes 12, 13 et 14 de example.c dans la sortie de llvm-objdump au-dessus de. Le compilateur a déplacé les instructions de division (0x40190) pour la ligne 13 avant certaines des instructions de la ligne 12 pour masquer la latence de la division. Au fur et à mesure que vous parcourez les instructions du débogueur pour ce code, vous voyez le débogueur sauter d’une ligne à l’autre plutôt que de suivre toutes les instructions d’une ligne avant de passer à la ligne suivante. Notez également que lorsque vous avancez, la ligne 13 avec l’opération de division n’a pas été affichée, mais la division s’est définitivement produite pour produire la sortie. Vous pouvez voir GDB rebondir entre les lignes lorsque vous parcourez la fonction principale du programme :

    (gdb) run 1 2
    Starting program: /home/wcohen/present/202207youarehere/example 1 2
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib64/libthread_db.so.1".
    
    Breakpoint 1, main (argc=3, argv=0x7fffffffdbe8) at /usr/include/stdlib.h:364
    364      return (int) strtol (__nptr, (char **) NULL, 10);
    (gdb) print $pc
    $10 = (void (*)()) 0x401060 <main>
    (gdb) next
    10   	 a = atoi(argv[1]);
    (gdb) print $pc
    $11 = (void (*)()) 0x401061 <main+1>
    (gdb) next
    11   	 b = atof(argv[2]);
    (gdb) print $pc
    $12 = (void (*)()) 0x401074 <main+20>
    (gdb) next
    10   	 a = atoi(argv[1]);
    (gdb) print $pc
    $13 = (void (*)()) 0x40107a <main+26>
    (gdb) next
    11   	 b = atof(argv[2]);
    (gdb) print $pc
    $14 = (void (*)()) 0x401080 <main+32>
    (gdb) next
    12   	 a = a + 1;
    (gdb) print $pc
    $15 = (void (*)()) 0x401085 <main+37>
    (gdb) next
    14   	 printf ("a = %d, b = %f\n", a, b);
    (gdb) print $pc
    $16 = (void (*)()) 0x4010ae <main+78>
    (gdb) next
    a = 2, b = 0.047619
    15   	 return 0;
    (gdb) print $pc
    $17 = (void (*)()) 0x4010b3 <main+83>

    Avec cet exemple simple, vous pouvez voir que l’ordre des instructions ne correspond pas au code source d’origine. Lorsque le programme s’exécute normalement, vous n’observerez jamais ces changements. Cependant, ils sont assez visibles lors de l’utilisation d’un débogueur pour parcourir le code. Les frontières entre les lignes de code deviennent floues. Cela a d’autres implications. Lorsque vous décidez de définir un point d’arrêt sur une ligne suivant une ligne avec une mise à jour de variable, le planificateur du compilateur peut avoir déplacé la variable après l’emplacement où vous vous attendez à ce que la variable soit mise à jour, et vous n’obtenez pas la valeur attendue pour la variable à la point d’arrêt.

    Laquelle des instructions d’une ligne obtient le point d’arrêt ?

    Avec le précédent example.c, le compilateur a généré plusieurs instructions pour implémenter des lignes de code individuelles. Comment le débogueur sait-il laquelle de ces instructions doit être celle sur laquelle il place le point d’arrêt ? Il existe un indicateur d’instruction supplémentaire dans les informations de ligne qui marque les emplacements recommandés pour placer les points d’arrêt. Vous pouvez voir ces instructions marquées par S dans la colonne ci-dessous SBPE dans eu-readelf --debug-dump=decodedline example:

    DWARF section [31] '.debug_line' at offset 0x50fd:
    
     CU [c] example.c
      line:col SBPE* disc isa op address (Statement Block Prologue Epilogue *End)
      /home/wcohen/present/202207youarehere/example.c (mtime: 0, length: 0)
         9:1   S        0   0  0 0x0000000000401060 <main>
        10:2   S        0   0  0 0x0000000000401060 <main>
      /usr/include/stdlib.h (mtime: 0, length: 0)
       362:1   S        0   0  0 0x0000000000401060 <main>
       364:3   S        0   0  0 0x0000000000401060 <main>
      /home/wcohen/present/202207youarehere/example.c (mtime: 0, length: 0)
         9:1            0   0  0 0x0000000000401060 <main>
      /usr/include/stdlib.h (mtime: 0, length: 0)
       364:16           0   0  0 0x0000000000401061 <main+0x1>
       364:16           0   0  0 0x0000000000401065 <main+0x5>
      /home/wcohen/present/202207youarehere/example.c (mtime: 0, length: 0)
         9:1            0   0  0 0x0000000000401065 <main+0x5>
      /usr/include/stdlib.h (mtime: 0, length: 0)
       364:16           0   0  0 0x0000000000401068 <main+0x8>
       364:16           0   0  0 0x000000000040106f <main+0xf>
       364:16           0   0  0 0x0000000000401074 <main+0x14>
      /usr/include/bits/stdlib-float.h (mtime: 0, length: 0)
        27:10           0   0  0 0x0000000000401074 <main+0x14>
      /usr/include/stdlib.h (mtime: 0, length: 0)
       364:10           0   0  0 0x000000000040107a <main+0x1a>
      /home/wcohen/present/202207youarehere/example.c (mtime: 0, length: 0)
        11:2   S        0   0  0 0x0000000000401080 <main+0x20>
      /usr/include/bits/stdlib-float.h (mtime: 0, length: 0)
        25:1   S        0   0  0 0x0000000000401080 <main+0x20>
        27:3   S        0   0  0 0x0000000000401080 <main+0x20>
        27:10           0   0  0 0x0000000000401080 <main+0x20>
        27:10           0   0  0 0x0000000000401085 <main+0x25>
      /home/wcohen/present/202207youarehere/example.c (mtime: 0, length: 0)
        12:2   S        0   0  0 0x0000000000401085 <main+0x25>
        12:8            0   0  0 0x0000000000401085 <main+0x25>
        14:2            0   0  0 0x000000000040108b <main+0x2b>
        13:8            0   0  0 0x0000000000401090 <main+0x30>
        13:4            0   0  0 0x0000000000401098 <main+0x38>
        12:8            0   0  0 0x00000000004010a0 <main+0x40>
        14:2            0   0  0 0x00000000004010a3 <main+0x43>
        12:4            0   0  0 0x00000000004010a8 <main+0x48>
        13:2   S        0   0  0 0x00000000004010ae <main+0x4e>
        14:2   S        0   0  0 0x00000000004010ae <main+0x4e>
        15:2   S        0   0  0 0x00000000004010b3 <main+0x53>
        16:1            0   0  0 0x00000000004010b3 <main+0x53>
        16:1            0   0  0 0x00000000004010b6 <main+0x56>
        16:1       *    0   0  0 0x00000000004010b6 <main+0x56>
    
    
    • Les groupes d’instructions sont délimités par le chemin d’accès au fichier source de ces instructions.
    • La colonne de gauche contient le numéro de ligne et la colonne auxquels l’instruction renvoie, suivis des drapeaux.
    • Le nombre hexadécimal est l’adresse de l’instruction, suivie du décalage dans la fonction de l’instruction.

    Si vous regardez attentivement la sortie, vous voyez que certaines instructions correspondent à plusieurs lignes dans le code. Par exemple, 0x0000000000401060 correspond aux lignes 9 et 10 de example.c. La même instruction correspond également aux lignes 362 et 364 de /usr/include/stdlib.h. Les mappages ne sont pas un à un. Une ligne de code source peut correspondre à plusieurs instructions et une instruction peut correspondre à plusieurs lignes de code. Lorsque le débogueur décide d’imprimer un mappage d’une seule ligne pour une instruction, ce n’est peut-être pas celui que vous attendez.

    Fusion et suppression de lignes

    Comme vous l’avez vu dans la sortie des informations de mappage de ligne détaillées, les mappages ne sont pas un à un. Il existe des cas où le compilateur peut éliminer des instructions car elles n’ont aucun effet sur le résultat final du programme. Le compilateur peut également fusionner des instructions à partir de lignes distinctes grâce à des optimisations, telles que l’élimination de sous-expressions communes (CSE), et omettre que l’instruction puisse provenir de plusieurs endroits dans le code.

    L’exemple suivant a été compilé sur une machine x86_64 Fedora 36, ​​en utilisant GCC-12.2.1. Selon l’environnement particulier, vous pouvez ne pas obtenir les mêmes résultats, car différentes versions de compilateurs peuvent optimiser le code différemment.

    Noter la sinon déclaration dans le code. Les deux ont des déclarations faisant les mêmes divisions coûteuses. Le compilateur factorise l’opération de division.

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

    Regarder objdump -dl whichlinevous voyez une opération de division dans le binaire :

    /home/wcohen/present/202207youarehere/whichline.c:13
      401085:    b8 64 00 00 00  		 mov	$0x64,%eax
      40108a:    f7 fb           		 idiv   %ebx

    La ligne 13 est l’une des lignes avec une division, mais vous pourriez soupçonner qu’il existe d’autres numéros de ligne associés à ces adresses. Regardez la sortie de eu-readelf --debug-dump=decodedline whichline pour voir s’il existe d’autres numéros de ligne associés à ces adresses.

    La ligne 11, où se produit l’autre division, n’est pas dans cette liste :

      /usr/include/stdlib.h (mtime: 0, length: 0)
       364:16       0   0  0 0x0000000000401082 <main+0x32>
       364:16       0   0  0 0x0000000000401085 <main+0x35>
      /home/wcohen/present/202207youarehere/whichline.c (mtime: 0, length: 0)
       10:2   S    	0   0  0 0x0000000000401085 <main+0x35>
       13:3   S    	0   0  0 0x0000000000401085 <main+0x35>
       15:2   S    	0   0  0 0x0000000000401085 <main+0x35>
       13:5        	0   0  0 0x0000000000401085 <main+0x35>

    Si les résultats ne sont pas utilisés, le compilateur peut éliminer complètement la génération de code pour certaines lignes.

    Prenons l’exemple suivant, où le autre la clause calcule c = 100 * amais ne l’utilise pas :

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

    Programmation et développement

    Compiler eliminate.c avec GCC :

    $ gcc -O2 -g eliminate.c -o eliminate

    Lorsque vous regardez à travers la sortie générée par objdump -dl eliminateil n’y a aucun signe de multiplication pour 100 * a (ligne 14) de eliminate.c. Le compilateur a déterminé que la valeur n’était pas utilisée et l’a éliminée.

    Lorsque vous regardez à travers la sortie de objdump -dl eliminateil n’y a pas:

    /home/wcohen/present/202207youarehere/eliminate.c:14

    Peut-être est-il masqué comme l’une des autres vues des informations de ligne. Vous pouvez utiliser eu-readelf avec le --debug-dump option pour obtenir une vue complète des informations de la ligne :

    $ eu-readelf --debug-dump=decodedline eliminate > eliminate.lines

    Il s’avère que GCC a enregistré certaines informations de mappage. Il paraît que 0x4010a5 correspond à l’instruction de multiplication, en plus du printf à la ligne 15 :

     /home/wcohen/present/202207youarehere/eliminate.c (mtime: 0, length: 0)
    …
    	18:1        	0   0  0 0x00000000004010a4 <main+0x54>
    	14:3   S    	0   0  0 0x00000000004010a5 <main+0x55>
    	15:3   S    	0   0  0 0x00000000004010a5 <main+0x55>
    	15:3        	0   0  0 0x00000000004010b0 <main+0x60>
    	15:3   	*	0   0  0 0x00000000004010b6 <main+0x66>

    L’optimisation affecte les informations de ligne

    Les informations de ligne incluses dans les binaires compilés sont utiles pour identifier où se trouve le processeur dans le code. Cependant, l’optimisation peut affecter les informations de ligne et ce que vous voyez lors du débogage du code.

    Lorsque vous utilisez un débogueur, attendez-vous à ce que les limites entre les lignes de code soient floues et que le débogueur soit susceptible de rebondir entre elles lors de la progression dans le code. Une instruction peut correspondre à plusieurs lignes de code source, mais le débogueur ne peut en signaler qu’une seule. Le compilateur peut éliminer entièrement les instructions associées à une ligne de code, et il peut inclure ou non des informations de mappage de ligne. Les informations de ligne générées par le compilateur sont utiles, mais gardez à l’esprit que quelque chose peut être perdu lors de la traduction.

    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