.. _02-compilation: Processus de compilation ======================== On vient de compiler/exécuter un premier programme minimal en langage C. Dans ce chapitre nous allons aborder plus en détail le processus de compilation (au sens large) qui conduit à la création d'un programme exécutable. C est un langage compilé, ce qui signifie que le code source doit être transformé avant de pouvoir être exécuté sur la machine. Explorons les différentes étapes de ce processus. Pour illustrer notre propos, on considère le programme minimal déjà utilisé dans le chapitre :ref:`01-getting-started` : .. code-block:: c :linenos: #include int main(){ printf("Hello World !"); return 0; } Le processus de compilation (au sens large) se décompose en 4 étapes : - le pré processing ; - la compilation proprement dite ; - l'assemblage ; - l'édition de liens. Abordons chacune de ces étapes. 1. Le pre processing -------------------- La première phase s'appelle le pre processing et consiste à simplement faire du remplacement de texte dans le fichier source lorsqu'une directive (débutant par ``#``) est rencontrée. Le texte en question sera remplacé par le contenu d'un autre fichier, d'une expression, etc. Dans une première approche, on va s'intéresser seulement à deux directives du préprocesseur (il y en a d'autres) : - ``#include`` - ``#define`` La directive ``#include`` ......................... Cette étape consiste simplement à inclure les lignes du fichier indiqué après la directive ``#include`` dans le code source. On peut observer le résultat de cette opération avec :: $ gcc -E hello.c Le fichier produit fait plus de 700 lignes ! Il contient la définition des types et les prototypes des fonctions contenus dans :file:`` suivi du code de :file:`hello.c`. .. note:: Sous Linux, on peut compter le nombre de lignes produites avec la commande suivante:: $ gcc -E hello.c | wc -l $ 732 L'opérateur ``|`` est un *pipe*, c'est à dire intercepte les données produites par la commande à sa gauche pour les utiliser comme données d'entrée pour la commande à sa droite. La visualisation de ce résultat intermédiaire n'est pas strictement nécessaire pour l'exécution du programme, mais facilite la compréhension globale. La directive ``#define`` ......................... Une ligne débutant par ``#define`` peut être structurée en 3 parties, chacune séparée de la précédente par un espace : - la directive proprement dite suivie d'un espace ; - une première chaine de caractère suivie d'un espace ; - une seconde chaine de caractère. Le pré processeur va rechercher la première chaine de caractères dans le code source et la remplacer littéralement par la seconde. Le code suivant contient trois directives ``#define`` et leur utilisation: .. code-block:: C :linenos: #include #define carre1(X) X*X #define carre2(X) X*(X) #define carre3(X) (X)*(X) int main() { int n = 5; printf("%d\n", carre1(n+3) ); printf("%d\n", carre2(n+3) ); printf("%d\n", carre3(n+3) ); } Effectuer "à la main" l'opération de remplacement pour les trois directives ci dessus, et inférer le résultat obtenu. .. quiz:: quizz-01 :title: La directive #define - La ligne 10 affiche la valeur :quiz:`{"type":"FB","answer":"23", "size":5}` - La ligne 11 affiche la valeur :quiz:`{"type":"FB","answer":"29", "size":5}` - La ligne 12 affiche la valeur :quiz:`{"type":"FB","answer":"64", "size":5}` Vérifier en exécutant le programme 2. La compilation proprement dite --------------------------------- Par abus de langage, on utilise le terme générique de compilation pour désigner le processus global conduisant à la création d'un fichier exécutable à partir du fichier source. Cependant la phase de compilation proprement dite n'est qu'une partie de ce processus global en 4 étapes. Elle consiste à produire des instructions compréhensibles par le microprocesseur. On peut observer ces instructions avec :: $ gcc -S hello.c Le résultat est stocké dans le fichier :file:`hello.s` :: $ more hello.s .file "hello.c" .text .section .rodata .LC0: .string "Hello World !" .text .globl main .type main, @function main: .LFB0: .cfi_startproc endbr64 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 leaq .LC0(%rip), %rdi call puts@PLT movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu 9.3.0-10ubuntu2) 9.3.0" .section .note.GNU-stack,"",@progbits .section .note.gnu.property,"a" .align 8 .long 1f - 0f .long 4f - 1f .long 5 0: .string "GNU" 1: .align 8 .long 0xc0000002 .long 3f - 2f 2: .long 0x3 3: .align 8 4: La visualisation de ce résultat intermédiaire n'est pas strictement nécessaire pour l'exécution du programme, mais facilite la compréhension globale. 3. L'assemblage --------------- L'assemblage permet de produire le langage machine qui sera exécuté sur le microprocesseur. Cette étape est réalisée par la commande :: $ gcc -c hello.c -o hello.o Le fichier :file:`hello.o` est appelé un fichier **objet**. Le contenu du fichier :file:`hello.o` est observable avec un éditeur hexadécimal. L'extension `Hex editor `_ de `VS Code `_ peut être utilisée. Pour le code source précédent, le code machine est le suivant :: 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 01 00 3E 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10 03 00 00 00 00 00 00 00 00 00 00 40 00 00 00 00 00 40 00 0E 00 0D 00 F3 0F 1E FA 55 48 89 E5 48 8D 3D 00 00 00 00 E8 00 00 00 00 B8 00 00 00 00 5D C3 48 65 6C 6C 6F 20 57 6F 72 6C 64 20 21 00 00 47 43 43 3A 20 28 55 62 75 6E 74 75 20 39 2E 33 2E 30 2D 31 30 75 62 75 6E 74 75 32 29 20 39 2E 33 2E 30 00 00 00 04 00 00 00 10 00 00 00 05 00 00 00 47 4E 55 00 02 00 00 C0 04 00 00 00 03 00 00 00 00 00 00 00 14 00 00 00 00 00 00 00 01 7A 52 00 01 78 10 01 1B 0C 07 08 90 01 00 00 1C 00 00 00 1C 00 00 00 00 00 00 00 1B 00 00 00 00 45 0E 10 86 02 43 0D 06 52 0C 07 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00 00 00 04 00 F1 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 La visualisation de ce résultat intermédiaire n'est pas strictement nécessaire pour l'exécution du programme, mais facilite la compréhension globale. 4. L'édition de liens --------------------- Jusqu'à maintenant, nous avons produit le code machine correspondant à notre code source. Mais le code peut faire appel à des fonctions externes ou système qu'il faut "lier" à notre code source pour produire un programme réellement exécutable. Par exemple, notre programme minimal fait appel à la fonction :func:`printf`. dont le fichier objet est stocké quelque part dans le système. L'étape qui consiste à lier les différents fichiers objet nécessaires à l'exécution du programme s'appelle l'édition de liens. La commande :: $ gcc hello.c -o hello réalise l'ensemble des 4 opérations décrites ci dessus: - le pré processing - la compilation - l'assemblage - et l'édition de lien et produit un programme que l'on peut exécuter dans le terminal :: $ ./hello Hello World ! 5. Récapitulatif ---------------- Le schéma ci dessus récapitule les différentes phases, et les fichiers intermédiaires utilisés par le processus global de compilation. .. image:: ../images/compilation.png :width: 75 % :alt: Le processus de compilation :align: center 6. Compilation modulaire ------------------------ Les programmes C que l'on va écrire seront évidemment plus volumineux que le programme minimal qui nous sert de fil rouge dans ce chapitre. Pour la lisibilité du code, et une organisation efficace, il sera important de pouvoir structurer un code volumineux en plusieurs fichiers, de façon à pouvoir réutiliser des fonctions écrites par ailleurs sans avoir à les copier/coller dans le code source que l'on écrit. Pour pouvoir utiliser une fonction externe dans un programme, il y a deux prérequis : - son prototype doit être inclus dans le code qui l'utilise ; - le compilateur doit savoir où la trouver pour construire le programme exécutable. Nous avons déjà utilisé cette façon de faire, en faisant appel à la fonction :func:`printf`. Pour cette dernière, le prototype ```` est défini dans la directive ``#include``, et les fonctions système sont automatiquement liées par le compilateur. Il y aura des situations pour lesquelles il faudra: - préciser la localisation des prototypes des fonctions que l'on a écrites ; - et indiquer au compilateur la localisation des fichiers objet correspondant. Le prototype ............ `Le prototype d'une fonction `_ permet de définir toutes les informations nécessaires pour son utilisation : son nom, les paramètres qu'elle nécessite et le type de la valeur qu'elle retourne. Le prototype est constitué de la partie qui précède le corps de la fonction, terminé par un ``;``. Ainsi, .. code-block:: c int gcd(int a, int b); est le prototype de .. code-block:: c int gcd(int a, int b) { if (b == 0) { return a; } else { return gcd(b, a % b); } } Les fichiers ``.h`` regroupent les prototypes des fonctions par famille. Ainsi : - le prototype de :func:`printf` est placé dans le fichier système :file:`` ; - le prototype de :func:`sqrt` est placé dans le fichier système :file:`` ; - le prototype de :func:`rand` est placé dans le fichier système :file:``. Le prototype doit être inclus dans le code source avec une directive insérée en tête de fichier : - ``#include `` pour les fichiers systèmes ; - ``#include "nom_de_fichier.h"`` pour les autres fichiers ; Pour illustrer ça, on considère le programme minimal suivant : .. code-block:: c :linenos: int main(){ printf("Hello World !\n"); return 0; } Si on essaye de compiler, une erreur se produit ligne 2 lorsqu'on essaye d'utiliser la fonction :func:`printf` : .. code-block:: bash $ gcc -std=c99 -Wall -Wextra hello.c -o hello hello.c: In function ‘main’: hello.c:2:2: warning: implicit declaration of function ‘printf’ [-Wimplicit-function-declaration] 2 | printf("Hello World !\n"); | ^~~~~~ hello.c:2:2: warning: incompatible implicit declaration of built-in function ‘printf’ hello.c:1:1: note: include ‘’ or provide a declaration of ‘printf’ +++ |+#include 1 | int main(){ L'insertion de :file:`` règle le problème. .. code-block:: c :linenos: #include int main(){ printf("Hello World !\n"); return 0; }