Chapitre 7. Notions de compilation et configuration du noyau

Table des matières
7.1. Notions de base
7.2. Compilation de GCC
7.3. Compilation du noyau Linux

Ce chapitre présente les notions de base de compilation et de fichiers sources de programmes. Il est certain que le lecteur de ce document n'a pas forcément l'intention de programmer sous Linux, cependant, il est nécessaire de connaître les notions qui vont être décrites ici. En effet, il n'est pas rare, voire il est même courant, d'avoir à recompiler une application lorsqu'on travaille avec Linux. Cela n'est pas étonnant, quand on sait que toute bonne installation passe par la recompilation du noyau de Linux ! La raison de cet état de fait provient sans nul doute du fait que la licence GNU impose de fournir les fichiers sources aux utilisateurs d'une part, et que le langage C et Unix sont historiquement fortement liés.

Nous allons commencer par donner la définition des notions de base et les étapes intervenant dans le processus de génération des fichiers binaires des programmes. Nous verrons ensuite comment installer (et compiler !) la suite de compilateurs GCC du projet GNU, et nous présenterons les étapes intervenant dans la compilation et l'installation d'un nouveau noyau. Ces informations pourront être utiles pour la lecture du chapitre traitant de la configuration du matériel puisque, bien souvent, la prise en charge d'un périphérique nécessite la compilation de ses pilotes au niveau du noyau.

7.1. Notions de base

7.1.1. Définition des termes

Le langage naturel est le langage que les êtres humains utilisent pour communiquer, soit oralement, soit par écrit. Le langage naturel est très riche : il utilise des constructions grammaticales et syntaxiques complexes, et il dispose d'un vocabulaire très étendu. Il permet donc d'exprimer la plupart des idées humaines, et c'est bien sa fonction. En revanche, il n'est pas rigoureux, dans le sens où il laisse la porte ouverte à un grand nombre d'ambiguïtés. Ce qui n'est pas dit est souvent sous-entendu, et c'est donc pratiquement entre les mots que se trouve le sens du discours. Il n'est ainsi pas rare de pouvoir interpréter une même phrase différemment, selon le contexte socioculturel, géographique ou temporel dans lequel elle est dite. Les jeux de mots utilisent cette caractéristique à merveille. La conséquence est que l'on ne peut pas utiliser facilement le langage naturel pour décrire rigoureusement, voire mathématiquement, quelque chose.

Un langage formel est, au contraire, un langage restreint, qui ne dispose que de très peu de constructions syntaxiques et d'un vocabulaire très limité. La caractéristique principale des langages formels est qu'ils sont rigoureux, une expression ou une phrase donnée peut être validée selon les règles de syntaxe qui doivent être respectées. Tout texte écrit dans un langage formel est donc soit valide, soit invalide.

En définissant une association entre les constructions d'un langage formel et un jeu de concepts limité, il est possible de donner une sémantique à un langage formel. Cette sémantique donne la possibilité de qualifier de « vraie » ou de « fausse » les assertions écrites dans le langage formel (on notera qu'une expression peut être valide du point de vue de la syntaxe mais sémantiquement fausse).

Un langage formel permet donc d'exprimer avec précision, sans aucune ambiguïté possible, une idée basée sur les concepts qu'il utilise. Les notations mathématiques constituent un langage formel par excellence.

Note : Par exemple, l'expression mathématique « x=1 » est toujours valide, c'est une simple équation. En revanche, elle n'est pas toujours vraie, cela dépend de la valeur de x. En particulier, si x représente 3, on a « 3 = 1 », ce qui est valide, mais faux. Notez bien la différence.

Un langage de programmation est un langage formel qui permet de définir les tâches qu'un ordinateur doit effectuer, et de décrire les objets informatiques sur lesquels il doit travailler. Un langage de programmation est donc un code, et tout programme doit être écrit dans un tel langage. Pratiquement, les langages de programmation sont des langages très simples, disposant de constructions du type « si ... alors ... » ou « pour chaque ... fait ... ». Les programmes étant écrits avec de tels langages, il est clair qu'un programme ne fait que ce qui a été écrit : ni plus, ni moins. Il faut donc tout dire à l'ordinateur quand on écrit un programme, ce qui en général est relativement fatigant et compliqué. C'est le prix à payer pour la rigueur de l'informatique : l'ordinateur ne se trompe jamais, parce qu'il ne fait qu'obéir aux ordres donnés dans un langage formel (donc précis). Celui qui se trompe, c'est en général le programmeur, et assez couramment l'utilisateur.

