cursus.

Cours 1 · Prendre en main le langageLeçon 1 sur 2

De la source à l'exécutable

5 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

Préprocesseur, compilation, édition de liens ; structure d'un programme et rôle de main ; lire un message d'erreur du compilateur.

Un débutant tape gcc hello.c, obtient undefined reference to printf, et cherche l'erreur dans sa syntaxe. Il ne la trouvera pas : ce message ne vient pas du compilateur. Il vient de l'éditeur de liens, un programme différent, appelé bien après, et qui reproche autre chose.

C'est le premier obstacle du cours, et il n'est pas conceptuel : gcc est un pilote qui enchaîne quatre programmes distincts, chacun avec ses erreurs propres. Savoir lequel a parlé, c'est déjà avoir résolu la moitié du problème.

Pourquoi le C

Le choix demande d'être justifié, car le C n'est ni le plus accueillant ni le plus productif des langages.

Il est le complément direct de l'architecture : un pointeur est une adresse au sens du chapitre 5, un char est un octet, un décalage de bits est l'instruction du chapitre 6. Ce que ce cours-là décrivait de l'extérieur devient ici manipulable.

Il est le langage des systèmes : le noyau Linux, les bibliothèques standard, les interpréteurs de Python et de Java sont écrits en C, et tous les appels système du cours de systèmes se présentent comme des fonctions C.

Il est enfin un contrepoint utile à Java en programmation orientée objet. Avoir géré la mémoire à la main rend le ramasse-miettes intelligible au lieu de le rendre invisible — on sait ce qu'il fait, parce qu'on l'a fait soi-même.

Il vient avec une contrepartie qu'il vaut mieux annoncer : le C ne protège de rien. Pas de vérification des bornes, pas de gestion automatique de la mémoire, et des comportements dits indéfinis où le programme peut faire n'importe quoi — y compris fonctionner. C'est le prix de la transparence, et c'est aussi ce qui rend le bloc VI indispensable.

Les quatre étages

hello.c   │  ① préprocesseur          cpp        gcc -E hello.chello.i        source pur C, sans directive, avec les en-têtes recopiés   │  ② compilation            cc1        gcc -S hello.ihello.s        assembleur — celui du chapitre 6 d'architecture   │  ③ assemblage             as         gcc -c hello.shello.o        fichier objet : code machine, mais adresses non résolues   │  ④ édition de liens       ld         gcc hello.o -o hellohello          exécutable

Le préprocesseur ne comprend pas le C : il fait de la manipulation de texte. Il recopie le contenu des fichiers désignés par #include, remplace les #define, et supprime les portions écartées par #ifdef. Un #include n'est pas un import : c'est un copier-coller, et c'est ce qui explique la taille du fichier .i — plusieurs centaines de kilooctets pour un hello.c de cinq lignes.

Le compilateur traduit le C en assembleur. C'est lui qui signale les erreurs de syntaxe et de typage, et lui seul travaille un fichier à la fois : il ne sait rien des autres fichiers du projet.

L'assembleur traduit l'assembleur en code machine. Il produit un fichier objet dont les appels à des fonctions extérieures sont laissés en blanc, sous forme de symboles non résolus.

L'éditeur de liens rassemble les fichiers objets et les bibliothèques, et remplit ces blancs. Il ne connaît que des noms de symboles : il ignore le C, les types et les lignes de code.

D'où le diagnostic annoncé en ouverture. undefined reference to truc signifie : « quelqu'un appelle truc, et je ne trouve nulle part sa définition ». Trois causes possibles, et aucune n'est une faute de syntaxe — le fichier qui définit truc n'a pas été passé à la compilation, la bibliothèque manque (-lm pour les fonctions mathématiques), ou la fonction est déclarée sans être définie.

Un premier programme

#include <stdio.h> int main(void) {    printf("Bonjour\n");    return 0;}

Quatre remarques, une par ligne.

#include <stdio.h> recopie les déclarations des fonctions d'entrée/sortie, dont printf. L'en-tête ne contient pas le code de printf — seulement sa signature, pour que le compilateur sache la vérifier. Le code, lui, est dans la bibliothèque C, et c'est l'éditeur de liens qui l'y trouvera. Les chevrons désignent les en-têtes du système ; les guillemets, ceux du projet.

