La suite d’outils GCC

Présentation générale

La suite d’outils GCC (GNU Compiler Collection) est un composant clé de la chaîne d’outils GNU ( → GNU Toolchain).

Cette chaîne d’outils a vu le jour dans les années 80 dans le cadre du projet GNU lancé par Richard Stallman et dont le but consistait à créer un système de type UNIX complètement libre de droit.

GNU est l’abbréviation de Gnu is Not Unix.

C’est ce qu’on appelle un “acronyme récursif” :

GNU (GNU (GNU (…​) is Not Unix) is Not Unix) is Not Unix.

Linus Torvalds a, par la suite, conçu un noyau de système d’exploitation nommé (modestement…​) Linux qui, combiné à la chaîne d’outils GNU, a donné naissance au système d’exploitation GNU/Linux que l’on désigne plus souvent et plus simplement par le seul nom du noyau c’est-à-dire Linux.

Contrairement à ce que l’on pourrait penser, GCC ne fourni pas uniquement un compilateur pour le langage C/C++. Il prend également en charge plusieurs autres langages :

  • Fortran

  • Ada

  • Java

  • Objective C (une variation du langage C/C++ utilisée par exemple par Apple).

Le nom du compilateur java dans la suite GCC est gcj. Celui du compilateur C++ est g++ et celui du compilateur C est…​ gcc (→ c’est sûrement pour cette raison que l’on croit habituellement que la suite GCC se résume au seul compilateur gcc).

Le processus de génération d’un fichier exécutable à partir d’un code source en langage C se décompose dans GCC en 4 étapes qu’il est INDISPENSABLE de connaître pour faire face aux différents cas de figure qui peuvent se présenter pour produire une application :

processus compilation

Lorsqu’on compile un code source C, on exécute donc la commande gcc qui va en fait invoquer non seulement le compilateur mais également et successivement les différents outils mentionnés dans le schéma précédent (cppgccasld). C’est pour cette raison que gcc est parfois qualifié d'enchaîneur de passes.

Détail des passes

Pour détailler les différentes étapes, on va se baser sur le programme C suivant qui renvoie le minimum entre la valeur passée en argument au programme et une constante (→ SEUIL_MAX)

myapp.h
// Constante
#define SEUIL_MAX 50
// Macro
#define MIN(a,b) ((a) < (b) ? (a) : (b))
myapp.c
#include <stdio.h>
#include <stdlib.h>
#include "myapp.h"

int main(int argc, char *argv[]) {
    int val = atoi(argv[ 1 ]);
    int min;

    min = MIN(val, SEUIL_MAX);

    printf("Min(%d,%d) = %d\n", val, SEUIL_MAX, min);
    return min;
}
Résultat exécution
$ ./myapp 6
Min(6,50) = 6
$ ./myapp 75
Min(75,50) = 50
$

Précompilation

Pour stopper l’enchaineur de passe gcc juste après la précompilation il suffit de taper la commande :

$ gcc -E -o myapp.i myapp.c (1)
$
1 On lance l’enchaineur de passe gcc sur le fichier myapp.c mais on l’arrête après la précompilation (→ option -E). Le résultat est stocké dans le fichier myapp.i (→ option -o myapp.i)

La commande file appliquée sur le fichier obtenu indique toujours que c’est un code source en langage C.

$ file myapp.i
myapp.i: C source, ASCII text
$

Son contenu est le suivant :

myapp.i
# 1 "myapp.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "myapp.c"
# 1 "/usr/include/stdio.h" 1 3 4
[...] (1)
# 5 "myapp.c"
int main(int argc, char *argv[]) {
    int val = atoi(argv[ 1 ]);
    int min;

    min = ((val) < (50) ? (val) : (50)); (2)(3)

    printf("Min(%d,%d) = %d\n", val, 50, min);(3)
    return min;
}
1 ici, on a le contenu de tous les fichiers d’entête inclus (→ stdio.h, stdlib.h, myapp.h)
2 expansion de la macro MIN(…​)((a) < (b) ? (a) : (b))
3 substitution de la constante SEUIL_MAX par sa valeur → 50

Compilation

Pour stopper l’enchaineur de passe gcc après la compilation et juste avant l’appel de l’assembleur il suffit de taper la commande :

$ gcc -S -o myapp.s myapp.c (1)
$
1 On lance l’enchaineur de passe gcc sur le fichier myapp.c mais on l’arrête avant l’appel à l’assembleur (→ option -S). Le résultat est stocké dans le fichier myapp.s (→ option -o myapp.s)

La commande file appliquée sur le fichier obtenu indique cette fois-ci que c’est un code source en assembleur.

$ file myapp.s
myapp.s: assembler source, ASCII text
$

Son contenu est le suivant :

myapp.s
[...]
 .type   main, %function
