4.2 Création de programmes C
Objectifs d'apprentissage
À la fin de cette section, vous serez capable de :
- Écrire du code C en utilisant les éléments fondamentaux du langage
- Résumer les étapes pour développer un programme C
- Comprendre le processus de compilation et d'exécution d'un programme C
- Décrire comment la liaison est utilisée dans un programme C
- Comprendre comment appliquer la gestion du contrôle de version
Comme discuté, un langage de programmation (programming language) est un type de modèle computationnel (computational model) utilisé pour écrire des programmes. Le C (C) est un langage de niveau intermédiaire populaire, largement utilisé pour créer des logiciels système (systems software). Cette section est un cours intensif sur les éléments essentiels du C.
Introduction au C
Le langage de programmation C a été inventé en 1972 par Dennis Ritchie des Bell Labs (Figure 4.15) et popularisé par le livre The C Programming Language de Brian Kernighan et Dennis Ritchie. Le nom particulier du C — une seule lettre — était un choix pragmatique, car le C a remplacé un langage antérieur nommé B. Le C est un langage procédural de niveau intermédiaire qui donne aux programmes un accès de bas niveau à la mémoire. C'est un langage relativement simple, ce qui rend l'apprentissage du C et la création d'un compilateur (compiler) C plus faciles que pour des langages plus complexes. Cette combinaison de fonctionnalités a fait du C un succès instantané, et il a maintenu une grande popularité et importance jusqu'à ce jour. Le C a également influencé d'autres langages de programmation. Le C++ (C++) est un langage plus récent qui ajoute le paradigme orienté objet au C.
Pourquoi le C est-il si populaire ? Principalement parce que ses concepteurs ont réussi à trouver un équilibre entre les fonctionnalités de bas niveau et de niveau intermédiaire qui permet au code C de s'exécuter pratiquement à la même vitesse que le langage assembleur (assembly language), tout en permettant aux programmeurs d'être suffisamment productifs pour créer de grands programmes fiables. Le C est le langage de programmation derrière une grande partie des logiciels de bas niveau dont nous dépendons, y compris les systèmes d'exploitation (operating system), les compilateurs (compiler) de langage, les assembleurs (assembler), les éditeurs de texte, les serveurs d'impression, les pilotes réseau, les interpréteurs de langage et les utilitaires de ligne de commande. Voici quelques produits logiciels spécifiques écrits en C :
- La machine virtuelle Java
- Linux (Linux), un système d'exploitation open-source
- Python (C)
- Noyau macOS X (C)
- Windows (C, C++)
- La base de données Oracle (C, C++)
- Routeurs Cisco (C)
Pleins feux sur l'industrie
Applications du C
Le C est utilisé dans une variété d'industries. Un exemple est l'astrophysique, où les scientifiques écrivent des programmes qui simulent le mouvement des corps stellaires et contrôlent des instruments tels que les télescopes. En raison de la grande taille de l'univers, ces simulations impliquent l'exécution de calculs sur de très grands tableaux de nombres. La capacité du C à s'exécuter rapidement et à contrôler la disposition des grands tableaux en mémoire est avantageuse pour cette application. En tant que langage relativement simple, le C est accessible aux physiciens qui ne sont pas nécessairement experts en informatique. Les expériences scientifiques doivent être reproductibles, ce qui signifie que le code impliqué dans la science doit fonctionner même des décennies plus tard. Le fait que le C soit un langage stable et populaire depuis si longtemps signifie qu'il est très probable qu'il perdure, ce qui ne peut pas être dit des langages de niche plus récents.
Une caractéristique notable du C est la façon dont il gère la mémoire. Dans un programme, nous avons des variables et des valeurs. Par exemple, dans x = 10, x est une variable et 10 est la valeur. Chaque valeur dans un programme est stockée en mémoire. Les régions de mémoire sont divisées en quatre blocs : pile, tas, statique et blocs de code. Ces régions stockent diverses parties d'un programme en cours d'exécution. Les programmes en cours d'exécution créent et détruisent des valeurs extrêmement rapidement (peut-être des millions ou des milliards par seconde), et la mémoire est finie, de sorte que les emplacements de mémoire doivent être réutilisés, sinon ils s'épuiseraient rapidement. Lorsqu'une valeur est créée, de la mémoire est réservée en tant que mémoire allouée (allocated memory) pour contenir cette valeur. Finalement, lorsque la valeur n'est plus nécessaire, cette mémoire devient de la mémoire libérée (freed memory), ce qui signifie qu'elle est rendue afin de pouvoir être réutilisée. Le processus d'allocation et de libération de mémoire est appelé gestion de la mémoire (memory management). Une fuite de mémoire (memory leak) se produit lorsqu'une partie de la mémoire est allouée mais jamais libérée. Une fuite de mémoire est un bug qui fait gaspiller des ressources à un programme ; des fuites graves peuvent gaspiller toute la mémoire de l'ordinateur, le rendant insensible ou le faisant planter. En tant que langage de programmation de niveau intermédiaire (middle-level programming language), le C exige des programmeurs qu'ils gèrent manuellement la mémoire. Ce type de flexibilité doit être utilisé avec prudence car il peut entraîner la création de programmes peu fiables et peu sûrs. Dans les langages de haut niveau, la gestion de la mémoire est automatisée.
Voici d'autres caractéristiques notables du C :
- Exécution efficace : Le C a une puissance expressive inférieure à celle de certains autres langages de niveau intermédiaire comme le C++ (C++) et pourtant suffisamment simple pour que les compilateurs puissent générer du code machine (machine code) comparable en vitesse au code assembleur écrit à la main. De nombreuses recherches et développements se sont concentrés sur la création de compilateurs C axés sur les performances.
- Portabilité : Le C peut s'exécuter dans plusieurs environnements informatiques, ce qui est également connu sous le nom de propriété de portabilité. Unix a été conçu pour fonctionner sur diverses architectures matérielles, de sorte que le langage C n'est pas dépendant du matériel. Le même code C peut être compilé et exécuté sur différentes architectures matérielles et systèmes d'exploitation.
- Modularité (Modularity) : La programmation modulaire fait référence au processus de division des programmes informatiques en sous-programmes distincts. Un module est un composant logiciel distinct, tel qu'un gestionnaire d'erreurs, qui peut être utilisé par une variété d'applications et de fonctions au sein d'un système. Le C prend en charge la modularité.
- Prise en charge de la programmation procédurale et structurée (structured programming) : Le C adhère aux paradigmes procéduraux et structurés.
- Types de données et opérateurs (operator) : Chaque variable dans un programme C a un type de données. Les types de données dictent la quantité de mémoire utilisée pour stocker la variable et les types d'opérateurs qui peuvent être utilisés avec la variable.
- Prise en charge de la récursion : La récursion (Recursion) est le phénomène d'un système étant défini en termes de lui-même. Dans le code, cela signifie qu'une fonction (function) peut s'appeler encore et encore. Le C prend en charge la récursion. Cependant, il ne fournit pas de fonctionnalité appelée « appel terminal » qui rend la récursion efficace, de sorte que la récursion n'est pas autant utilisée en C que dans les langages qui fournissent l'appel terminal. Un appel terminal est un appel de fonction effectué comme action finale d'une fonction. Si la fonction cible d'un appel terminal est la même fonction, la fonction est dite récursive terminale, ce qui est un cas particulier de récursion. La récursion terminale (également appelée « récursion de fin de queue ») est utile et contribue aux optimisations de code.
- Pointeurs : Un pointeur (pointer) est une variable qui contient l'adresse mémoire d'une autre variable et pointe vers cette variable (Figure 4.16). Les pointeurs jouent un rôle crucial dans le langage C. Ils sont utilisés pour stocker et gérer les adresses de blocs alloués dynamiquement en mémoire dans le système informatique sous-jacent. La gestion des périphériques matériels implique la manipulation de certains emplacements de mémoire, et la prise en charge des pointeurs par le C est l'une des raisons pour lesquelles il est utilisé pour implémenter les noyaux (kernel) et les pilotes de périphérique (device driver).
La technologie au quotidien
L'application du C aux débuts de YouTube
Le C a une variété de types entiers, tels que short, int et long. Un programmeur C (C) doit choisir le type le plus approprié pour chaque information de son programme. Les types plus petits utilisent moins de mémoire mais ne peuvent stocker qu'une plage de valeurs plus étroite. Sur un ordinateur typique, la valeur maximale de short est d'environ 32 mille et la valeur maximale de int est d'environ 2 milliards. Une bonne pratique consiste à réfléchir de manière critique à la taille que pourrait atteindre une valeur particulière et à choisir le type de données le plus petit qui s'adapte à cette plage.
Les programmeurs de YouTube ont été confrontés à ce problème lorsqu'ils ont implémenté le compteur de vues pour les vidéos YouTube. Ils ont dû réfléchir : quel est le nombre maximal de vues qu'une vidéo est susceptible d'obtenir ? Deux milliards semblaient un choix sûr, ils ont donc choisi int.
Cette décision s'est avérée malavisée. En 2014, le clip musical viral « Gangnam Style » de l'artiste coréen Psy a accumulé plus de deux milliards de vues, et le compteur de vues est tombé en panne. La variable int stockant le nombre de vues de « Gangnam Style » a débordé et est revenue à un nombre négatif. Cela s'est avéré être un embarras pour YouTube, qui a dû rapidement modifier son code pour utiliser long à la place.
Quel est le type de données entier le plus approprié (short, int ou long) pour les quantités suivantes ?
- Le nombre de personnes dans un avion
- Le nombre de personnes sur Terre
- Le nombre de personnes dans un foyer
- Le nombre de dollars sur un compte bancaire
Les langages de haut niveau vérifient généralement les indices de tableau à l'exécution, ce qui facilite l'identification et la correction des bugs hors limites, mais ralentit légèrement les indices de tableau. En tant que langage de niveau intermédiaire, le C ne vérifie pas les indices de tableau. Un tableau est un espace de stockage où les éléments sont stockés dans des cellules de mémoire contiguës. Ils sont indexés de 0 (la première cellule) à n-1 (la dernière cellule).
En C, un indice de tableau invalide accèdera à la mémoire en dehors de la variable tableau. Si l'indice n'est que légèrement hors limites, cela accèdera à des variables proches, ce qui est un bug subtil qui peut passer inaperçu. Une erreur de segmentation (segmentation fault) (« segfault » en abrégé) se produit si l'indice est très loin des limites. Lorsque cela se produit, il accède à une adresse mémoire interdite au programme, et votre système d'exploitation arrêtera de force le programme en réponse. Ce type d'erreur d'exécution peut être notoirement difficile à corriger. Les indices de tableau hors limites sont une source courante d'erreurs de segmentation.
Chaque valeur dans un programme est stockée à une adresse mémoire spécifique. Un pointeur est une valeur qui contient une adresse mémoire. Techniquement, un pointeur doit contenir l'emplacement d'une valeur de données valide. Cependant, de nombreux emplacements de mémoire ne contiennent pas de valeurs de données valides, il est donc possible d'avoir un pointeur invalide (invalid pointer) qui ne contient pas un emplacement valide. Le « pointee » est la valeur vers laquelle un pointeur pointe. Un pointeur est analogue à une adresse postale telle que « 123 Main Street », car il fait référence à un emplacement spécifique. Dans cette analogie, chaque bâtiment est un « pointee ». Généralement, une adresse est valide et fait référence à un endroit que vous pouvez visiter. Cependant, il est possible d'avoir une adresse invalide qui n'est pas un endroit que l'on peut visiter ; par exemple, si le bâtiment à cet emplacement a été démoli.
L'une des différences entre les langages de niveau intermédiaire et de haut niveau est que les langages de haut niveau interdisent entièrement les pointeurs invalides, ou fournissent des mécanismes pour les gérer en toute sécurité. En tant que langage de niveau intermédiaire, le C donne aux programmeurs la liberté de créer des pointeurs nuls/invalides, ce qui peut être utile lors de l'écriture de code qui interagit avec des périphériques matériels. Étant donné que tous les périphériques matériels ne prennent pas en charge les mêmes fonctionnalités, la prise en charge des fonctionnalités individuelles par un périphérique donné peut être indiquée comme un pointeur nul/non initialisé, ce qui est acceptable tant que le programme vérifie les pointeurs non initialisés pour déterminer si une fonctionnalité donnée est disponible. Cependant, en général, la liberté d'utiliser des pointeurs nuls/invalides s'accompagne de la responsabilité de s'assurer que les pointeurs sont toujours utilisés correctement. Cela s'est avéré difficile ; les pointeurs invalides sont une source courante de bugs dans les programmes C.
En C, le programmeur est responsable de s'assurer que les tableaux de caractères sont réellement suffisamment grands pour contenir des chaînes, et que les chaînes incluent le caractère de terminaison nulle. Un tableau de caractères est une chaîne de caractères parfois terminée par un caractère nul. Un exemple pourrait être quelque chose comme : char *arr= "string\0". Oublier l'un ou l'autre de ces éléments entraîne des bugs. C'est un excellent exemple de la façon dont les langages de niveau intermédiaire tels que le C attendent des programmeurs qu'ils gèrent plus de détails que les langages de haut niveau.
Lien vers l'apprentissage
La bibliothèque standard C contient des dizaines de fichiers d'en-tête et des centaines de fonctions. Il est peu pratique de mémoriser toutes ces informations. Les programmeurs ne mémorisent pas les prototypes (c'est-à-dire le nom et les paramètres) des fonctions de bibliothèque. Au lieu de cela, ils se réfèrent à des documents de référence et développent la capacité à trouver rapidement des informations dans ces documents. Ces documents de référence de la bibliothèque C sont disponibles à de nombreux endroits.
Développement de programmes C
Un programmeur passe un temps considérable à travailler dans son environnement de développement ; en effet, un développeur professionnel pourrait passer la majeure partie de sa journée de travail à l'utiliser. Il est rentable d'investir du temps et de l'attention en amont pour apprendre votre environnement et le personnaliser selon vos besoins afin que votre expérience continue soit fluide et ergonomique. Les chefs, les mécaniciens et autres artisans accordent beaucoup d'attention à la culture d'espaces de travail sûrs et productifs, et de la même manière, les programmeurs expérimentés s'occupent de leur environnement de développement.
Les programmeurs travaillant avec des langages compilés, y compris le C, travaillent généralement en utilisant le cycle illustré à la Figure 4.17.
Plus précisément, ces étapes sont :
- Développement d'algorithme : Le développeur conçoit une compréhension de haut niveau de ce que le code fera et comment il le fera. Il peut documenter l'algorithme avec du pseudocode, un diagramme de blocs ou un croquis. Dans le cas de programmes extrêmement simples, l'algorithme peut être suffisamment trivial pour que le programmeur puisse le garder en tête. Dans certains cas, un programmeur implémente un algorithme que quelqu'un d'autre a créé et décrit dans un ouvrage de référence ou un article de recherche.
- Développement de programme : Le programmeur écrit le code qui implémente les étapes de l'algorithme.
- Traduction de programme : Le programmeur exécute le compilateur (compiler) sur le code. Souvent, le code contient des erreurs de syntaxe, et le compilateur fournit des messages d'erreur décrivant les erreurs. Une erreur de syntaxe est une violation des règles de construction d'instructions valides dans le langage. Par exemple, l'utilisateur peut avoir introduit une faute de frappe, comme un point-virgule manquant, ou l'utilisation d'un mot-clé comme nom de variable. Dans ce cas, le programmeur retourne à l'étape 2 (Développement de programme) pour résoudre les erreurs une par une.
- Exécution de programme : À cette étape, le code n'a pas d'erreurs de syntaxe, il a donc été compilé avec succès en un programme exécutable. Le développeur exécute le programme et teste qu'il fonctionne correctement. Une première ébauche de code contient souvent une erreur sémantique (semantic error), ce qui signifie que le code compile et s'exécute, mais ne se comporte pas comme il le devrait. Lorsqu'un programmeur trouve une erreur sémantique, il retourne à l'étape 2 pour déboguer le code et corriger l'erreur sémantique. Finalement, après des tests approfondis, qui nécessitent une approche spécifique non décrite ici, plus aucune erreur sémantique ne peut être trouvée, et le code est considéré comme terminé.
Certains compilateurs C (C) incluent :
- GCC (GCC), un compilateur C open-source développé par le projet GNU
- Clang (Clang), un compilateur C open-source développé par le projet LLVM
- Visual C++ (Visual C++), un compilateur C et C++ (C++) développé par Microsoft
Selon le système d'exploitation que vous utilisez, il existera de nombreux environnements de développement C alternatifs viables. Un système d'exploitation (operating system) est un programme logiciel complexe qui aide l'utilisateur à contrôler le matériel et à utiliser plusieurs autres applications. Les exemples incluent Windows 10 et 11, et les versions Linux (Linux) telles qu'Ubuntu, Fedora, CentOS. Vous pouvez choisir d'utiliser un environnement de développement intégré (IDE) (integrated development environment (IDE)), qui est un programme avec une interface utilisateur graphique qui comprend un éditeur de texte, un compilateur et d'autres outils, le tout dans une seule application. Par exemple, vous pouvez installer et utiliser Eclipse pour C/C++, un IDE multilingue open-source initialement créé pour la programmation Java. Eclipse est portable car il est construit en Java et peut être installé sur n'importe quel système d'exploitation.
Compilation et exécution de programmes C
Le processus de compilation implique plusieurs étapes :
- compilateur : le langage de haut niveau se convertit en assembleur
- assembleur (assembler) : l'assembleur se convertit en code machine (machine code)
- éditeur de liens (linker) : un programme qui effectue la liaison (linking), un processus de collecte et de combinaison de diverses pièces de code objet (object) en un seul fichier programme qui peut être chargé en mémoire et exécuté
En pratique, les compilateurs tels que GCC regroupent toutes ces étapes en une seule commande. Habituellement, lorsque vous exécutez la commande GCC, GCC compile, assemble et lie un programme.
Pour écrire, compiler et exécuter un programme C simple :
- Écrire le texte du programme (c'est-à-dire le code source (source code)) à l'aide d'un éditeur de texte, et l'enregistrer sous forme de fichier texte (par exemple, « my_program.c »)
- Exécuter le compilateur, l'assembleur et l'éditeur de liens pour convertir votre programme du code source en un « exécutable » ou « binaire ». La compilation est nécessaire pour que chaque programme s'exécute et effectue l'opération souhaitée.
gcc –Wall –g –o my_program my_program.cOptions du compilateur GCC :
-Wallindique au compilateur de générer tous les « avertissements ». Ces avertissements identifient souvent des erreurs.-gindique au compilateur de générer des informations de débogage.- Si vous ne fournissez pas d'option
–opour définir un nom de fichier de sortie, il créera un exécutable appeléa.out. - Un fichier
.cest appelé un « module ». Souvent, les programmes sont composés de plusieurs fichiers.cet de bibliothèques qui sont liés ensemble pendant le processus de compilation.
- Si le compilateur donne des erreurs et des avertissements, modifiez le fichier source, corrigez-le et recompilez. C'est une bonne pratique de travailler sur une seule erreur/avertissement à la fois, à savoir le premier. C'est parce qu'une erreur de syntaxe peut provoquer de fausses alertes plus tard dans le code source, de sorte que les avertissements/erreurs après le premier pourraient être de fausses alertes. Nous recommandons que, lorsque vous obtenez des erreurs ou des avertissements de compilation, vous modifiez pour corriger uniquement le premier, et recompilez ; n'essayez pas de corriger les avertissements/erreurs après le premier.
Considérez le programme C « Hello World » suivant1:
#include <stdio.h> /* inclure le prototype printf */
/* Le programme C le plus simple */
int main(int argc, char **argv) /* point d'entrée du programme principal */
{
printf("Hello World\n");
return 0; /* retourner sans erreur */
}
Pour exécuter un programme dans le répertoire courant, utilisez ./program . (« . » signifie le répertoire courant). Dans le monde des systèmes d'exploitation, tout est défini en termes de répertoires et de fichiers. Même le bureau est un répertoire, qui est une collection de fichiers. Un répertoire peut parfois être vide aussi, et certains répertoires ont des fichiers cachés pour des raisons de sécurité. Un sous-répertoire est un répertoire dans un répertoire.
> ./my_program
Hello World
>
Liaison de programmes
Figure 4.18 illustre les étapes de traitement des programmes C du code source à l'exécution.
La liaison fait référence au processus de collecte et de combinaison de diverses pièces de code objet en un seul fichier programme qui peut être chargé en mémoire et exécuté. Un éditeur de liens est un programme qui effectue la liaison. Comprendre les éditeurs de liens vous aidera à construire de grands programmes, à éviter les erreurs de programmation dangereuses, à comprendre comment les règles de portée de langage sont implémentées, à comprendre d'autres concepts importants du système (tels que la mémoire virtuelle et la pagination), et à utiliser des bibliothèques partagées (un fichier qui doit être partagé par un fichier exécutable). La mémoire virtuelle est un concept de système d'exploitation où la mémoire secondaire agit comme mémoire principale pour compenser le manque de mémoire. La pagination est une technique où la mémoire secondaire est utilisée pour stocker et récupérer les données dans la mémoire principale. La mémoire est divisée en petites régions appelées pages, ce qui permet un accès rapide aux données. Si une page est trouvée, on parle de « succès de page » ; sinon, c'est un « échec de page ».
Les programmes sont traduits et liés à l'aide d'un pilote de compilateur, un programme qui invoque d'autres composants qui aident à traduire le programme de haut niveau en code machine, comme dans la Figure 4.19 et en utilisant le code suivant :
linux> gcc -Og -o prog main.c sum.c
linux> ./prog
Les éditeurs de liens (linker) sont utilisés pour assurer :
- Modularité (Modularity) : Le programme peut être écrit comme une collection de fichiers source plus petits, plutôt qu'une masse monolithique. L'utilisation d'un éditeur de liens facilite la création de bibliothèques de fonctions (function) courantes (par exemple, bibliothèque Math, bibliothèque C (C) standard). Une bibliothèque (library) est un fichier qui contient du code objet (un code du fichier objet généré après la compilation) pour des fonctions et des variables globales (variables qui ont une portée globale et peuvent être utilisées n'importe où dans le programme) destinées à être réutilisées.
- Efficacité : Il est plus rapide d'exécuter des compilations séparées et de modifier un fichier source, de le compiler, puis de le relier, car il n'est pas nécessaire de recompiler d'autres fichiers source. De plus, les bibliothèques économisent de l'espace mémoire car les fonctions courantes peuvent être agrégées dans un seul fichier et pourtant les fichiers exécutables (le produit final après compilation et liaison) et les images mémoire en cours d'exécution (mémoire actuelle) ne contiennent que le code des fonctions qu'ils utilisent réellement.
Étapes de liaison
Les programmes définissent et référencent des symboles. Un symbole (symbol) est un identifiant pour une fonction ou une variable globale. La première étape de liaison effectue la résolution de symboles (symbol resolution). Pendant l'étape de résolution de symboles, l'éditeur de liens associe chaque référence de symbole à exactement une définition de symbole (Figure 4.20).
Les définitions de symboles sont stockées dans un fichier objet (par l'assembleur (assembler)) appelé table de symboles (symbol table). Une table de symboles est un tableau de structures dans lequel chaque entrée comprend le nom, la taille et l'emplacement du symbole.
La deuxième étape de liaison (linking) effectue la relogement de code (code relocation) (Figure 4.21). Cette étape fusionne des sections de code et de données séparées en sections uniques (une pour le code et une pour les données). Elle reloge les symboles de leurs emplacements relatifs dans les fichiers .o (les fichiers objet (object)) vers leurs emplacements mémoire absolus finaux dans l'exécutable. Elle met à jour toutes les références à ces symboles pour refléter leurs nouvelles positions.
Format de module exécutable et liables
Il existe trois types de fichiers objet qui sont liés au processus de liaison (Figure 4.22) :
- Fichier objet relogeable (fichier
.o) : Contient du code et des données sous une forme qui peut être combinée avec d'autres fichiers objet relogeables pour former un fichier objet exécutable. Chaque fichier.oest produit à partir d'exactement un fichier source (.c). - Fichier objet exécutable (fichier
a.out) : Contient du code et des données sous une forme qui peut être copiée directement en mémoire puis exécutée. - Fichier objet partagé (fichier
.so) : Type spécial de fichier objet relogeable qui peut être chargé en mémoire et lié dynamiquement, soit au moment du chargement, soit à l'exécution. Ces fichiers objet sont appelés Dynamic Link Libraries sous Windows.
Les trois fichiers objet suivent le format exécutable et lié (ELF) (executable and linkable format (ELF)), qui est un format binaire standard pour les fichiers objet proposé à l'origine par AT&T System V Unix, puis adopté par les variantes BSD Unix et Linux (Linux). Unix (Unix) est un système d'exploitation (operating system) qui a été largement utilisé, principalement dans les serveurs et le développement logiciel depuis les années 1970, et Linux est compatible avec Unix.
Types de symboles et résolution
Un éditeur de liens (linker) classe les symboles en trois catégories, comme illustré dans la Figure 4.23 et la Figure 4.24. Un symbole peut être le nom d'une variable ou d'une chaîne. Dans d'autres cas, il peut s'agir des noms de fonctions ou de procédures, tels que :
- Symboles globaux : Symboles définis par le module m qui peuvent être référencés par d'autres modules
- Symboles externes : Symboles globaux qui sont référencés par le module m mais définis par un autre module.
- Symboles locaux: Symboles qui sont définis et référencés exclusivement par le module m (par exemple, fonctions C et variables globales définies avec l'attribut statique) ; les symboles de l'éditeur de liens locaux ne sont pas des variables de programme locales (l'éditeur de liens ne traite pas les variables locales d'une fonction). Notez également que les variables C locales non statiques sont stockées sur la pile tandis que les variables C statiques locales sont stockées soit dans .bss, soit dans .data.
Les symboles de programme sont soit forts (par exemple, procédures et globales initialisées) soit faibles (par exemple, globales non initialisées). Un symbole (symbol) fort a un emplacement mémoire unique. Prenons l'exemple de : int array[2] = {1 ,2};. Cela crée une ambiguïté pendant le processus de liaison lorsqu'un autre fichier essaie d'accéder à nouveau au même symbole en raison d'une définition forte. D'autre part, un symbole faible permet plusieurs définitions du même symbole sans créer d'ambiguïté. Cela aide pendant le processus de liaison lorsqu'un autre fichier crée une définition forte du même nom. Dans des langages comme le C (C) et le C++ (C++), le symbole faible est défini à l'aide du mot-clé _attribute_((weak)). Les variables globales doivent être évitées (c'est-à-dire, utilisez static chaque fois que vous le pouvez, initialisez la variable globale, ou utilisez extern si vous référencez une variable globale externe).
L'éditeur de liens (linker) applique les règles suivantes :
- Règle 1 : Plusieurs symboles forts ne sont pas autorisés. Chaque élément ne peut être défini qu'une seule fois, sinon l'éditeur de liens émet une erreur.
- Règle 2 : Étant donné un symbole fort et plusieurs symboles faibles, choisissez le symbole fort (les références au symbole faible se résolvent en symbole fort).
- Règle 3 : S'il existe plusieurs symboles faibles, choisissez-en un arbitraire (peut être annulé avec
gcc –fno-common). « -fno-common » aide à détecter les collisions accidentelles de noms communs.
Bibliothèques statiques
Les fonctions (Function) couramment utilisées par les programmeurs (par exemple, mathématiques, E/S, gestion de la mémoire (memory management), manipulation de chaînes) peuvent être regroupées dans un fichier appelé bibliothèque. Une bibliothèque statique (static library) (ou .a, un fichier d'archive (archive file)) est un type simple de bibliothèque qui copie le contenu des fichiers objet dans un seul fichier appelé une archive. L'éditeur de liens essaie de résoudre les références externes (external references) non résolues en recherchant les symboles dans une ou plusieurs archives. Une référence externe est un symbole qui est utilisé dans un module, mais non défini dans ce module, il est donc censé être défini dans un autre module. Si un fichier membre d'archive résout une référence, l'éditeur de liens le lie à l'exécutable. L'archiveur permet des mises à jour incrémentielles ; il recompile également les fonctions qui ont changé et remplace le fichier .o correspondant dans l'archive (Figure 4.25).
libc.aqui n'inclut que les fichiers objet
atoi.o,
printf.oet
random.o. (attribution : Copyright Rice University, OpenStax, sous licence CC BY 4.0)
Les bibliothèques couramment utilisées incluent libc.a (la bibliothèque standard C), qui gère : les E/S, l'allocation de mémoire, la gestion des signaux, la manipulation de chaînes, les données et l'heure, les nombres aléatoires et les mathématiques entières. Une autre bibliothèque courante est libm.a (la bibliothèque mathématique C) qui gère les mathématiques en virgule flottante (par exemple, sin, cos, tan, log, exp, sqrt).
Figure 4.26 illustre comment lier des programmes avec des bibliothèques statiques.
L'éditeur de liens utilise l'algorithme suivant pour résoudre les références externes :
- Analyser les fichiers
.oet.adans l'ordre de la ligne de commande. - Pendant l'analyse, maintenir une liste des références non résolues actuelles.
- À chaque nouveau fichier
.oou.a, obj, rencontré, essayer de résoudre chaque référence non résolue de la liste par rapport aux symboles définis dans obj. - Si des entrées restent dans la liste non résolue à la fin de l'analyse, émettre une erreur.
Par conséquent, l'ordre de la ligne de commande est important et les bibliothèques doivent être placées à la fin de la ligne de commande pour éviter les erreurs de l'éditeur de liens, comme illustré dans la Figure 4.27.
Concepts en pratique
APIs en C
Le langage C (C) permet de créer une API modulaire en tant que bibliothèque (library) avec des prototypes de fonctions publiquement visibles mais des définitions de fonctions secrètes. Ceci est accompli en distribuant librement les fichiers .h avec les déclarations de fonctions, tout en gardant les fichiers .c secrets et en distribuant uniquement le code compilé .a ou .so (code objet (object)). Un fichier .h est utilisé en C, C++ (C++), où les bibliothèques peuvent être utilisées dans le programme courant au lieu d'écrire le code entièrement.
Un exemple est math.h. Cette stratégie est utilisée dans de nombreuses industries, comme les jeux vidéo. DirectX est une API créée par Microsoft pour les plateformes utilisées sur les PC Windows et Xbox. Microsoft fournit une bibliothèque C avec de nombreux appels de fonctions pour des opérations liées aux jeux, telles que le rendu graphique, la lecture de sons et la lecture des entrées du clavier, de la souris ou du joystick. Un programmeur de jeux écrit son jeu comme un programme C qui appelle ces fonctions. Cet arrangement est un bon compromis — la commodité de l’API DirectX facilite le travail des programmeurs de jeux et les incite à créer des jeux pour Windows et Xbox. Mais garder les fichiers .c propriétaires signifie que Microsoft n’a pas à divulguer le travail considérable qui a été consacré à la création de DirectX, Windows ou Xbox.
Le même arrangement fonctionne sur d’autres plateformes également. OpenGL est une API multiplateforme qui fonctionne sur presque toutes les plateformes modernes, et Sony PlayStation possède une API similaire. Ces deux API sont distribuées sous forme de bibliothèques C avec des fichiers .h publics et des implémentations propriétaires.
Chargement des fichiers objet exécutables
Un fichier objet est un fichier qui est une combinaison de métadonnées du code source ou objet avec une combinaison de bytecode
Liaison dynamique au moment du chargement
Les bibliothèques statiques présentent les inconvénients suivants : duplication dans les exécutables stockés (chaque fonction a besoin de libc), duplication dans les exécutables en cours d'exécution, et les corrections de bugs mineurs des bibliothèques système nécessitent que chaque application soit explicitement reliée. Une solution moderne à ce problème est d'utiliser des bibliothèques partagées (également appelées bibliothèques de liens dynamiques, DLLs, ou fichiers .so). Une bibliothèque partagée (shared library) est un fichier de bibliothèque qui peut être partagé par plusieurs programmes simultanément (Figure 4.28).
Lors de l'utilisation de bibliothèques partagées, les fichiers objet qui contiennent du code et des données peuvent être chargés et liés à une application dynamiquement au moment du chargement, comme illustré dans la Figure 4.29. Cette liaison au moment du chargement (load time linking) se produit lorsque la liaison (linking) dynamique se produit en même temps qu'un exécutable de programme est exécuté pour la première fois. C'est un cas courant pour Linux, qui est géré automatiquement par l'éditeur de liens (linker) dynamique (ld-linux.so). La bibliothèque C standard (libc.so) est généralement liée dynamiquement. L'outil ldd peut être utilisé pour identifier les dépendances/bibliothèques nécessaires au moment du chargement. Dans la liaison statique, le code des routines fait partie de l'exécutable. Dans la liaison dynamique, les routines peuvent être mises à jour pendant l'exécution du code. Pour lier dynamiquement une bibliothèque au moment du chargement sous Linux, placez-la dans le répertoire /lib/x86_64-linux-gnu/ et compilez les fichiers source avec l'indicateur -l (par exemple, gcc main.c -lcso).
ld-linux.so). (attribution : Copyright Rice University, OpenStax, sous licence CC BY 4.0)
Liaison dynamique à l'exécution
Une alternative à la liaison au moment du chargement est la liaison à l'exécution (runtime linking), ce qui signifie que la liaison se produit après qu'un programme a déjà commencé à s'exécuter. Comme illustré dans l'exemple de code, le code source (source code) du programme doit appeler explicitement des fonctions pour lier des bibliothèques supplémentaires. Sous Linux, cela se fait par des appels à l'interface dlopen() et en compilant la source avec l'indicateur -l (par exemple, gcc main.c -ldl). C'est une meilleure approche pour aider à distribuer des logiciels, prendre en charge des serveurs Web haute performance ou effectuer une interposition de bibliothèque (library) à l'exécution.
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* Charger dynamiquement la bibliothèque partagée qui contient addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
...
/* Obtenir un pointeur vers la fonction addvec() que nous venons de charger */
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
exit(1);
}
/* Maintenant, nous pouvons appeler addvec() comme n'importe quelle autre fonction */
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
/* Décharger la bibliothèque partagée */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
Outils pour manipuler les fichiers objet
Un fichier objet (object) contient beaucoup d'informations telles que des métadonnées, du code machine (machine code) et d'autres informations provenant de symboles (symbol). Pour manipuler de tels fichiers, Unix fournit certains outils pour les utiliser efficacement, tels que :
- ar : Crée des bibliothèques statiques, et insère, supprime, liste et extrait des membres.
- strings : Liste toutes les chaînes imprimables contenues dans un fichier objet.
- strip : Supprime les informations de symboles d'un fichier objet.
- nm : Liste les symboles définis dans la table de symboles d'un fichier objet.
- size : Liste les noms et les tailles des sections dans un fichier objet.
- readelf : Affiche la structure complète d'un fichier objet, y compris toutes les informations encodées dans l'en-tête ELF ; englobe la fonctionnalité de « size » et « nm ».
- objdump : Affiche toutes les informations d'un fichier objet ; utile pour désassembler les instructions binaires dans la section .text.
- ldd (linux (linux)) : Liste les bibliothèques partagées dont un exécutable a besoin à l'exécution.
Gestion du contrôle de version
Le processus et les outils utilisés pour stocker et améliorer plusieurs versions de fichiers de projet sont appelés contrôle de version (version control). Le contrôle de version aide également à soutenir la collaboration d'équipe et permet de revenir à des versions antérieures. Git (Git) est un système de contrôle de version largement utilisé. La création et la mise à jour de fichiers de projet à l'aide de Git nécessitent la création d'un dépôt (repository) Git, également appelé « repo » en abrégé. Un dépôt est un conteneur pour les fichiers et les informations connexes stockés dans un outil de contrôle de version. GitHub (GitHub) est un site Web qui permet le stockage gratuit de dépôts git publics.
Lien vers l'apprentissage
Apprenez-en davantage en installant Git sur votre machine locale sur n'importe quelle plateforme. Vous pouvez exécuter « brew install git » sur MacOS pour installer Git ou « sudo apt install git » sur Linux.
Les commandes Git utiles sont les suivantes :
git config --global user.email "[email protected]"etgit config --global user.name "Your Name"- Cloner : pour télécharger le contenu
- Pull :
git pull origin masterpour récupérer les dernières modifications - Status :
git statuspour voir les fichiers mis en scène (affichés en vert) et non mis en scène (affichés en rouge) - Staging :
git add <filename’pour ajouter des fichiers à la zone de staging (les jokers sont acceptés) - Commit :
git commit –m "<votre message ici>"pour valider les fichiers mis en scène - Push :
git push origin masterpour pousser toutes les modifications effectuées localement vers l'origine
Lien vers l'apprentissage
Explorez le tutoriel Git/GitHub pour plus de détails sur l'utilisation de Git.