C est un langage compilé
On a vu dans le chapitre précédent que les langages Python et C ont quelques points communs, mais ils ont aussi des différences notables. Dans ce chapitre, nous allons aborder la phase de production d’un programme exécutable en langage C.
Python est un langage interprété, ce qui signifie que le code source est exécuté directement par l’interpréteur Python. En revanche, C est un langage compilé, ce qui signifie que le code source doit être transformé en code machine avant de pouvoir être exécuté.
On a déjà compilé/exécuté 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 Installation et premiers pas :
1#include <stdio.h>
2
3int main(){
4 printf("Hello World !");
5 return 0;
6}
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.
Python est un langage compilé
C est un langage compilé
Le processus de compilation (au sens large) se décompose en étapes
La première étape du processus est le
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 <stdio.h> suivi du code de 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
#includeinsère le contenu d’un fichier dans le code sourceLa directive
#includecompile séparément le fichier inclusL’option de
gccpour observer le résultat du pre processing estL’inclusion de
<stdio.h>danshello.cproduit environ lignes de code
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:
1#include <stdio.h>
2
3#define carre1(X) X*X
4#define carre2(X) X*(X)
5#define carre3(X) (X)*(X)
6
7int main()
8{
9 int n = 5;
10 printf("%d\n", carre1(n+3) );
11 printf("%d\n", carre2(n+3) );
12 printf("%d\n", carre3(n+3) );
13}
Effectuer « à la main » l’opération de remplacement pour les trois directives ci dessus, et inférer le résultat obtenu.
La ligne 10 affiche la valeur
La ligne 11 affiche la valeur
La ligne 12 affiche la valeur
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 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.
La compilation au sens strict produit directement un fichier exécutable
La compilation produit des instructions pour la machine
L’option de
gccpour produire le fichier assembleur estLe fichier qui contient les instructions en langage assembleur a l’extension
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 hello.o est appelé un fichier objet.
Le contenu du fichier 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.
L’assemblage produit directement un fichier exécutable
L’assemblage produit le langage machine exécutable par le microprocesseur
Le fichier produit par l’assemblage est appelé un fichier
L’option de
gccpour produire le fichier objet estLe fichier objet a l’extension
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 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 !
L’édition de liens assemble les fichiers objet pour produire un programme exécutable
La commande
gcc hello.c -o helloréalise les 4 étapes en une seule commandeLa commande
gcc hello.c -o helloproduit un fichierLa fonction est un exemple de fonction externe liée lors de l’édition de liens
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.
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 printf(). Pour cette dernière, le prototype <stdio.h> 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.
Pour utiliser une fonction externe, son prototype doit être inclus dans le code source
Pour les fichiers système, la directive d’inclusion utilise
<>comme délimiteursPour les fichiers personnels, la directive d’inclusion utilise
<>comme délimiteursLes fichiers regroupant les prototypes de fonctions portent l’extension
Le prototype de
printfest défini dans le fichier
Nous venons d’utiliser la notion de prototype d’une fonction. Rentrons un peu plus dans le détail.
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,
int gcd(int a, int b);
est le prototype de
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
printf()est placé dans le fichier système<stdio.h>;le prototype de
sqrt()est placé dans le fichier système<math.h>;le prototype de
rand()est placé dans le fichier système<stdlib.h>.
Le prototype doit être inclus dans le code source avec une directive insérée en tête de fichier :
#include <nom_de_fichier.h>pour les fichiers systèmes ;#include "nom_de_fichier.h"pour les autres fichiers ;
Pour illustrer ça, on considère le programme minimal suivant :
1int main(){
2 printf("Hello World !\n");
3 return 0;
4}
Si on essaye de compiler, une erreur se produit ligne 2 lorsqu’on essaye d’utiliser la fonction printf() :
$ 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 '<stdio.h>' or provide a declaration of 'printf'
+++ |+#include <stdio.h>
1 | int main(){
L’insertion de <stdio.h> règle le problème.
1#include <stdio.h>
2
3int main(){
4 printf("Hello World !\n");
5 return 0;
6}
Le prototype d’une fonction contient le corps de la fonction
Le prototype précise le nom, les paramètres et le type de retour d’une fonction
Le prototype d’une fonction se termine par le caractère
Le prototype de
sqrtest défini dans le fichierL’insertion de
<stdio.h>est suffisante pour utiliserprintfsans avertissement du compilateur