Note : Notez qu'un programme est un texte valide s'il vérifie la grammaire du langage formel dans lequel il est écrit. Cela ne l'empêchera pas de faire n'importe quoi si on l'exécute. Un programme « vrai » est donc un programme syntaxiquement correctement écrit et qui en plus fait ce que l'on désire qu'il fasse. Il n'y en a pas beaucoup... surtout que dans bien des cas, on ne s'est jamais posé clairement la question de savoir ce que doivent faire les programmes que l'on écrit !

Le C est un langage de programmation relativement simple, qui se trouve sur tous les types d'ordinateurs et de systèmes d'exploitation. Les programmes sont plus difficile à écrire en C que dans d'autres langages de programmation, mais ils sont plus performants. Le C est très utilisé pour écrire les systèmes d'exploitation : le noyau Linux lui-même est écrit en C. Le C++ est un langage plus évolué, qui est dérivé du C. Il est beaucoup plus riche, et il permet d'écrire des programmes plus compliqués et de les faire évoluer plus facilement. Bien que plus lents que ceux écrits en C, les programmes écrits en C++ sont toujours très performants par rapport à ceux écrits dans d'autres langages.

Les programmes sont en général écrits dans un certain nombre de fichiers. Ces fichiers sont appelés les fichiers sources, du fait qu'ils sont à l'origine du programme. Le texte de ces fichiers est appelé le code source, ou plus simplement le code.

Il va de soi que le code source est écrit dans un langage de programmation, qui n'est pas compréhensible tel quel par le matériel de l'ordinateur. Pour exécuter un programme à partir de son code source, il n'y a que deux solutions :

  • disposer d'un autre programme, nommé interpréteur, capable de lire le code source et d'effectuer les opérations décrites dans le code source ;

  • disposer d'un autre programme, nommé compilateur, capable de traduire le code source en langage binaire, qui sera alors directement exécutable par l'ordinateur.

Les programmes interprétés sont fatalement relativement lents, puisque l'interpréteur doit analyser en permanence les fichiers sources pour le traduire en opération à exécuter à la volée.

Figure 7-1. Interprétation d'un programme

En revanche, les programmes compilés sont beaucoup plus rapides à l'exécution, puisque la phase d'analyse du code source a été réalisée au préalable et se fait en dehors de la phase d'exécution. Ces programmes peuvent donc s'exécuter nativement, sans être ralenti par l'interpréteur.

Figure 7-2. Compilation et exécution d'un programme

Note : Si vous avez bien suivi, on se trouve face au problème de l'œuf et de la poule. En effet, les interpréteurs et les compilateurs sont eux-mêmes des programmes écrits dans un langage informatique. Quel est donc le premier compilateur ou interpréteur ? Ce problème a effectivement dû être résolu au début de l'informatique, de la manière la plus simple : les premiers programmeurs entraient directement le langage machine en binaire dans les ordinateurs (via les câblages ou les cartes perforées...). Quand on sait le travail que cela représentait, et le nombre d'erreurs que cela a pu générer, on comprend facilement pourquoi les compilateurs et les interpréteurs font partie des tous premiers programmes qui ont été développés...

Dans les systèmes Unix, les deux types de programmes existent. Les programmes interprétés constituent ce que l'on appelle des scripts. Le système utilise le shell pour les interpréter. La plupart des opérations d'administration du système utilisent des scripts, car il est toujours plus facile d'écrire et de tester un script que d'écrire un programme en C. Les programmes compilés sont notamment le noyau lui-même, le shell, les commandes de base et les applications. Ce sont eux qui en fin de compte effectuent le travail, et ils sont souvent appelés à partir de scripts. Nous avons déjà vu des exemples de scripts lors de la configuration du système. Pour l'instant, nous allons nous intéresser au langage C et au processus de compilation.

La compilation est l'opération qui consiste à lire un fichier source et à le traduire dans le langage binaire du processeur. Ce langage est absolument incompréhensible par les êtres humains, cependant, il existe un langage de programmation qui permet de coder directement le binaire : il s'agit de l'assembleur.

