COURS C avancé
COURS C avancé
COURS C avancé
1
Prof. Yassine ZARROUK 2024/2025
Sommaire
2
Prof. Yassine ZARROUK 2024/2025
Des sources à la mémoire
I. Définitions :
1. Le fichier source est un texte exprimé dans un langage de programmation.
Un fichier source contient le code écrit par le programmeur dans un langage lisible par l'humain,
comme le C. Par exemple, un fichier source typique en C pourrait être nommé Hello.c.
Après avoir écrit le fichier source, il doit être compilé pour être converti en un fichier binaire. Ce
fichier binaire est une version traduite du code source en instructions compréhensibles par le
processeur. Ces instructions sont en langage machine (des nombres binaires).
En compilant le fichier source, vous obtenez un fichier objet (fichier .o), qui est un fichier binaire
intermédiaire. Dans notre exemple, si vous compilez Hello.c, vous obtiendrez un fichier nommé
Hello.o.
Une fois tous les fichiers objets créés, ils doivent être liés pour former un fichier exécutable. Ce
fichier contient un point de départ qui indique à l'ordinateur où commencer l'exécution du
programme (en général, la fonction main en C).
Après la phase de linking, vous obtenez un fichier exécutable, par exemple, Hello (ou Hello.exe
sous Windows).
Les scripts ne sont pas transformés en binaire avant leur exécution. L'interpréteur traduit les
instructions ligne par ligne en instructions machine au moment de l'exécution.
Un fichier source (comme un script) est lisible par un humain et contient du code qui doit être
interprété ou compilé.
Un fichier binaire est un fichier contenant du code machine qui peut être directement exécuté
par le processeur.
Un fichier exécutable est un fichier binaire prêt à être exécuté par l'ordinateur, généralement
après avoir été compilé (ce qui n'est pas nécessaire pour les scripts).
La ligne qui commence par #! est appelée shebang. Elle spécifie quel interpréteur doit être utilisé
pour exécuter le script.
• #!/bin/bash indique que le script doit être interprété par l'interpréteur Bash
(généralement utilisé pour les scripts Shell).
Sans cette ligne, l'exécution du script pourrait échouer ou l'utilisateur devrait spécifier
manuellement quel interpréteur utiliser pour le fichier.
Généralement, Les scripts sont des fichiers sources interprétés, contrairement aux fichiers binaires ou
exécutables qui nécessitent une compilation. Leur grande flexibilité provient du fait qu'ils sont faciles à
modifier et qu'ils peuvent être exécutés directement via un interpréteur sans passer par un processus de
compilation. Le choix de l'interpréteur dépend du langage utilisé (Bash, Python, etc.), et cette spécification
est faite via la ligne shebang (#!).
Un langage de programmation est un ensemble de règles syntaxiques et sémantiques qui permet aux
programmeurs de donner des instructions à une machine (ordinateur). Il est un outil qui permet de
manipuler les ressources matérielles (comme la mémoire) en définissant des instructions spécifiques.
Par exemple, en C, vous devez gérer explicitement la mémoire (avec des commandes comme malloc() et
free()), ce qui aide à structurer précisément l'utilisation de la mémoire vive (RAM).Il existe de nombreux
langages : typés, non typés, compilés, interprétés, impératifs, fonctionnels…
• Langages typés : Ils imposent une déclaration de types pour les variables (par exemple,
en C, vous devez déclarer si une variable est un int, un float, etc.).
• Langages non typés : Ils n'imposent pas de types stricts. Par exemple, en Python, vous
pouvez assigner un entier à une variable, puis un texte sans redéclaration.
• Langages compilés : Le code source est converti en un fichier binaire exécutable par un
compilateur (ex : C, C++).
• Langages interprétés : Le code est lu et exécuté ligne par ligne par un interpréteur (ex :
Python, JavaScript).
4
Prof. Yassine ZARROUK 2024/2025
• Langages impératifs : Ils décrivent comment exécuter une série d'instructions, en
modifiant l'état du programme à travers des commandes. Le C est un langage impératif.
• Langages fonctionnels : Ils se basent sur des fonctions mathématiques sans modifier
l'état. Les exemples incluent Haskell et Lisp.
Ici, le programme suit une série d'étapes précises pour arriver au résultat final.
• Langage typé : Le C est fortement typé, ce qui signifie que chaque variable doit être
déclarée avec un type spécifique (comme int, char, etc.). Cela permet de mieux contrôler
l'utilisation de la mémoire et d'éviter certaines erreurs, mais cela impose plus de rigueur
lors de la programmation.
• Langage compilé : En C, le code source doit être compilé en un fichier binaire exécutable
avant de pouvoir être exécuté. Cela signifie qu'il y a une étape intermédiaire où le
programme passe par un compilateur (comme GCC) qui transforme le code lisible par
l'humain en instructions machine. Cela diffère des langages interprétés comme Python,
où vous pouvez exécuter directement le fichier source.
II. La compilation
Le processus de compilation comporte généralement trois étapes principales :
5
Prof. Yassine ZARROUK 2024/2025
int main() {
printf("Maximum value is %d\n", MAX); return 0;
}
Le préprocesseur remplacera #include <stdio.h> par le contenu réel du fichier d'en-tête stdio.h et
remplacera toutes les occurrences de MAX par 100. Le fichier résultant est encore du code source,
mais il est prêt à être compilé. La pré-compilation est une phase dans laquelle le préprocesseur C
analyse et modifie le code source avant qu’il ne soit envoyé au compilateur. Cette étape prend un
ou plusieurs fichiers sources en entrée, et produit un fichier source unique qui est ensuite transmis
à l’étape de compilation. Durant cette phase, le préprocesseur traite des directives spécifiques
qui commencent toutes par un symbole #, telles que #include, #define, et #ifdef.
Lorsqu'une directive #include est rencontrée, le préprocesseur remplace cette ligne par le contenu
du fichier inclus.
Le préprocesseur va insérer le contenu entier du fichier d'en-tête stdio.h à cet emplacement dans
le fichier source. Cela permet au programmeur d’utiliser les fonctions définies dans ce fichier
(comme printf). Les fichiers inclus peuvent être des fichiers d'en-tête standard (comme stdio.h)
ou des fichiers définis par l'utilisateur (comme #include "monheader.h").
Les macros sont des directives qui permettent de définir des substitutions dans le code avant la
compilation. La directive #define permet de définir des macros, qui seront remplacées par leur
valeur au moment de la pré-compilation.
Par exemple, si vous avez #define MAX 100 ,Partout où le terme MAX apparaît dans le fichier
source, il sera remplacé par 100. Si le code contient : int array[MAX]; Après le préprocessing, il
deviendra : int array[100];
Les directives conditionnelles comme #ifdef permettent d’inclure ou d’exclure des parties de code
en fonction de certaines conditions.
Exemple :
#ifdef DEBUG printf("Mode debug activé"); #endif
Si la macro DEBUG est définie quelque part dans le code, le message sera inclus, sinon il sera
ignoré.
C. Le résultat de la pré-compilation
Après la pré-compilation, le fichier source généré est exempt de toute macro et toutes les
inclusions de fichiers sont remplacées par le contenu des fichiers inclus. Cela signifie que toutes
6
Prof. Yassine ZARROUK 2024/2025
les instructions comme #include, #define, et #ifdef ont été traitées et remplacées par leur contenu
ou évaluations.
Le fichier résultant est un fichier source C unique sans dépendances externes (les fichiers d'en-
tête ont été inclus dans le code lui-même). Ce fichier peut alors être transmis au compilateur pour
être transformé en un fichier binaire.
L'illustration en haut montre des fichiers sources séparés (petits rectangles bleus) qui sont ensuite
combinés par le préprocesseur pour produire un fichier source unifié.
• Par exemple, si vous avez trois fichiers source C qui incluent le même fichier d'en-tête
monheader.h, après le préprocessing, le contenu de monheader.h sera présent dans
chacun de ces fichiers avant qu’ils ne soient compilés.
Exemple de pré-compilation :
Prenons un exemple de code « exemple.c » avec plusieurs directives
du préprocesseur :
#define N 10
int main(){
int i,j=0;
for(i=0;i<N;++i) j+=i;
return j;
}
La commande gcc -E exemple.c est utilisée pour effectuer uniquement la phase de pré-processing.
Cette commande produit une sortie où toutes les directives du préprocesseur (#define, #include,
etc.) ont été expansées (traitées) mais sans générer de fichier binaire. Et le préprocesseur
remplace toutes les occurrences de N par 10. Le fichier pré-compilé ressemblerait à quelque chose
comme ceci (en omettant pour simplification le contenu complet de stdio.h) :
# 1 "exemple.c"
# 1 "<built-in>"
# 1 "<command-line>"
7
Prof. Yassine ZARROUK 2024/2025
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "hello.c"
int main(){
int i,j=0;
for(i=0;i<N;++i) j+=i;
return j;
}
Vous pouvez également voir des lignes spécifiques générées par le préprocesseur, comme :
# 1 "exemple.c" # 1 "<built-in>" # 1 "<command-line>" # 1
"/usr/include/stdc-predef.h" 1 3 4
Ces lignes sont des informations de débogage insérées par le préprocesseur pour aider à la
compilation et à l’édition de lien, Elles indiquent la provenance des différentes parties du code
(comme le fichier source original exemple.c ou les fichiers d'en-tête standards inclus par défaut).
Elles aident également le compilateur à savoir d'où proviennent les différentes instructions pour
pouvoir associer les messages d'erreur à la ligne et au fichier approprié.
Ces lignes ne font pas partie du code C compilé mais sont utilisées pour garder une trace des
inclusions et des expansions réalisées lors de la pré-compilation.
La pré-compilation est une étape essentielle qui prépare le code source pour la compilation. Elle
remplace les macros et directives par leur contenu ou évaluation, ce qui simplifie le fichier source
avant de l’envoyer au compilateur. Le préprocesseur n’exécute pas encore le code, il se contente
de le transformer pour rendre la phase de compilation plus directe et efficace.
D. Les macros
D.1. Substitution de macros
Les macros permettent de définir des symboles ou des expressions qui sont substituées dans le
code avant la compilation.
#define NAME VALUE : Cela définit une macro simple où NAME est remplacé par VALUE dans tout
le code source.
• Exemple :
#define PI 3.14 printf("%f", PI); // PI est remplacé par 3.14
lors de la pré-compilation
#define NAME(arg) VALUE : Il s'agit d'une macro paramétrée. arg est un argument qui sera
remplacé par la valeur fournie lors de l'utilisation de la macro.
8
Prof. Yassine ZARROUK 2024/2025
• Exemple :
#define SQUARE(x) (x * x) printf("%d", SQUARE(4)); //
SQUARE(4) est remplacé par (4 * 4), soit 16
Les macros paramétrées sont souvent utilisées pour effectuer des opérations simples sans avoir à
définir une fonction complète.
Ici, la commande gcc -E exécute uniquement la phase de pré-compilation. Lors de cette phase, la
macro N est substituée par la valeur 10 dans le code source. Résultat après pré-processing :
La macro N est remplacée par sa valeur (10), ce qui est la principale fonction des macros dans le
pré-processing.
Dans cet exemple, la macro N n'est pas définie dans le code. Au lieu de cela, elle est définie
directement dans la ligne de commande lors de l'appel à gcc :
int main() { int i = N; }
Commande utilisée : gcc -E -DN=10 exemple.c. Ici, le compilateur reçoit une instruction pour
définir N comme 10 via l'option -D de GCC. Cela permet de définir des macros directement dans
la ligne de commande au lieu de les déclarer dans le code source.
9
Prof. Yassine ZARROUK 2024/2025
int main() { int i = 10; }
La macro N, bien qu'elle ne soit pas présente dans le code, est remplacée par 10 grâce à la
définition donnée dans la ligne de commande.
Ici, la directive conditionnelle #ifndef est utilisée pour vérifier si la macro N est définie. Si ce n'est
pas le cas, elle est définie comme ayant la valeur 10 :
#ifndef N
#define N 10
#endif
int main() { int i = N; }
Ici, comme la macro N n'est pas définie, elle est définie dans le code avec la valeur 10 grâce à la
directive #ifndef.
L'instruction conditionnelle #ifndef (qui signifie "si N n'est pas défini") permet de définir la macro
N uniquement si elle n'a pas été définie auparavant.
Dans ce dernier exemple, nous combinons une définition de macro dans la ligne de commande
avec une directive #ifndef dans le code source :
#ifndef N
#define N 10
#endif
int main() { int i = N; }
Ici, la macro N est déjà définie dans la ligne de commande via l'option -DN=10. Puisque N est
défini, la directive #ifndef n'entre pas en jeu, donc la définition dans le code (#define N 10) est
ignorée.
Dans ce cas, comme N est défini dans la ligne de commande, la directive #ifndef ne redéfinit pas
la macro, ce qui évite des conflits ou des redéfinitions inutiles.
10
Prof. Yassine ZARROUK 2024/2025
Cette partie montre plusieurs façons d'utiliser les macros dans le processus de pré-compilation,
en illustrant la substitution avec la commande GCC. Voici les points clés à retenir :
• Les macros définies avec #define dans le code source sont directement remplacées lors du
pré-processing.
• L'option -D de GCC permet de définir des macros dans la ligne de commande sans modifier le
code source.
• Les directives conditionnelles comme #ifndef permettent de définir des macros uniquement
si elles ne sont pas déjà définies, offrant une plus grande flexibilité pour les configurations de
compilation.
Cela démontre la puissance des macros pour écrire du code plus modulaire et adaptable,
notamment dans les environnements complexes où différents paramètres doivent être ajustés à
la volée lors de la compilation.
La mise en chaîne est utilisée pour concaténer des chaînes ou des symboles dans une macro.
#define s(x) #x : Ici, le # est un opérateur spécial dans le préprocesseur qui convertit l'argument
en chaîne de caractères. Cette technique est utile si vous voulez transformer un nom de variable
ou d'argument en chaîne de caractères.
• Exemple :
#define msg(x) if (x) { \ printf(#x) ; \
}
int main(){ int i=0 ;
gcc -E exemple.c
int main(){ int i=0 ; if (i+1) { printf("i+1") ; } ;
msg(i+1) ;
}
}
• Explication de la syntaxe :
#define msg(x) : Cette macro prend un argument x et crée une condition if qui vérifie la valeur de
x.
• Exécution de la macro :
Lorsque vous appelez cette macro dans un programme comme ceci : int i = 0; msg(i + 1); Le
préprocesseur va remplacer msg(i + 1) par le code suivant : if (i + 1) { printf("i+1"); }
11
Prof. Yassine ZARROUK 2024/2025
Résultat après la pré-compilation (gcc -E exemple.c) :
int main() { int i = 0; if (i + 1) { printf("i+1"); } }
L'utilisation de la mise en chaîne est utile lorsque vous avez besoin de transformer une expression
ou une variable en texte. Cela permet de créer des messages de débogage dynamiques ou de
construire des chaînes de caractères basées sur des expressions dans le code.
• Concaténation (##) :
• L'opérateur ## permet de joindre deux tokens (noms, variables, etc.) en un seul élément.
Cela est utile pour créer des noms uniques ou pour générer dynamiquement des
identifiants basés sur les arguments passés à une macro.
La directive #undef est utilisée pour supprimer une macro précédemment définie. Une fois qu'une
macro est "déréférencée" avec #undef, elle n'est plus disponible dans le reste du fichier source.
• Exemple :
#define PI 3.14
#undef PI printf("%f", PI); // Erreur, car PI a été retiré
12
Prof. Yassine ZARROUK 2024/2025
#endif
#endif : Marque la fin d'une condition #ifdef ou #ifndef.
#else : Permet d'ajouter une alternative si la condition dans #ifdef ou #ifndef n'est pas
remplie.
• Exemple :
#ifdef DEBUG printf("Mode debug\n");
#else printf("Mode production\n");
#endif
#elif : Similaire à #else if, permet de tester d'autres conditions après une première
condition.
• Exemple :
#if VERSION == 1 printf("Version 1\n");
#elif VERSION == 2 printf("Version 2\n");
#else printf("Autre version\n");
#endif
#defined : Peut être utilisé avec #if pour vérifier si une macro est définie.
• Exemple :
#if defined(DEBUG) printf("Mode debug\n"); #endif
Conclusion
Les macros en C sont des outils puissants qui permettent de manipuler le code source avant la
compilation. Grâce à la substitution, à la mise en chaîne, et aux branchages conditionnels, les
développeurs peuvent rendre leur code plus modulaire, flexible, et adaptable à différents
13
Prof. Yassine ZARROUK 2024/2025
environnements. Ces fonctionnalités sont largement utilisées dans les projets logiciels de grande
taille pour améliorer la portabilité et l'efficacité du code.
Cette étape convertit le fichier source (modifié par le préprocesseur) en code objet ou fichier
binaire. Chaque fichier .c est compilé indépendamment pour générer un fichier objet.o . Le code
objet contient les instructions machines, mais ce fichier binaire n'est pas encore exécutable par
lui-même.
Ce processus est réalisé par un compilateur, qui analyse le code, le traduit en instructions
machines, et crée un fichier binaire appelé fichier objet (extension .o ou .obj).
Cependant, ce fichier objet n’est pas encore un exécutable complet. Il contient les instructions
machine pour les fonctions définies dans le code source, mais certaines informations comme les
références externes (fonctions ou variables non définies dans ce fichier) doivent encore être
résolues lors de l'édition de lien.
Lors de la compilation, les instructions du programme sont transformées en code machine. Cela
signifie que chaque fonction définie dans le code est convertie en une série d’instructions que le
processeur pourra exécuter.
Si le fichier source fait référence à une fonction qui n’est pas encore définie, le compilateur la
marque comme un symbole à résoudre. Cela signifie que le compilateur sait qu'une fonction ou
une variable externe est utilisée, mais ne connaît pas encore son emplacement dans le code.
Par exemple, si vous avez un fichier source qui appelle la fonction foo(), mais que cette fonction
est définie dans un autre fichier source, le compilateur va créer une référence à cette fonction
comme un symbole à résoudre.
La résolution de ce symbole sera effectuée plus tard lors de l'édition de lien, où le linker (l'éditeur
de liens) associera ce symbole à sa véritable définition.
Le compilateur gère également les variables globales externes de manière similaire aux fonctions
externes. Si une variable globale est déclarée mais pas définie dans le fichier source, elle sera
marquée comme externe, et sa véritable définition devra être fournie ailleurs (généralement dans
un autre fichier source).
14
Prof. Yassine ZARROUK 2024/2025
Par exemple :
extern int global_var;
Cela indique que la variable global_var existe quelque part dans un autre fichier, mais elle n’est
pas définie dans ce fichier. Le compilateur accepte cela, et la résolution du lien se fera lors de
l'édition de lien.
d. Pour compiler, toute fonction doit être soit implémentée, soit déclarée
Pour que la compilation réussisse, le compilateur doit être en mesure de résoudre toutes les
références aux fonctions et variables. Cela signifie que :
• Implémentées (c'est-à-dire définies dans le même fichier ou dans un fichier déjà lié),
• Déclarées (via une déclaration préalable dans un fichier d'en-tête ou une déclaration
extern).
Si une fonction est appelée dans le code sans être définie ou déclarée, le compilateur générera
une erreur.
Exemple :
#include <stdio.h>
void foo(); // Déclaration de la fonction foo()
int main() { foo(); // Appel de la fonction foo() return 0; } //
Implémentation de la fonction foo() void foo() { printf("Hello,
World!\n"); }
Ici, la fonction foo() est déclarée avant d’être utilisée dans main(). La fonction est ensuite
implémentée plus tard dans le même fichier.
Si la fonction foo() n'était pas déclarée ou implémentée, le compilateur afficherait une erreur
indiquant que la fonction est non définie.
Exemple : objdump
15
Prof. Yassine ZARROUK 2024/2025
int f(int); : C'est une déclaration de la fonction f() qui prend un entier en argument et retourne un
entier. La fonction n'est pas définie dans ce fichier, elle sera probablement définie ailleurs. main()
: La fonction principale initialise une variable i à 0, puis elle appelle la fonction f(i) et retourne son
résultat.
La commande utilisée pour compiler ce fichier source est la suivante : gcc -c exemple.c
L'option -c de GCC permet de compiler le fichier source en un fichier objet (.o) sans procéder à
l'édition de lien. Cela génère un fichier intermédiaire nommé exemple.o, qui contient le code
machine correspondant à main() et à la référence non résolue à f().
Le fichier objet exemple.o est un fichier binaire contenant le code compilé, mais il n'est pas encore
exécutable, car la fonction f() n'a pas été résolue.
La commande objdump -t permet de lister la table des symboles du fichier objet. La table des
symboles contient des informations sur toutes les fonctions et variables présentes dans le fichier
objet, ainsi que les références non résolues qui devront être traitées lors de l'édition de lien.
Le résultat de la commande objdump -t exemple.o est affiché en bas de l’image ci-dessus et peut
être decomposé comme suit :
SYMBOL TABLE : La table des symboles répertorie toutes les fonctions, variables et sections du
fichier objet.
Sections :
16
Prof. Yassine ZARROUK 2024/2025
• .text : Contient le code machine (instructions des fonctions, comme main()).
Symbole main : La fonction main() est présente dans la section .text. Le symbole est marqué
comme g F, où :
• g signifie que le symbole est global, c'est-à-dire qu'il peut être utilisé par d'autres fichiers
objets.
Symbole f : Le symbole f est marqué comme *UND*, ce qui signifie qu'il est non défini dans ce
fichier objet. Cela indique que la fonction f() est déclarée mais pas encore définie. Elle devra être
résolue lors de l'étape d'édition de lien (linking), où le lien entre la fonction f() et son
implémentation sera établi.
l ou g : Ce sont des indicateurs de visibilité. l signifie local (le symbole est interne au fichier objet),
tandis que g signifie global (accessible par d'autres fichiers objets).
*UND* : Cela signifie que le symbole est non défini et devra être résolu par l'éditeur de liens.
Compilation (gcc -c) : Cette étape traduit le fichier source en un fichier objet binaire, mais ne
résout pas encore les références externes comme la fonction f().
Inspection du fichier objet (objdump -t) : Cette commande permet de lister les symboles définis
et non définis dans le fichier objet. Ici, on voit que main est bien défini, mais que f reste une
référence non résolue.
En gros, Le processus de compilation est essentiel pour convertir un programme C écrit en langage
humain en code machine exécutable. Lors de cette phase :
Ce processus prépare le code pour l'étape suivante, qui est l'édition de lien, où toutes les
références externes sont résolues et le programme final exécutable est créé.
17
Prof. Yassine ZARROUK 2024/2025
3. L'édition de lien : binaires → exécutable, bibliothèque
L'éditeur de liens (linker) prend un ou plusieurs fichiers objets (.o) et les relie ensemble pour
former un fichier exécutable. Il combine également les bibliothèques nécessaires (comme la
bibliothèque standard C libc), afin que le programme puisse utiliser des fonctions comme printf().
Après cette étape, le fichier généré est exécutable et peut être lancé directement par le système
d'exploitation.
Des fichiers objets : Ces fichiers (.o) sont générés lors de la compilation et contiennent les
instructions machine correspondant aux fonctions et variables définies dans le code source.
Cependant, certaines fonctions ou variables peuvent ne pas être définies dans le fichier objet
actuel et doivent être résolues à partir d'autres fichiers objets ou bibliothèques.
Des bibliothèques statiques : Ce sont des fichiers qui regroupent plusieurs fichiers objets dans un
seul fichier (.a). Lors de l'édition de lien, le contenu de la bibliothèque statique est copié
directement dans l'exécutable.
• Exemple : libbar.a dans le schéma représente une bibliothèque statique. Les fonctions
nécessaires dans cette bibliothèque seront extraites et ajoutées à l'exécutable final.
Des bibliothèques dynamiques : Ce sont des fichiers partagés (.so sous Linux, .dll sous Windows)
qui ne sont pas inclus directement dans l'exécutable. Au lieu de cela, les références à ces
bibliothèques sont résolues dynamiquement au moment de l'exécution du programme.
Après avoir combiné les fichiers objets et résolu tous les symboles non définis, l'éditeur de lien
produit l'une des deux choses suivantes :
Une bibliothèque dynamique : Si vous créez une bibliothèque partagée (.so), elle pourra être
utilisée par d'autres programmes sans être incluse directement dans chaque exécutable. Les
18
Prof. Yassine ZARROUK 2024/2025
bibliothèques dynamiques sont chargées en mémoire au moment de l'exécution, ce qui réduit la
taille des fichiers exécutables et permet la réutilisation du code.
Un exécutable : Le fichier final qui peut être exécuté directement par le système d'exploitation.
L'exécutable contient toutes les fonctions et variables nécessaires pour que le programme
fonctionne. S'il utilise des bibliothèques dynamiques, elles seront chargées en mémoire lors de
l'exécution.
Entrées :
• a.o et b.o : Ce sont des fichiers objets, générés par la compilation de plusieurs fichiers
sources.
• libbar.a : Bibliothèque statique, dont les symboles requis seront directement copiés dans
l'exécutable.
Processus d'édition de liens (ld) : Le programme ld (linker) prend ces différents fichiers, résout les
symboles et crée soit :
• Un exécutable : Si tous les symboles sont résolus et que les fichiers objets sont
correctement reliés.
Un aspect clé du processus d'édition de lien est la résolution des symboles : Lors de la compilation,
certains symboles (comme des fonctions ou des variables) peuvent être déclarés mais non définis
dans le fichier source. L'éditeur de lien résout ces références en cherchant la définition du symbole
dans d'autres fichiers objets ou bibliothèques. Si un symbole ne peut pas être résolu, une erreur
de lien se produit, indiquant que le programme ne peut pas être finalisé tant que toutes les
références n'ont pas été définies.
Bibliothèque statique : Lorsque vous utilisez une bibliothèque statique (fichier .a), le contenu des
fichiers objets dans la bibliothèque est copié directement dans l'exécutable. Cela signifie que
l'exécutable sera autonome et n'aura pas besoin de la bibliothèque statique à l'exécution.
Bibliothèque dynamique : Lorsqu'une bibliothèque dynamique (.so) est utilisée, elle n'est pas
incluse directement dans l'exécutable. Au lieu de cela, l'exécutable fait référence à cette
bibliothèque, qui sera chargée lorsque le programme sera exécuté. Cela réduit la taille de
l'exécutable et permet aux bibliothèques partagées d'être utilisées par plusieurs programmes.
19
Prof. Yassine ZARROUK 2024/2025
gcc a.o b.o -o programme -L/path/to/lib -lbar -lfoo
• a.o et b.o : Ce sont les fichiers objets qui contiennent le code compilé.
• -L/path/to/lib : Indique au compilateur où chercher les bibliothèques.
• -lbar : Indique que la bibliothèque libbar.a ou libbar.so doit être utilisée.
• -lfoo : Indique que la bibliothèque libfoo.so doit être utilisée.
L'éditeur de liens (ou linker) a pour rôle de combiner plusieurs fichiers objets pour produire un
fichier exécutable ou une bibliothèque. Lors de cette phase, il doit énumérer tous les symboles
présents dans les fichiers objets et les résoudre.
Symboles fournis : Ce sont des symboles (fonctions, variables) qui sont définis dans un fichier objet
particulier. Par exemple, si une fonction foo() est définie dans un fichier objet, ce fichier fournit le
symbole foo.
Symboles manquants : Ce sont des symboles qui sont déclarés mais non définis dans un fichier
objet particulier. Par exemple, une fonction bar() peut être utilisée dans un fichier objet sans y
être définie. Elle est donc considérée comme un symbole manquant dans ce fichier.
Lorsqu'un fichier objet contient un symbole manquant, l'éditeur de liens va chercher ce symbole
dans d'autres fichiers objets ou bibliothèques.
Symbole résolu : Si le symbole manquant est trouvé dans un autre fichier objet ou une
bibliothèque, l'éditeur de liens relie les deux fichiers objets et résout ainsi le symbole.
• Par exemple, si f() est déclaré mais non défini dans a.o et que sa définition est présente
dans b.o, l'éditeur de liens associera a.o et b.o pour résoudre le symbole.
Si un symbole reste non résolu après l'édition de lien, cela signifie qu'aucun fichier objet ou
bibliothèque ne fournit ce symbole. Dans ce cas, l'éditeur de liens produit une erreur de lien.
Sortie d'erreur :
a.o: In function `main`:
a.c:(.text+0xa): undefined reference to `f`
collect2: error: ld returned 1 exit status
Explication de l'erreur :
• undefined reference to 'f' : Cela signifie que la fonction f() a été utilisée dans le fichier
objet a.o, mais que l'éditeur de liens n'a pas pu trouver de définition pour cette fonction
dans les autres fichiers ou bibliothèques fournis.
20
Prof. Yassine ZARROUK 2024/2025
• collect2: error: ld returned 1 exit status : Le programme ld (l'éditeur de liens) a renvoyé
une erreur, ce qui signifie que la compilation n'a pas pu aboutir à un exécutable à cause
de cette référence non résolue.
3.7. Cas d'erreur courants lors de l'édition de lien
Oubli d'une bibliothèque : Si vous oubliez d'inclure une bibliothèque nécessaire à l'édition de lien,
les symboles fournis par cette bibliothèque resteront non résolus.
• Par exemple, si f() est une fonction définie dans une bibliothèque statique ou dynamique,
mais que cette bibliothèque n'est pas spécifiée lors de l'édition de lien, une erreur comme
undefined reference to 'f' se produira.
Symboles mal définis : Si un symbole est mal défini ou non défini dans les fichiers objets fournis,
l'éditeur de lien ne pourra pas le résoudre.
Conflit entre fichiers objets : Si plusieurs fichiers objets tentent de définir le même symbole (par
exemple, deux fichiers définissant une fonction foo()), cela peut aussi causer une erreur de lien.
Vérifier que toutes les bibliothèques nécessaires sont incluses dans la commande de liaison. Par
exemple, si f() est défini dans une bibliothèque libfoo.a, vous devez spécifier cette bibliothèque
lors de l'édition de lien : gcc a.o -o a -lfoo
S'assurer que toutes les fonctions externes utilisées dans le code source sont bien définies dans
les fichiers objets ou bibliothèques fournis.
Utiliser des fichiers d'en-tête (.h) correctement pour déclarer les fonctions externes afin d'éviter
des conflits de déclaration lors de l'édition de lien.
Exemple d'erreur 2 : gcc a.o b.o passe les fichiers a.o et b.o à l'éditeur de lien pour être
combinés en un exécutable.
21
Prof. Yassine ZARROUK 2024/2025
Pour comprendre comment les symboles sont définis dans chaque fichier objet, la commande nm
est utilisée pour examiner les symboles dans a.o et b.o.
• Le symbole U f indique que la fonction f() est non définie dans a.o (elle est "undefined"),
mais utilisée. Elle est marquée comme un symbole non résolu, qui devra être trouvé dans
un autre fichier objet.
Le symbole T main indique que la fonction main() est définie dans a.o.
• Le symbole C f indique que f est un symbole commun, ce qui signifie que c'est
probablement une variable globale non initialisée. Cela suggère que f n'est pas une
fonction dans b.o, mais plutôt une variable globale.
Le segfault survient lors de l'exécution de l'exécutable produit à partir de a.o et b.o. La raison de
cette erreur est un conflit de définition du symbole f.
Pourquoi un segfault ?
Le segfault est causé par une incohérence dans la façon dont f est utilisé dans a.c et défini dans
b.c. Dans ce cas, le fichier a.c utilise f comme une fonction, c'est-à-dire que f() est appelée dans
main().Cependant, le fichier b.c définit f comme une variable globale, et non comme une fonction.
Cette incohérence conduit à une erreur de segmentation (segfault) lors de l'exécution, car le
programme tente d'exécuter une variable globale comme si c'était une fonction.
22
Prof. Yassine ZARROUK 2024/2025
Comment éviter ce genre d'erreur ?
Vérifiez la cohérence des déclarations : Assurez-vous que les fonctions et variables sont déclarées
et définies de manière cohérente entre les différents fichiers source.
Utilisez les fichiers d'en-tête (.h) correctement : Les fichiers d'en-tête permettent de centraliser
les déclarations de fonctions et de variables, garantissant ainsi que tous les fichiers source utilisent
les mêmes définitions.
Outil de vérification : Utilisez des outils comme nm et objdump pour vérifier la façon dont les
symboles sont définis et utilisés dans les fichiers objets avant l'édition de liens.
L'éditeur de lien (linker) ne vérifie pas la cohérence des types des symboles entre différents
fichiers objets. Cela signifie que si deux fichiers objets déclarent ou utilisent le même symbole (par
exemple une fonction ou une variable), mais avec des types différents, le linker ne signalera
aucune erreur à ce stade.
Cela peut entraîner des erreurs à l'exécution si le type attendu pour une fonction ou une variable
n'est pas celui fourni. Par exemple, si une fonction est déclarée comme int foo() dans un fichier et
comme float foo() dans un autre, l'éditeur de lien ne détectera pas cette incohérence, mais cela
pourrait conduire à un comportement indéterminé lors de l'exécution.
Pour éviter ces incohérences, il est essentiel d'utiliser des fichiers d'en-tête (.h) correctement, qui
définissent de manière cohérente les signatures des fonctions et les déclarations de variables. En
incluant les fichiers d'en-tête dans chaque fichier source, vous vous assurez que les types des
symboles sont uniformes dans tout le projet. Le compilateur vérifiera la cohérence des types lors
de la compilation des fichiers individuels, mais l'éditeur de lien ne fait pas cette vérification.
Si un symbole (par exemple une fonction ou une variable) est défini dans plusieurs fichiers objets,
l'éditeur de lien génère une erreur. Un symbole doit être défini une seule fois dans l'ensemble
des fichiers objets fournis au linker. S'il y a plusieurs définitions du même symbole, cela entraîne
une erreur de lien, car le linker ne sait pas quelle version utiliser.
L’exemple montre une erreur de ce type lorsque la fonction pow est définie dans deux fichiers
objets (b.o et c.o) :
Sortie d'erreur :
c.o: In function `pow`:
c.c:(.text+0x0): multiple definition of `pow`
23
Prof. Yassine ZARROUK 2024/2025
b.o:/tmp/b.c:1: first defined here
collect2: error: ld returned 1 exit status
Explication de l'erreur :
multiple definition of 'pow' : Cela signifie que la fonction pow a été définie dans plusieurs fichiers
objets. Ici, pow est définie à la fois dans b.o et dans c.o, ce qui cause un conflit. L'éditeur de lien
signale que pow a été d'abord définie dans b.o, et qu'il a rencontré une redéfinition dans c.o.
Cette erreur se produit parce que chaque symbole (comme une fonction ou une variable) doit être
défini une seule fois dans l'ensemble du programme. Si un même symbole est défini dans
plusieurs fichiers objets, le linker ne peut pas savoir quelle version du symbole utiliser.
Utilisation correcte des déclarations externes : Si une fonction ou une variable est utilisée dans
plusieurs fichiers, mais définie dans un seul fichier, elle doit être déclarée avec le mot-clé extern
dans les autres fichiers. Cela indique au compilateur que la définition se trouve ailleurs. Par
exemple :
• Dans un fichier source (par exemple b.c), vous définissez la fonction pow
• Dans un autre fichier (par exemple a.c), vous déclarez simplement pow comme étant
externe :
Bibliothèques statiques : Si plusieurs fichiers objets doivent utiliser la même fonction, placez-la
dans une bibliothèque statique et liez les fichiers objets à cette bibliothèque plutôt que de
dupliquer le code.
5. Conclusion
En gros, L'édition de lien est une étape essentielle pour générer un exécutable à partir de fichiers
objets. Le linker résout les symboles manquants en reliant les fichiers objets et les bibliothèques.
Toutefois, s'il n'est pas capable de résoudre un symbole, une erreur de lien se produit. Il est
important de s'assurer que toutes les fonctions et variables utilisées dans un programme sont
correctement définies et disponibles pour éviter ce genre d'erreur. Cependant, il y a des pièges à
éviter :
• Le linker ne vérifie pas les types, d'où l'importance d'utiliser des fichiers d'en-tête pour assurer la
cohérence des déclarations et définitions dans tout le projet.
24
Prof. Yassine ZARROUK 2024/2025
• Un symbole ne doit être défini qu'une seule fois dans l'ensemble du projet. Si un symbole est
défini plusieurs fois dans différents fichiers objets, cela entraîne une erreur de lien.
25
Prof. Yassine ZARROUK 2024/2025