main:
        @ args = 0, pretend = 0, frame = 16
        @ frame_needed = 1, uses_anonymous_args = 0
        push    {fp, lr} (1)
        add     fp, sp, #4
        sub     sp, sp, #16
        str     r0, [fp, #-16]
        str     r1, [fp, #-20]
        ldr     r3, [fp, #-20]
        add     r3, r3, #4
        ldr     r3, [r3]
        mov     r0, r3
        bl      atoi
        str     r0, [fp, #-8]
        ldr     r3, [fp, #-8]
        cmp     r3, #50
        movlt   r3, r3
        movge   r3, #50
        str     r3, [fp, #-12]
        ldr     r3, [fp, #-12]
        mov     r2, #50
        ldr     r1, [fp, #-8]
        ldr     r0, .L3
        bl      printf
        ldr     r3, [fp, #-12]
        mov     r0, r3
        sub     sp, fp, #4
        @ sp needed
        pop     {fp, pc}
.L4:
        .align  2
.L3:
        .word   .LC0
        .size   main, .-main
        .ident  "GCC: (Raspbian 8.3.0-6+rpi1) 8.3.0"
        .section        .note.GNU-stack,"",%progbits
1 Les instructions en langage C ont été traduites en instructions assembleur.

Assemblage

Pour stopper l’enchaineur de passe gcc après la phase d’assemblage (juste avant l’édition de lien), il suffit de taper la commande :

$ gcc -c myapp.c (1)
$
1 On lance l’enchaineur de passe gcc sur le fichier myapp.c mais on l’arrête avant l’édition de liens (→ option -c). Le résultat est stocké par défaut dans le fichier myapp.o.

La commande file appliquée sur le fichier obtenu indique cette fois-ci c’est un fichier “relogeable” appelé plus couramment fichier objet.

$ file myapp.o
myapp.o: ELF 32-bit LSB relocatable, ARM, EABI5 version 1 (SYSV), not stripped
$

Ce fichier est un fichier binaire et non un fichier texte comme ceux qu’on a pu rencontrer jusqu’ici : les octets qu’il contient n’ont pas vocation à coder des caractères.

$ less myapp.o
"myapp.o" may be a binary file.  See it anyway? (1)
^?ELF^A^A^A^@^@^@^@^@^@^@^@^@^A^(^@^A^@^@^@^@^@^@^@^@^@^@^@<8C>^B^@^@^@^@^@^E4 (2)
^@^@^@^@^@(^@^L^@^K^@^@H-<E9>^D<B0><8D><E2>^P<D0>M<E2>^P^@^K<E5>^T^P^K<E5>^T0ESC<E5>^D0<83><E2>^@0<93><E5>^C^@<A0><E1><FE><FF><FF><EB>^H^@^K<E5>^H0ESC<E5>2^@S
<E3>^C0<A0><B1>20<A0><A3>^L0^K<E5>^L0ESC<E5>2 <A0><E3>^H^PESC<E5>^P^@<9F><E5>
<FE><FF><FF><EB>^L0ESC<E5>^C^@<A0><E1>^D<D0>K<E2>^@<88><BD><E8>^@^@^@^@Min(%d,%d) = %d
^@^@GCC: (Raspbian 8.3.0-6+rpi1) 8.3.0^@A.^@^@^@aeabi^@^A$^@^@^@^E6^@^F^F^H^A   ^A
[...]
$ hexdump -C myapp.o (3)
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 28 00 01 00 00 00  00 00 00 00 00 00 00 00  |..(.............|
00000020  8c 02 00 00 00 00 00 05  34 00 00 00 00 00 28 00  |........4.....(.|
00000030  0c 00 0b 00 00 48 2d e9  04 b0 8d e2 10 d0 4d e2  |.....H-.......M.|
00000040  10 00 0b e5 14 10 0b e5  14 30 1b e5 04 30 83 e2  |.........0...0..|
00000050  00 30 93 e5 03 00 a0 e1  fe ff ff eb 08 00 0b e5  |.0..............|
00000060  08 30 1b e5 32 00 53 e3  03 30 a0 b1 32 30 a0 a3  |.0..2.S..0..20..|
00000070  0c 30 0b e5 0c 30 1b e5  32 20 a0 e3 08 10 1b e5  |.0...0..2 ......|
00000080  10 00 9f e5 fe ff ff eb  0c 30 1b e5 03 00 a0 e1  |.........0......|
00000090  04 d0 4b e2 00 88 bd e8  00 00 00 00 4d 69 6e 28  |..K.........Min(|
000000a0  25 64 2c 25 64 29 20 3d  20 25 64 0a 00 00 47 43  |%d,%d) = %d...GC|
000000b0  43 3a 20 28 52 61 73 70  62 69 61 6e 20 38 2e 33  |C: (Raspbian 8.3|
[...]
1 la commande less détecte que c’est un fichier binaire et demande confirmation pour le visualiser
2 Si on confirme l’affichage du fichier binaire sous forme de texte, celui-ci est illisible
3 Pour afficher la représentation hexadécimale des octets du fichier, on peut utiliser la commande hexdump

Édition de liens

La dernière étape consiste à “combiner” le fichier myapp.o avec les librairies qu’il utilise ou les autres fichiers objet de l’application de façon à rendre possible l’appel à leurs fonctions, variables, objets…​ lors de son exécution.

Ainsi, on voit ci-dessous, grâce à la commande nm, que notre fichier myapp.o fait référence à 2 fonctions (→ atoi et printf) qui ne sont pas définies dans le fichier (→ U comme Undefined)

$ nm myapp.o
         U atoi
00000000 T main
         U printf
$

Pour procéder à la génération de l’exécutable depuis le code source, il suffit de taper la commande :

$ gcc -o myapp myapp.c (1)
1 l’exécutable généré est stocké dans le fichier myapp (→ option -o myapp). Sans cette option, l’exécutable sera nommé par défaut a.out

La commande file appliquée sur le fichier obtenu indique bien que c’est un fichier exécutable.

$ file myapp
myapp: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=ea8e69c9683b1b490af34e099d3d77cba62d7256, not stripped
$

🞄  🞄  🞄