En général, le processus de compilation génère un fichier binaire pour chaque fichier source. Ces fichiers binaires sont nommés fichiers objets, et porte de ce fait l'extension « .o » (ou « .obj » dans les systèmes Microsoft). Comme un programme peut être constitué de plusieurs fichiers sources, il faut regrouper les fichiers objets pour générer le fichier exécutable du programme, fichier que l'on appelle également le fichier image en raison du fait que c'est le contenu de ce fichier qui sera chargé en mémoire par le système pour l'exécuter et qu'il contient donc l'image sur disque des instructions des programmes en cours d'exécution. L'opération de regroupement des fichiers objets pour constituer le fichier image s'appelle l'édition de liens, et elle est réalisée par un programme nommé le linker (éditeur de liens en français). Ce programme regarde dans tous les fichiers objets les références partielles aux autres fichiers objets et, pour chaque « lien » ainsi trouvé, il complète les informations nécessaires pour en faire une référence complète. Par exemple, un fichier source peut très bien utiliser une fonctionnalité d'un autre fichier source. Comme cette fonctionnalité n'est pas définie dans le fichier source courant, une référence partielle est créée dans le fichier objet lors de sa compilation, mais il faudra tout de même la terminer en indiquant exactement comment accéder à la fonctionnalité externe. C'est le travail de l'éditeur de liens, lorsqu'il regroupe les deux fichiers objets.

Certains fichiers objets sont nécessaires pour tous les programmes. Ce sont notamment les fichiers objets définissant les fonctions de base, et les fonctions permettant d'accéder au système d'exploitation. Ces fichiers objets ont donc été regroupés dans des bibliothèques (également couramment appelées « librairies »), que l'on peut ainsi utiliser directement lors de la phase d'édition de liens. Les fichiers objets nécessaires sont alors lus dans la bibliothèque et ajoutés au programme en cours d'édition de liens. Les bibliothèques portent souvent l'extension « .a » (ou « .lib » dans les systèmes Microsoft).

Figure 7-3. Processus de génération d'un exécutable

Malheureusement, la solution consistant à stocker dans des bibliothèques les fonctions les plus utilisées souffre de la duplication du code contenu dans ces bibliothèques dans tous les programmes, d'où une perte de place considérable. De plus, la mise à jour d'une bibliothèque nécessite de refaire l'édition de liens de tous les programmes qui l'utilisent, ce qui n'est pas réalisable en pratique. C'est pour cela que les bibliothèques dynamiques ont été crées : une bibliothèque dynamique n'est pas incluse dans les fichiers des exécutables qui l'utilisent, mais reste dans un fichier séparé. Les bibliothèques sont regroupés dans un répertoire bien défini du système de fichiers, ce qui permet de les partager entre différents programmes. Ainsi, la mise à jour d'une bibliothèque peut se faire sans avoir à toucher tous les programmes qui l'utilisent. Le problème est cependant que l'édition de liens reste incomplète, parce que les références aux objets des bibliothèques dynamiques sont toujours externes. Il existe donc un programme spécial, l'éditeur de liens dynamiques (« ld », pour « Link Dynamically »), qui résout les dernières références incomplètes lors du chargement de chaque programme. Les bibliothèques dynamiques portent l'extension « .so » (pour « Shared Object »), ou « .dll » dans les systèmes Microsoft (pour « Dynamic Link Library »).

Évidemment, le chargement des programmes est un peu plus lent avec les bibliothèques dynamiques, puisqu'il faut réaliser l'édition de liens dynamiques lors de leur lancement. Cependant, ce processus a été optimisé, et les formats de fichiers binaires utilisés contiennent toutes les informations précalculées pour faciliter la tâche de l'éditeur de liens dynamiques. Ainsi, la différence de performance est devenue à peine décelable.

7.1.2. Processus de génération

Classiquement, les fichiers sources C et C++ se divisent en deux grande catégories :

  • les fichiers d'en-tête, qui contiennent les déclarations des symboles du programme ;

  • les fichiers C (ou C++), qui contiennent leurs définitions.

Le fait de faire cette distinction entre la déclaration (qui consiste à dire : « telle chose existe ») et la définition (qui consiste à décrire la chose précédemment déclarée) permet de faire en sorte que l'on peut utiliser les fichiers objets sans avoir les fichiers sources. En effet, la déclaration permet de réaliser les références partielles dans les programmes, tandis que les fichiers objets contiennent bien entendu la définition binaire de ce qui a été déclaré.

Pour compiler un programme, on n'a donc réellement besoin que de trois types de fichiers :

  • les fichiers de déclaration du système et des bibliothèques de base ;

  • les fichiers des bibliothèques de base eux-mêmes ;

  • les fichiers source (de déclaration et de définition) du programme à compiler.

