Programmation C · L2 · Section 1/12
Introduction au C
Progression
#Introduction au langage C
Le premier contact avec C passe par l’outillage : un éditeur de texte, un compilateur (gcc ou clang), un terminal. Contrairement aux langages gérés qui embarquent un runtime, C produit un exécutable natif qui parle directement au système d’exploitation : pas de collecteur embarqué, pas de vérification à l’exécution. Le contrat est simple et exigeant : ce que vous écrivez est exécuté tel quel, y compris vos erreurs. Comprendre la chaîne de compilation donne de l’assurance pour la suite du module.
#1. Anatomie d’un programme minimal
1#include <stdio.h>2 3int main(void) {4 puts("Hello, C !");5 return 0;6}Quatre éléments structurent ces lignes :
#include <stdio.h>insère les déclarations de la bibliothèque d’entrées/sorties pendant le prétraitement ; sans elle, l’appel àputsserait refusé (déclaration implicite interdite par les compilateurs actuels).int main(void)est la signature définie par la norme pour un programme sans arguments : elle renvoie unintet ne prend rien. Ce n’est pas cosmétique, c’est le contrat avec le système.putsécrit la chaîne sur la sortie standard suivie d’un saut de ligne. Préférezputsàprintfquand il n’y a rien à formater : sans chaîne de format, pas de bug de format, etprintf(user_input)serait de toute façon une vulnérabilité.return 0;signale la réussite au système. Omettre cereturndansmainreste valide : la norme garantit un retour implicite de 0.
Compilation et exécution :
1cc -std=c17 -Wall -Wextra -Wpedantic hello.c -o hello2./hello # affiche : Hello, C !3echo $? # affiche : 0#2. Les quatre phases de la compilation
La commande précédente enchaîne quatre étapes que l’on peut observer séparément :
1cc -E hello.c -o hello.i # 1. prétraitement : directives résolues2cc -S hello.i -o hello.s # 2. compilation : C vers assembleur3cc -c hello.s -o hello.o # 3. assemblage : code objet réadressable4cc hello.o -o hello # 4. édition de liens : symboles résolus- Prétraitement :
#includerecopie le contenu des en-têtes,#defineeffectue des substitutions textuelles.hello.icontient des milliers de lignes carstdio.hs’y déroule entièrement. - Compilation : le compilateur traduit le C en assembleur et émet les diagnostics. C’est la phase où les warnings sauvent des heures de débogage.
- Assemblage : l’assembleur produit un fichier objet, du code machine avec des références externes non résolues (
putsn’existe pas encore dans le binaire). - Édition de liens : le linker (
ld, piloté par le drivercc) résoutputsdans la bibliothèque C et produit l’exécutable.
Cette gymnastique vous servira lors du débogage : un symbole indéfini est un problème d’édition de liens, pas de syntaxe, et les messages des deux phases se lisent différemment. Elle prépare aussi les builds séparés du chapitre sur la modularité.
#3. Une discipline de compilation dès le premier jour
Adoptez immédiatement des options strictes ; placez-les dans un Makefile pour ne pas compter sur votre mémoire :
-std=c17fixe le dialecte du langage (gnu17y ajoute des extensions GNU ; réservez-les aux projets qui les assument).-Wall -Wextra -Wpedanticactivent les diagnostics les plus rentables : variable non initialisée, comparaison signé/non signé, conversions suspectes.-Wconversionsignale les conversions implicites rétrécissantes, précieuse avant tout protocole binaire.-gembarque les informations de débogage ; sans elle, ASan et Valgrind ne parlent qu’en adresses brutes.-fsanitize=address,undefinedinstrumente le binaire pour détecter au runtime dépassements de tampon, use-after-free et comportements indéfinis arithmétiques. À réserver aux builds de test : le binaire est plus gros et plus lent.
#4. Parler au shell : arguments et code de sortie
1#include <stdio.h>2 3int main(int argc, char **argv) {4 printf("programme : %s\n", argv[0]);5 for (int i = 1; i < argc; ++i) {6 printf("argument %d : %s\n", i, argv[i]);7 }8 return 0;9}La norme garantit : argc vaut au moins 1, argv[0] est le nom du programme, argv[argc] vaut NULL, et les chaînes sont modifiables. Leur durée de vie est celle du programme : ne libérez jamais argv[i], et copiez toute chaîne que vous souhaitez conserver au-delà de main.
1./args un deux2# programme : ./args3# argument 1 : un4# argument 2 : deuxLe code de sortie de main devient le statut du processus : 0 pour succès, non nul sinon. C’est la valeur lue par $?, testée par les scripts shell et les Makefiles. Restez entre 0 et 255 : le shell tronque au-delà.
#Atelier
- Compilez le programme minimal avec
gccpuis avecclang(mêmes options), comparez :ls -l hello-gcc hello-clang. - Activez un sanitizer et provoquez un dépassement volontaire :
1#include <string.h>2 3int main(void) {4 char tampon[8];5 strcpy(tampon, "bien trop long pour huit octets");6 return (int)tampon[0]; /* force la lecture du tampon */7}1cc -std=c17 -g -Wall -Wextra -fsanitize=address,undefined debord.c -o debord2./debord- Générez l’assembleur avec
cc -S hello.cet repérez l’appel àputs.
Sorties attendues et lecture du rapport ASan
1. Les tailles diffèrent de quelques pourcents entre GCC et Clang : chaque compilateur embarque son propre prologue et son runtime. 2. ASan interrompt le programme avec une première ligne du type ERROR: AddressSanitizer: stack-buffer-overflow, suivie de la pile d’appels (numéros de ligne exacts grâce à -g) et d’un SUMMARY: AddressSanitizer: stack-buffer-overflow .... GCC a de son côté émis un warning sur ce strcpy : les deux niveaux de détection sont complémentaires. 3. Dans hello.s (syntaxe AT&T sous Linux), cherchez la ligne call puts ou callq puts@PLT : c’est l’appel résolu au chargement.