int main(void) est le point d'entrée. Le système lance toujours cette fonction-là, et son type de retour n'est pas décoratif : c'est le code de retour du chapitre 2 du cours de systèmes, celui que le shell teste dans un if.

printf écrit sur la sortie standard — le descripteur 1. Le \n n'est pas de la décoration : sans lui, la sortie reste dans le tampon dont parlait le chapitre 8 du même cours, et peut ne jamais apparaître si le programme plante.

return 0 signifie « tout s'est bien passé ». Toute autre valeur signale une erreur, et c'est la convention que respecte l'ensemble du monde Unix.

Quiz · vérifiez votre compréhension Sans réponse

Un projet compile sans erreur avec gcc -c sur chaque fichier, mais l'édition de liens échoue sur « undefined reference to calculer ». Que peut-on en déduire ?

Lire un message d'erreur

C'est la compétence la plus rentable du bloc I, et elle s'apprend par trois règles.

Toujours commencer par la première erreur. Le compilateur, une fois désorienté, en produit souvent des dizaines de fantômes. Un point-virgule oublié ligne 12 peut engendrer quarante messages sur les lignes 13 à 50, qui disparaissent tous en corrigeant le premier.

Lire le numéro de ligne comme un indice, pas comme une adresse. L'erreur est signalée là où le compilateur s'est aperçu du problème, souvent une ligne après sa cause — c'est typiquement le cas du point-virgule manquant.

Distinguer erreur et avertissement. Une erreur arrête la compilation. Un avertissement la laisse passer, et signale un code légal mais suspect : conversion qui perd de l'information, variable utilisée sans être initialisée, comparaison entre signé et non signé. Ce sont souvent de vrais bogues.

D'où la ligne de commande à prendre comme habitude dès le premier TP :

gcc -Wall -Wextra -g -o programme source.c

-Wall -Wextra activent les avertissements — le compilateur en tait beaucoup par défaut, pour des raisons de compatibilité historique. -g conserve les informations de débogage, sans lesquelles gdb du chapitre 10 ne pourra pas afficher vos noms de variables. Et l'on prend l'habitude de traiter tout avertissement comme une erreur : c'est plus rapide que de le diagnostiquer trois heures plus tard au débogueur.

Quiz · vérifiez votre compréhension Sans réponse

Pourquoi le fichier produit par gcc -E sur un hello.c de cinq lignes fait-il plusieurs centaines de kilooctets ?

À vous

Le C ne s'exécute pas dans un navigateur : l'exercice simule donc la chaîne de compilation en JavaScript, ce qui a l'avantage de rendre visible ce que chaque étage fait — et surtout ce qu'il refuse.

Vous écrirez un préprocesseur qui traite #define et #include, puis un éditeur de liens qui collecte les symboles définis et les symboles appelés, et signale les non résolus. Le jeu de tests contient les trois erreurs classiques : un symbole jamais défini, une définition présente dans un fichier qu'on a oublié de passer, et une double définition.

Exercice · JavaScript · à vous de jouer

Simulez le préprocesseur et l'éditeur de liens, puis provoquez les deux erreurs classiques.

En attente
// ── Les fichiers du projet ────────────────────────────────────────────────
const FICHIERS = {
  "calcul.h": [
    "#define PI 3.14159",
    "int aire(int r);",              // déclaration seule
    "int perimetre(int r);",         // déclarée, jamais définie : piège
  ].join("\n"),

  "calcul.c": [
    '#include "calcul.h"',
    "int aire(int r) { return PI * r * r; }",
  ].join("\n"),

  "main.c": [
    '#include "calcul.h"',
    "int main(void) {",
    "  int a = aire(3);",
    "  int p = perimetre(3);",       // appelée, jamais définie
    "  return a + p;",
    "}",
  ].join("\n"),
};