En C et C++, les fichiers sources de déclaration portent l'extension « .h » (plus rarement « .hpp » pour les fichiers de déclaration C++), et les fichiers de définition portent l'extension « .c » pour les programmes écrits en C et « .C » (ou « .cpp », ou encore « .c++ ») pour les programmes écrits en C++.

Il va de soi que la réalisation d'un programme peut nécessiter la création d'un grand nombre de fichiers, tous plus ou moins dépendants les uns des autres. Pour pouvoir s'y retrouver plus facilement, les programmeurs utilisent un programme spécial : make. Ce programme est capable de réaliser toutes les opérations nécessaires à la création des fichiers exécutables d'un programme à partir de ses fichiers sources. Pour cela, il utilise un fichier contenant la définition des opérations à réaliser, ainsi que la description des dépendances entre les différents fichiers sources. Ce fichier est appelé le fichier makefile. Grâce à make, l'utilisateur de base que vous êtes va pouvoir compiler la plupart des programmes sans avoir la moindre notion de programmation.

En général, la compilation d'un programme passe par les étapes suivantes :

  • récupération des fichiers sources du programme ;

  • configuration du programme pour l'environnement courant ;

  • appel à make pour la compilation ;

  • appel à make pour l'installation.

La première étape est élémentaire et va de soi.

La deuxième étape se fait souvent en appelant un script dans le répertoire d'installation des fichiers sources. Ce script se nomme souvent configure, et peut être appelé avec la ligne de commande suivante :

./configure
à partir du répertoire où se trouvent les fichiers sources. Ce script effectue tous les tests sur le système et l'environnement de développement, et génère le fichier makefile. Le programme configure est en général fourni avec les logiciels GNU. Il permet de déterminer l'environnement logiciel et système et de générer le fichier makefile et des fichiers d'en-têtes contenant la définition de quelques options du programme. Dans quelques cas particuliers, vous aurez à utiliser des options de configure afin de préciser certains paramètres. L'option --host permet d'indiquer le type de la machine cible, dans le cas où configure ne parvient pas à le déterminer automatiquement :

--host=machine

machine est la description de votre machine. Pour les PC fonctionnant sous Linux, cette description est de la forme « ix86-pc-linux-gnu », où le 'x' représente le numéro de la génération du processeur du PC. Par exemple, pour un Pentium ou un AMD K6, la description sera « i586-pc-linux-gnu ». L'option --enable-shared permet de générer des bibliothèques dynamiques lors de la compilation. Cela procure un gain de place évident. Enfin, l'option --prefix permet de préciser le répertoire de base dans lequel le programme sera installé a priori. Cette option s'utilise de la manière suivante :

--prefix=répertoire

répertoire est le répertoire de base de l'installation. La connaissance de ce répertoire est utile aux autres programmes pour localiser les composants qui vont être installés. Dans la plupart des cas, la valeur par défaut du préfixe convient, mais il est parfois nécessaire de le modifier. C'est en particulier le cas lorsque vous désirez mettre à jour un programme déjà installé et que votre distribution n'utilise pas le répertoire par défaut. Dans ce cas, il convient d'indiquer le répertoire de base dans lequel ce programme a été installé, afin de le remplacer au lieu de le dupliquer d'une part, et afin que les autres programmes puissent trouver la nouvelle version à la place de l'ancienne d'autre part.

La troisième étape est très simple aussi. Il suffit de taper la commande suivante :

make
toujours dans le répertoire où se trouvent les fichiers sources.

Enfin, la dernière étape se fait en tapant la commande suivante :

make install

Bien entendu, ces différentes étapes varient parfois avec le logiciel à installer. Cependant, il existe quasiment toujours un fichier texte indiquant comment effectuer ces opérations dans le répertoire des fichiers sources. Ce fichier porte souvent le nom de « readme », « install », ou quelque chose de similaire.

Avec les notions que vous venez de voir, vous devriez pouvoir compiler quasiment tous les programmes que vous pourrez récupérer sous la forme de fichiers sources. Un dernier détail cependant : la compilation est une opération très gourmande en ressources. Cela signifie qu'elle peut consommer beaucoup de ressources processeur, de mémoire, de disque et de temps. Pour des gros programmes, il n'est pas rare de passer jusqu'à une heure de compilation, même sur une machine récente. Notez également que les facteurs limitants dans les compilations sont souvent la rapidité du processeur et du disque dur, ainsi que la quantité de mémoire vive disponible.