Comment fonctionne le processus de compilation et de liaison?
(Remarque: il s'agit d'une entrée dans FAQ C++ de Stack Overflow . Si vous souhaitez critiquer l'idée de fournir un FAQ dans ce formulaire, alors l'affichage sur meta qui a commencé tout cela serait l'endroit pour le faire. Les réponses à cette question sont suivies dans le salle de discussion C++ , où le FAQ idée a commencé à la base, donc votre réponse sera très probablement lue par ceux qui en ont eu l'idée.)
La compilation d’un programme C++ implique trois étapes:
Prétraitement: le préprocesseur prend un fichier de code source C++ et traite le #include
s, le #define
s et d'autres directives du préprocesseur. Le résultat de cette étape est un fichier "pur" C++ sans directives de pré-processeur.
Compilation: le compilateur prend la sortie du pré-processeur et en génère un fichier objet.
Liaison: l'éditeur de liens prend les fichiers objets produits par le compilateur et génère une bibliothèque ou un fichier exécutable.
Le préprocesseur gère les directives de préprocesseur, comme #include
et #define
. Il est agnostique de la syntaxe de C++, raison pour laquelle il doit être utilisé avec précaution.
Il fonctionne sur un fichier source C++ à la fois en remplaçant les directives #include
par le contenu des fichiers respectifs (qui ne sont généralement que des déclarations), en remplaçant les macros (#define
) et en sélectionnant différentes parties du fichier. texte dépendant des directives #if
, #ifdef
et #ifndef
.
Le pré-processeur fonctionne sur un flux de jetons de pré-traitement. La substitution de macro est définie comme le remplacement de jetons par d'autres jetons (l'opérateur ##
permet de fusionner deux jetons quand cela a du sens).
Après tout cela, le préprocesseur produit une seule sortie qui est un flux de jetons résultant des transformations décrites ci-dessus. Il ajoute également des marqueurs spéciaux qui indiquent au compilateur d'où provient chaque ligne afin qu'il puisse les utiliser pour produire des messages d'erreur sensibles.
Certaines erreurs peuvent être générées à ce stade avec une utilisation intelligente des directives #if
et #error
.
L'étape de compilation est effectuée sur chaque sortie du préprocesseur. Le compilateur analyse le code source pur C++ (maintenant sans aucune directive de préprocesseur) et le convertit en code Assembly. Invoque ensuite le back-end sous-jacent (assembleur dans la chaîne d'outils) qui assemble ce code en un code machine produisant un fichier binaire réel dans un format (ELF, COFF, a.out, ...). Ce fichier objet contient le code compilé (sous forme binaire) des symboles définis dans l'entrée. Les symboles dans les fichiers d'objet sont désignés par leur nom.
Les fichiers objet peuvent faire référence à des symboles non définis. C'est le cas lorsque vous utilisez une déclaration et ne lui en donnez pas la définition. Cela ne dérange pas le compilateur, qui produira volontiers le fichier objet tant que le code source est bien formé.
Les compilateurs vous laissent généralement arrêter la compilation à ce stade. Ceci est très utile car vous pouvez compiler chaque fichier de code source séparément. L'avantage que cela offre est que vous n'avez pas besoin de recompiler tout si vous ne modifiez qu'un seul fichier.
Les fichiers d'objets produits peuvent être placés dans des archives spéciales appelées bibliothèques statiques, pour une réutilisation ultérieure plus facile.
C'est à ce stade que les erreurs "normales" du compilateur, telles que les erreurs de syntaxe ou les erreurs de résolution de surcharge, sont signalées.
L'éditeur de liens est ce qui produit la sortie de la compilation finale à partir des fichiers objets produits par le compilateur. Cette sortie peut être une bibliothèque partagée (ou dynamique) (et bien que le nom soit similaire, elles n’ont pas grand chose en commun avec les bibliothèques statiques mentionnées précédemment) ou un exécutable.
Il relie tous les fichiers objets en remplaçant les références aux symboles non définis par les adresses correctes. Chacun de ces symboles peut être défini dans d'autres fichiers objets ou dans des bibliothèques. S'ils sont définis dans des bibliothèques autres que la bibliothèque standard, vous devez en informer l'éditeur de liens.
À ce stade, les erreurs les plus courantes sont les définitions manquantes ou les définitions en double. Le premier signifie que les définitions n’existent pas (c’est-à-dire qu’elles ne sont pas écrites) ou que les fichiers objets ou les bibliothèques où ils résident n’ont pas été donnés à l’éditeur de liens. Ce dernier est évident: le même symbole a été défini dans deux fichiers objets ou bibliothèques différents.
Ce sujet est discuté à CProgramming.com:
https://www.cprogramming.com/compilingandlinking.html
Voici ce que l'auteur a écrit:
Compiler n'est pas tout à fait la même chose que créer un fichier exécutable! La création d'un exécutable est plutôt un processus en plusieurs étapes divisé en deux composants: la compilation et la liaison. En réalité, même si un programme "compile bien", il risque de ne pas fonctionner correctement en raison d'erreurs commises pendant la phase de liaison. L'ensemble du processus consistant à passer des fichiers de code source à un exécutable pourrait plutôt être qualifié de génération.
Compilation
La compilation fait référence au traitement des fichiers de code source (.c, .cc ou .cpp) et à la création d'un fichier 'objet'. Cette étape ne crée rien que l'utilisateur puisse réellement exécuter. Au lieu de cela, le compilateur produit simplement les instructions en langage machine correspondant au fichier de code source compilé. Par exemple, si vous compilez (mais ne liez pas) trois fichiers distincts, vous aurez trois fichiers objets créés en sortie, chacun portant le nom .o ou .obj (l'extension dépend de votre compilateur). Chacun de ces fichiers contient une traduction de votre fichier de code source dans un fichier de langage machine - mais vous ne pouvez pas les exécuter pour le moment! Vous devez les transformer en exécutables que votre système d'exploitation peut utiliser. C'est là que l'éditeur de liens entre en jeu.
Mise en relation
La liaison fait référence à la création d'un seul fichier exécutable à partir de plusieurs fichiers objet. Dans cette étape, il est courant que l'éditeur de liens se plaint de fonctions non définies (généralement, main elle-même). Lors de la compilation, si le compilateur ne parvenait pas à trouver la définition d'une fonction particulière, il supposerait simplement que la fonction était définie dans un autre fichier. Si ce n'est pas le cas, le compilateur ne le saura pas - il n'examinera pas le contenu de plus d'un fichier à la fois. L'éditeur de liens, en revanche, peut examiner plusieurs fichiers et essayer de trouver des références pour les fonctions non mentionnées.
Vous pourriez vous demander pourquoi il existe des étapes distinctes de compilation et de création de liens. Premièrement, il est probablement plus facile de mettre en œuvre les choses de cette façon. Le compilateur fait ce qu'il fait, et l'éditeur de liens le fait: en gardant les fonctions séparées, la complexité du programme est réduite. Un autre avantage (plus évident) est que cela permet la création de gros programmes sans avoir à refaire l'étape de compilation chaque fois qu'un fichier est modifié. Au lieu de cela, en utilisant ce qu'on appelle la "compilation conditionnelle", il est nécessaire de ne compiler que les fichiers source qui ont été modifiés; pour le reste, les fichiers objets constituent une entrée suffisante pour l'éditeur de liens. Enfin, cela simplifie la mise en œuvre de bibliothèques de code précompilées: il suffit de créer des fichiers objet et de les lier comme tout autre fichier objet. (Le fait que chaque fichier soit compilé séparément des informations contenues dans d'autres fichiers est d'ailleurs appelé "modèle de compilation distinct".)
Pour tirer pleinement parti de la compilation de conditions, il est probablement plus facile de faire appel à un programme que d’essayer de vous rappeler les fichiers que vous avez modifiés depuis la dernière compilation. (Vous pouvez bien sûr simplement recompiler chaque fichier dont l'horodatage est supérieur à celui du fichier objet correspondant.) Si vous travaillez avec un environnement de développement intégré (IDE), il s'en chargera peut-être déjà. Si vous utilisez des outils de ligne de commande, il existe un utilitaire astucieux appelé make, fourni avec la plupart des distributions * nix. Outre la compilation conditionnelle, il comporte plusieurs autres fonctionnalités Nice pour la programmation, telles que permettre différentes compilations de votre programme - par exemple, si vous avez une version produisant une sortie détaillée pour le débogage.
Connaître la différence entre la phase de compilation et la phase de liaison peut faciliter la recherche de bogues. Les erreurs de compilation sont généralement de nature syntaxique - un point-virgule manquant, une parenthèse supplémentaire. Les erreurs de liaison concernent généralement des définitions manquantes ou multiples. Si vous obtenez une erreur indiquant qu'une fonction ou une variable est définie plusieurs fois par l'éditeur de liens, c'est une bonne indication que l'erreur est que deux de vos fichiers de code source ont la même fonction ou la même variable.
Sur le front standard:
a nité de traduction est la combinaison d'un fichier source, d'en-têtes inclus et de fichiers source moins les lignes source ignorées par la directive de préprocesseur d'inclusion conditionnelle.
la norme définit 9 phases dans la traduction. Les quatre premiers correspondent au prétraitement, les trois suivants sont la compilation, le suivant est l'instanciation de modèles (produisant nités d'instanciation) et le dernier est la liaison.
En pratique, la huitième phase (l'instanciation des modèles) est souvent effectuée pendant le processus de compilation, mais certains compilateurs le retardent jusqu'à la phase de liaison et d'autres l'étalent dans les deux.
Le problème, c’est qu’une CPU charge les données à partir d’adresses de mémoire, les stocke dans des adresses de mémoire et exécute des instructions séquentiellement en dehors des adresses de mémoire, avec certains sauts conditionnels dans la séquence d’instructions traitées. Chacune de ces trois catégories d'instructions implique le calcul d'une adresse dans une cellule de mémoire à utiliser dans l'instruction machine. Étant donné que les instructions machine ont une longueur variable en fonction de l’instruction concernée, et que nous en enchaînons une longueur variable lors de la construction de notre code machine, le calcul et la construction d’adresses comportent deux étapes.
Nous définissons d’abord l’allocation de mémoire du mieux possible avant de savoir ce qui se passe exactement dans chaque cellule. Nous déterminons les octets, les mots ou tout ce qui constitue les instructions, les littéraux et les données. Nous commençons simplement à allouer de la mémoire et à construire les valeurs qui créeront le programme au fur et à mesure, et notons tout endroit où nous devons retourner et corriger une adresse. À cet endroit, nous plaçons un mannequin dans le tampon pour pouvoir continuer à calculer la taille de la mémoire. Par exemple, notre premier code machine peut prendre une cellule. Le code machine suivant peut prendre 3 cellules, impliquant une cellule de code machine et deux cellules d’adresse. Maintenant, notre pointeur d'adresse est 4. Nous savons ce qui se passe dans la cellule de la machine, c'est-à-dire le code opération, mais nous devons attendre pour calculer ce qui se passe dans les cellules d'adresse jusqu'à ce que nous sachions où ces données seront localisées, c'est-à-dire quelle sera la adresse machine de ces données.
S'il n'y avait qu'un seul fichier source, un compilateur pourrait théoriquement produire du code machine entièrement exécutable sans éditeur de liens. Dans un processus en deux passes, il pourrait calculer toutes les adresses réelles de toutes les cellules de données référencées par toute instruction de chargement ou de stockage de la machine. Et il pourrait calculer toutes les adresses absolues référencées par les instructions de saut absolu. C'est ainsi que fonctionnent des compilateurs simples, comme celui de Forth, sans éditeur de liens.
Un éditeur de liens est quelque chose qui permet à des blocs de code d'être compilés séparément. Cela peut accélérer le processus général du code de construction et permettre une certaine flexibilité quant à l'utilisation ultérieure des blocs. En d'autres termes, ils peuvent être déplacés en mémoire, par exemple en ajoutant 1 000 à chaque adresse pour augmenter le nombre de cellules d'adresse de 1 000.
Ainsi, le compilateur génère un code machine approximatif qui n’est pas encore complètement construit, mais qui est structuré de manière à connaître la taille de tout, c’est-à-dire à commencer à calculer l’emplacement de toutes les adresses absolues. le compilateur génère également une liste de symboles qui sont des paires nom/adresse. Les symboles associent un nom à un décalage de mémoire dans le code machine du module. Le décalage étant la distance absolue à l'emplacement mémoire du symbole dans le module.
C'est là que nous arrivons à l'éditeur de liens. L’éditeur de liens claque d’abord tous ces blocs de code machine et note où chacun commence. Ensuite, il calcule les adresses à fixer en additionnant le décalage relatif dans un module et la position absolue du module dans la plus grande mise en page.
Évidemment, j'ai trop simplifié cela pour que vous puissiez essayer de le comprendre, et je n'ai délibérément pas utilisé le jargon des fichiers objets, des tables de symboles, etc., ce qui pour moi fait partie de la confusion.
Regardez l'URL: http://faculty.cs.niu.edu/~mcmahon/CS241/Notes/compile.html
Le processus complet complet de C++ est présenté clairement dans cette URL.
GCC compile un programme C/C++ en fichier exécutable en 4 étapes.
Par exemple, un "gcc -o hello.exe hello.c
" est exécuté comme suit:
1. Pré-traitement
Prétraitement via le préprocesseur GNU C (cpp.exe), qui inclut les en-têtes (#include) et développe les macros (#define).
cpp hello.c> hello.i
Le fichier intermédiaire résultant "hello.i" contient le code source développé.
2. Compilation
Le compilateur compile le code source pré-traité en code d'assemblage pour un processeur spécifique.
gcc -S hello.i
L'option -S spécifie la production d'un code d'assemblage au lieu d'un code d'objet. Le fichier d'assemblage résultant est "hello.s".
. Assemblage
L'assembleur (as.exe) convertit le code d'assemblage en code machine dans le fichier objet "hello.o".
sous la forme -o hello.o hello.s
4. Linker
Enfin, l'éditeur de liens (ld.exe) lie le code de l'objet au code de la bibliothèque pour générer un fichier exécutable "hello.exe".
ld -o hello.exe hello.o ... bibliothèques ...