// ── ① Préprocesseur ───────────────────────────────────────────────────────
// Manipulation de TEXTE : recopier les #include, remplacer les #define.
function preprocesseur(nom, vus = new Set()) {
  if (vus.has(nom)) return "";        // garde d'inclusion
  vus.add(nom);
  const defines = {};
  const sortie = [];
  for (const ligne of FICHIERS[nom].split("\n")) {
    const inc = /^#include "(.+)"$/.exec(ligne.trim());
    if (inc) { sortie.push(preprocesseur(inc[1], vus)); continue; }
    const def = /^#define (\w+) (.+)$/.exec(ligne.trim());
    if (def) { defines[def[1]] = def[2]; continue; }
    // ← à écrire : remplacer chaque macro connue par sa valeur
    sortie.push(ligne);
  }
  return sortie.join("\n");
}

// ── ② Compilation : un fichier à la fois ──────────────────────────────────
// On récolte les symboles DÉFINIS et les symboles APPELÉS.
function compiler(source) {
  const definis = [...source.matchAll(/^(?:int|void) (\w+)\s*\([^)]*\)\s*\{/gm)].map((m) => m[1]);
  const appeles = [...source.matchAll(/(\w+)\s*\(/g)].map((m) => m[1])
    .filter((n) => !["int", "void", "if", "while", "for", "return"].includes(n));
  return { definis, appeles: [...new Set(appeles)].filter((n) => !definis.includes(n)) };
}

// ── ④ Édition de liens ────────────────────────────────────────────────────
function editerLiens(objets) {
  const erreurs = [];
  // ← à écrire : rassembler tous les symboles définis, puis vérifier que
  //   chaque symbole appelé en trouve un. Signaler aussi les doublons.
  return erreurs;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Faites remplacer les macros par le préprocesseur (PI doit disparaître).
// 2. Écrivez editerLiens, avec les DEUX erreurs : symbole non résolu et
//    définition multiple.
// 3. Compilez main.c SEUL, puis main.c + calcul.c. Quelle erreur reste ?

console.log("— calcul.c après préprocesseur —");
console.log(preprocesseur("calcul.c"));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 1 · sur machine

Ouvrir la chaîne de compilation

Exécuter séparément les quatre étapes qu'une seule commande cache, puis poser le squelette du programme qui grandira pendant tout le semestre.

2 h
Avant de commencer
  • gcc et make installés
  • Un éditeur de texte, et un terminal
  1. 1. Les quatre étapes, une par une

    À partir d'un programme d'une ligne, produisez successivement le fichier prétraité, l'assembleur, l'objet, puis l'exécutable. Regardez la taille de chacun.

  2. 2. Ce que fait le préprocesseur

    Comptez les lignes de votre source, puis celles du fichier prétraité. Trouvez d'où viennent les autres, et cherchez-y la déclaration de printf.

  3. 3. Activer les avertissements

    Écrivez un programme qui utilise une variable non initialisée et oublie une valeur de retour. Compilez sans option, puis avec -Wall -Wextra. Comparez.

  4. 4. Séparer en deux fichiers

    Créez le fil rouge du semestre : un fichier principal et un module de comptage, avec son en-tête. Compilez les deux séparément puis liez-les.

  5. 5. L'erreur de l'éditeur de liens

    Déclarez une fonction dans l'en-tête sans l'écrire. Compilez chaque fichier — ça passe. Liez — ça échoue. Expliquez la différence entre les deux messages.

  6. 6. Le Makefile

    Écrivez un Makefile qui reconstruit uniquement ce qui a changé. Modifiez un seul fichier et vérifiez que l'autre n'est pas recompilé.

C'est réussi quand
  • Vous nommez les quatre étapes et ce qui entre et sort de chacune
  • -Wall -Wextra vous signale au moins deux problèmes que la compilation muette laissait passer
  • make ne recompile que le fichier modifié
  • Vous distinguez une erreur de compilation d'une erreur d'édition de liens

Ce que la suite en fait

Le chapitre 2 remplit le programme : types, variables, opérateurs, et les entrées/sorties formatées dont printf n'est que le représentant le plus visible. On y verra que la taille d'un type n'est pas une abstraction mais un nombre d'octets — celui du chapitre 2 d'architecture.

La chaîne de compilation reviendra au chapitre 4, quand le projet cessera de tenir dans un seul fichier : les en-têtes, la compilation séparée et le Makefile sont exactement la gestion de ce que ce chapitre vient de décrire. Et le chapitre 10 y ajoutera les outils qui inspectent le résultat.

À retenir

Flashcards · 1 / 4Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.