Séance 0 — Introduction au cours

Pourquoi le C, histoire, plan des séances, environnement, chaîne de compilation, premier programme

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

Ce que la séance construit

Le cadre du cours : pourquoi le C reste le langage des couches basses, d’où il vient et comment sa norme évolue, ce que les six séances suivantes construisent ; puis l’atelier : l’environnement Docker du cours, le chemin d’un fichier source jusqu’au processus qui s’exécute, et les outils qui observent ce processus (gcc, gdb, valgrind, AddressSanitizer, make).

  • Citer des logiciels écrits en C, dire ce que le langage apporte (contrôle de la mémoire, coût prévisible) et ce qu’il laisse au programmeur (aucune vérification à l’exécution).
  • Situer les versions du langage, de K&R à C23, et lire la norme qu’applique gcc (__STDC_VERSION__).
  • Décrire le plan du cours, de la syntaxe (Séance 1) aux pointeurs de fonctions (Séance 6).
  • Lancer le conteneur du cours et y travailler sur les fichiers de l’hôte.
  • Suivre la chaîne de compilation, fichier produit par fichier produit, et reconnaître la phase qui signale une erreur.
  • Suivre un premier programme ligne par ligne, lire ses variables sous gdb, et détecter avec valgrind et AddressSanitizer une lecture hors d’un bloc.

Le cours et sa démarche

Comprendre ce que la machine fait d’un programme

Le cours part d’une syntaxe supposée connue et regarde ce qu’elle devient sur la machine.

Ce que le cours apprend

Ce cours n’est pas une introduction au C. Il montre, pour chaque construction du langage, ce que la machine en fait : les octets d’une valeur, l’adresse d’une variable, la place d’un champ dans une structure, le bloc rendu par malloc, l’appel d’une fonction par son adresse. L’objectif est la compréhension des mécanismes, de l’organisation de la mémoire et des outils du programmeur système.

La méthode

  • Chaque programme est compilé et exécuté ; les sorties montrées sont celles de l’exécution.
  • La mémoire se lit en hexadécimal, octet par octet : dumps, grille Addr | +0 … +7.
  • Les erreurs se provoquent et se lisent : messages de gcc, rapports de valgrind et d’AddressSanitizer.

Références du cours

R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., 2016 ; cours CSE 351 (The Hardware/Software Interface) et CSE 333 (Systems Programming) de l’université de Washington.

Trois questions, trois séances

struct mesure {
    char code;
    int valeur;
    char unite;
};
int main(void)
{
    double a = 0.1 + 0.2;
    printf("%d\n", a == 0.3);
    printf("%.17f\n", a);
    int x = 0x11223344;
    unsigned char *o = (unsigned char *)&x;
    for (int k = 0; k < 4; k++)
        printf("%02x ", o[k]);
    printf("\n");
    printf("%zu\n", sizeof(struct mesure));
    return 0;
}

Résultat \(\rightarrow\)

0
0.30000000000000004
44 33 22 11 
12

Où le cours répond

0, et non 1 Séance 2 : 0,1 n’a pas d’écriture binaire finie (IEEE 754)
44 33 22 11 Séance 3 : octet de poids faible en premier (little-endian)
12, et non 6 Séance 4 : alignement des champs, padding

Lecture. Trois résultats que la syntaxe n’explique pas : représentation des valeurs, place en mémoire.

Pourquoi le C

Systèmes d’exploitation et systèmes embarqués

Le C est le langage des couches basses : celles sur lesquelles les autres langages s’exécutent.

Systèmes d’exploitation

Linux : plus de 40 millions de lignes dans l’arbre des sources du noyau (début 2025), en très grande majorité du C, avec de l’assembleur et, depuis la version 6.1 (2022), une infrastructure pour Rust, utilisée par de premiers pilotes depuis la version 6.8 (2024). Windows : noyau NT écrit pour l’essentiel en C, avec du C++ et de l’assembleur. macOS, iOS : noyau XNU en C, pilotes (IOKit) en C++. Les interfaces système POSIX (open, read, write, fork) sont définies comme des fonctions C.

Systèmes embarqués

Micrologiciels des microcontrôleurs (ARM Cortex-M, AVR, PIC), noyaux temps réel (FreeRTOS, Zephyr), objets connectés, calculateurs des véhicules (ECU) reliés par le bus CAN, appareils médicaux : le C y est choisi pour la petite taille du code produit, un temps d’exécution prévisible et l’accès direct aux registres du matériel.

Bases de données, interpréteurs, réseau

Écrits en C

bases de données PostgreSQL, SQLite, Redis
interpréteurs CPython (l’interpréteur Python de référence), Lua, Ruby (MRI), PHP
serveurs, réseau nginx, Apache httpd, OpenSSH, curl
outils du système bash, zsh, coreutils (ls, cp…), Git
calcul, multimédia FFmpeg, x264, FFTW, OpenBLAS (C et assembleur)

À ne pas confondre

Sont écrits en C++, et non en C : MySQL (pour l’essentiel), la machine virtuelle Java HotSpot, GCC depuis la version 4.8 (2013), Clang et LLVM. Les bibliothèques de référence BLAS et LAPACK sont écrites en Fortran ; OpenBLAS en est une implémentation en C et en assembleur.

Calcul pour l’IA, cryptographie, et la contrepartie

Calcul pour l’IA

Les noyaux de calcul de PyTorch (ATen, c10), de TensorFlow et d’ONNX Runtime sont écrits en C++ et en CUDA, souvent derrière des API C (cuDNN, ONNX Runtime, extensions de CPython). Sur processeur, les produits de matrices passent par OpenBLAS (C et assembleur).

Cryptographie

OpenSSL, libsodium, WireGuard dans le noyau Linux (environ 4000 lignes) : du C, pour la vitesse et la portabilité ; c’est la petite taille du code de WireGuard, non le langage, qui réduit sa surface d’attaque.

La contrepartie

Le C ne vérifie ni les indices, ni les pointeurs, ni la durée de vie des blocs : environ 70 % des vulnérabilités traitées chaque année par Microsoft (bilan de 2019) et des failles graves de Chromium (2020), deux bases de code en C et en C++, sont des erreurs de sécurité mémoire (buffer overflow, use after free), au cœur de ce cours.

Un interpréteur Python est un programme C

Terminal \(\rightarrow\)

$ python3 -c 'import sys; print(sys.version)'
3.13.16 (main, Oct  1 2026, 15:40:24) [GCC 13.3.0]
$ file -bL /usr/bin/python3 | cut -d, -f1-2
ELF 64-bit LSB executable, x86-64
$ ldd /usr/bin/python3 | grep -o 'libc.so.6 => [^ ]*'
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6

Ce que le C offre

Un contrôle précis de la mémoire (où vit chaque objet, combien de temps, qui le libère), un coût prévisible pour chaque instruction, et une portabilité très large : un compilateur C existe pour presque chaque processeur.

Lecture. python3 de la machine de mesure (il n’est pas installé dans l’image du cours) est un exécutable ELF, compilé par gcc 13.3.0 à partir des sources C de CPython ; comme tout programme C de la machine, il appelle la bibliothèque C (libc.so.6), celle qu’utilisent les programmes de ce cours.

Histoire du C

De BCPL au C, d’UNIX à la norme

Le C est né avec UNIX, aux Bell Labs (AT&T), puis a été normalisé par l’ANSI et l’ISO.

Lecture. Ken Thompson écrit B (1969) à partir de BCPL (Martin Richards, 1967) ; Dennis Ritchie lui ajoute des types (char, int, pointeurs, puis structures) entre 1971 et 1973 : c’est le C. En 1973, le noyau d’UNIX (4e édition) est réécrit en C, jusque-là en assembleur. Le livre de Kernighan et Ritchie (1978) sert de définition jusqu’à la première norme.

Les versions du langage

version texte apports
K&R C livre, 1978 définition de fait, sans norme ; paramètres déclarés hors des parenthèses
C89, C90 ANSI X3.159-1989, ISO/IEC 9899:1990 prototypes de fonctions, const, volatile, void *, bibliothèque standard normalisée
C99 ISO/IEC 9899:1999 long long, _Bool, commentaires //, déclarations parmi les instructions et dans le for, VLA, inline, <stdint.h>, initialiseurs désignés
C11 ISO/IEC 9899:2011 _Static_assert, _Generic, _Alignas ; threads (<threads.h>) et atomiques (<stdatomic.h>), optionnels ; VLA devenus optionnels
C17 ISO/IEC 9899:2018 corrections, aucune nouveauté : la norme du cours (-std=c17)
C23 ISO/IEC 9899:2024 typeof, nullptr, bool et true mots-clés, attributs ([[nodiscard]]), _BitInt(N), constantes 0b1010, #embed

Lecture. Une norme porte le nom de l’année de sa rédaction : C17 (dite aussi C18) est publiée en 2018, C23 en octobre 2024. Les compilateurs suivent avec retard : gcc 13 connaît une partie de C23 sous le nom provisoire c2x ; gcc 15 compile par défaut en gnu23.

La norme qu’applique gcc

int main(void)
{
#ifdef __STDC_VERSION__
    printf("%ldL\n", __STDC_VERSION__);
#else
    printf("C89\n");
#endif
    return 0;
}
option sortie du programme
(aucune) 201710L
-std=c99 199901L
-std=c11 201112L
-std=c17 201710L
-std=c2x 202000L
int main(void)
{
    typeof(1.5) x = 2.5;
    bool ok = true;
    int *p = nullptr;
    printf("%.1f %d %d\n", x, ok, p == nullptr);
    return 0;
}

Compilateur, avec -std=c17 (extrait) \(\rightarrow\)

s3_c23.c:5:16: error: expected ';' before 'x'
s3_c23.c:6:5: error: unknown type name 'bool'
s3_c23.c:6:15: error: 'true' undeclared (first use in this function)
s3_c23.c:7:14: error: 'nullptr' undeclared (first use in this function); did you mean 'nullptr_t'?

Lecture. Par défaut : gnu17. typeof (vu comme une fonction non déclarée), bool, true, nullptr : inconnus en C17, mots-clés en c2x, où le programme affiche 2.5 1 1. C23 : 202311L (gcc 14).

Philosophie du langage et influence

L’« esprit du C » (Rationale de la norme C)

Faire confiance au programmeur ; ne pas l’empêcher de faire ce qui doit être fait ; garder un langage petit ; une seule façon de faire une opération ; être rapide, même sans garantie de portabilité.

Conséquences

Aucune vérification d’indice ni de pointeur à l’exécution ; des instructions proches de celles du processeur ; une bibliothèque standard réduite (entrées-sorties, chaînes, mémoire, mathématiques, temps) ; et des comportements indéfinis : la norme n’impose rien à un programme qui sort d’un tableau ou déborde un entier signé.

Influence

La syntaxe du C (accolades, opérateurs, if, for, while) est reprise par C++ et Objective-C, construits sur lui, puis par Java, C#, JavaScript, PHP et Go.

Exercice : quelle version du langage ?

/* (a) */ // commentaire de fin de ligne
/* (b) */ long long population = 18000000LL;
/* (c) */ _Static_assert(sizeof(int) == 4,
                         "int de 4 octets");
/* (d) */ int carre(int x);
/* (e) */ typeof(x) y = 4;
/* (f) */ for (int i = 0; i < n; i++)
              s = s + i;

Énoncé

Pour chaque fragment, donner la première norme qui l’accepte (C89, C99, C11, C23).

Correction

  1. C99 ; en C89 : « C++ style comments are not allowed in ISO C90 ».
  2. C99 : long long et le suffixe LL.
  3. C11 ; en C99 : « ISO C99 does not support ‘_Static_assert’ ».
  4. C89 : le prototype vient de la première norme (K&R écrivait int carre();).
  5. C23 ; avant, typeof n’est qu’une extension GNU, refusée sous -std=c17.
  6. C99 ; en C89 : « ‘for’ loop initial declarations are only allowed in C99 or C11 mode ».

Vérification : gcc -std=c89 -pedantic-errors -c (puis c99, c11, c17, c2x) sur chaque fragment.

Structure du cours

Sept séances, de la syntaxe aux pointeurs de fonctions

Chaque séance comporte un CM, un TD et un TP (la Séance 0 : un CM et le TP-1) ; chacune repart des outils de la précédente.

séance titre ce que la séance construit
S0 Introduction au cours cadre, environnement Docker, chaîne de compilation, outils (TP-1)
S1 Syntaxe C complète (fondamentaux) types, variables, opérateurs, contrôle, fonctions, tableaux, entrées-sorties, compilation séparée, comportements indéfinis
S2 Représentation numérique binaire, hexadécimal, entiers non signés et complément à deux, conversions et débordements, opérations bit à bit, flottants IEEE 754
S3 Organisation de la mémoire adresses, endianness, pointeurs, tableaux, chaînes, pile et tas, show_bytes
S4 Structures et unions champs, alignement, padding, unions, champs de bits, structures sur le tas
S5 Allocation dynamique et structures chaînées propriété d’un bloc, tableau dynamique, listes, piles, files, erreurs du tas, intérieur de malloc
S6 Pointeurs de fonctions et généricité adresse d’une fonction, tables de dispatch, rappels, qsort, conteneurs génériques

Lecture. Les octets (S2) se lisent aux adresses (S3) ; les adresses placent les champs (S4) et les blocs du tas (S5), que des fonctions génériques manipulent (S6).

Du plan en six parties aux séances

partie du plan où elle se trouve
I. Fondamentaux du C S1 (et S0 pour la compilation)
II. Représentation S2
III. Mémoire, pointeurs, tableaux S3
IV. Structures et allocation S4, S5
V. Exécution et système segments du processus et stack frame : S3 ; conventions d’appel, hiérarchie de cache, fichiers POSIX : hors des sept séances
VI. Outils et techniques édition de liens, préprocesseur, make : S0, S1 et leurs TP ; pointeurs de fonctions : S6 ; optimisation : hors des sept séances
chapitres optionnels multithreading, sockets réseau : hors des sept séances

Lecture. Le plan initial comptait quatorze chapitres (Ch0 à Ch13), en six parties ; le cours en couvre l’essentiel en sept séances, S0 à S6.

Compétences visées et prérequis

Compétences visées

  • Expliquer ce que la machine fait d’un programme C, au-delà de la syntaxe.
  • Lire la mémoire : octets, adresses, dumps hexadécimaux.
  • Déboguer avec gdb, valgrind et AddressSanitizer.
  • Écrire du code robuste : propriété des blocs, entrées vérifiées, avertissements traités.
  • Utiliser les outils du métier : gcc, make, Docker.

Prérequis

Variables et types de base (int, float, char) ; if, for, while ; fonctions et passage de paramètres ; premiers pointeurs (déclaration, *p) ; compiler avec gcc. Les bases sont supposées acquises : la Séance 1 les reprend en détail, sur la machine.

Environnement et organisation

Un environnement identique pour tous

Tous les TP se font dans un conteneur Docker : le même système et les mêmes outils sur chaque machine.

Lecture. Windows, macOS et Linux exécutent la même image : Ubuntu 24.04 et les outils du cours. Le dossier de travail de l’hôte est monté sous /workspace : un fichier écrit d’un côté est lu de l’autre.

Le contenu de l’image du cours

outils rôle
gcc 13.3.0, g++ compiler (options du cours)
AddressSanitizer erreurs mémoire (-fsanitize=address)
gdb 15.1, gdb-dashboard exécuter pas à pas, lire les variables
valgrind 3.22.0 erreurs mémoire, fuites
make 4.3, cmake 3.28 automatiser la compilation
objdump, nm, hexdump, file lire un binaire, un fichier
strace, ltrace suivre les appels système, de bibliothèque
vim, nano, git éditer, suivre les versions

Image

elbachirtoure/ c-advanced-course:v2.0, sur ubuntu:24.04, environ 1,1 Gio installée. Versions relevées dans une image reconstruite à partir du Dockerfile du cours ; help-tp liste les commandes.

Mac à puce Apple

L’image v2.0 est publiée pour amd64 et arm64 : sur un Mac à puce Apple, le conteneur tourne en arm64, et code machine et adresses diffèrent de ceux du cours.

Lancer le conteneur

Terminal \(\rightarrow\)

docker run -it --rm \
    --security-opt seccomp=unconfined \
    -v "$(pwd)":/workspace \
    elbachirtoure/c-advanced-course:v2.0
-it terminal interactif
--rm conteneur supprimé à la sortie (exit)
--security-opt autorise setarch -R et gdb à désactiver l’aléa des adresses
-v dossier courant de l’hôte monté sous /workspace

PowerShell (Windows) : -v "${PWD}:/workspace".

Sortie d’erreur sans –security-opt \(\rightarrow\)

$ setarch -R ./adresse
setarch: failed to set personality to (null): Operation not permitted
$ gdb -q -nx -batch -ex run ./adresse
warning: Error disabling address space randomization: Operation not permitted

Résultat avec –security-opt \(\rightarrow\)

$ setarch -R ./adresse
x = 2026, adresse de x : 0x7fffffffec04
$ setarch -R ./adresse
x = 2026, adresse de x : 0x7fffffffec04

Lecture. Le profil de sécurité par défaut de Docker interdit l’appel système qui désactive l’aléa des adresses (personality) : setarch -R échoue, gdb avertit. Avec l’option, une variable locale garde la même adresse d’une exécution à l’autre ; les séances suivantes s’en servent.

Organisation : CM, TD, TP

Pour chaque séance, trois temps et des documents distincts.

CM

Les notions, projetées pas à pas : code, état de la mémoire, sortie du programme exécuté. Un CM de lecture, rédigé, reste sur le site du cours (ebtoure.sn).

TD

Exercices sur papier : lire un dump hexadécimal, suivre des pointeurs, calculer des adresses, dire ce qu’affiche un programme.

TP

Sur machine, dans le conteneur : compiler, observer, provoquer et corriger des erreurs sous gdb, valgrind et AddressSanitizer.

Lecture. La Séance 0 n’a pas de TD : son TP, le TP-1, installe et vérifie l’environnement (Docker, image, éditeur, gcc, make, gdb, valgrind, AddressSanitizer).

De la source à l’exécutable

Quatre phases derrière la commande gcc

Une commande gcc bonjour.c -o bonjour enchaîne quatre transformations du fichier source.

bonjour.c

#define VILLE "Dakar"

int main(void)
{
    printf("Bonjour, %s !\n", VILLE);
    return 0;
}
phase programme fichiers arrêt
préprocesseur cc1 .c \(\rightarrow\) .i -E
compilation cc1 .i \(\rightarrow\) .s -S
assemblage as .s \(\rightarrow\) .o -c
édition de liens collect2, ld .o \(\rightarrow\) exécutable —

Terminal \(\rightarrow\)

$ gcc -### -std=c17 bonjour.c -o bonjour 2>&1 | grep '^ ' | awk '{ if ($1 == "as") print; else print $1 }'
/usr/libexec/gcc/x86_64-linux-gnu/13/cc1
 as --64 -o /tmp/cc7H9w5w.o /tmp/ccFwmVXE.s
/usr/libexec/gcc/x86_64-linux-gnu/13/collect2

Lecture. gcc est un pilote : gcc -### affiche les programmes qu’il lancerait, sans les lancer. cc1 réunit préprocesseur et compilateur ; as assemble ; collect2 appelle l’éditeur de liens ld. Sans -E, -S ni -c, le .s et le .o sont créés dans /tmp, puis effacés.

De la source à l’exécutable, pas à pas

Dossier après chaque commande ; nature : premier champ de file ; rouge : fichier produit, orange : fichier lu.

Sortie \(\rightarrow\)

Bonjour, Dakar !

Lecture. Le préprocesseur remplace #include <stdio.h> par le texte de l’en-tête (déclaration de printf comprise) et VILLE par "Dakar" : 549 lignes, plus aucune directive.

Lecture. Le compilateur traduit le C en assembleur x86-64 : les chaînes deviennent des .string, l’appel devient call printf@PLT.

Lecture. L’assembleur code les instructions en binaire : fichier objet ELF de 1544 octets ; main y est défini (T), printf utilisé mais absent (U).

Lecture. L’éditeur de liens ajoute le code de démarrage et relie printf à la glibc partagée (printf@GLIBC_2.2.5) : exécutable PIE (chargeable à toute adresse) de 15968 octets.

Lecture. Au lancement, le chargeur dynamique charge libc.so.6 et y trouve printf ; le code de démarrage appelle main : une ligne de sortie.

Exercice : quelle phase signale l’erreur ?

/* (a) */ #include <stdoi.h>

/* (b) */ int km = 70
          printf("%d km\n", km);

/* (c) */ void afficher_total(int total);
          /* ... dans main : */
          afficher_total(1500);

/* (d) */ int places = 0;
          int passagers = 45;
          printf("cars : %d\n",
                 passagers / places);

Énoncé

Programmes complets, compilés par gcc -std=c17 -Wall -Wextra : quelle étape s’arrête, sur quel message ?

Correction

  1. Préprocesseur : l’en-tête n’existe pas, « fatal error: stdoi.h: No such file or directory ».
  2. Compilateur : syntaxe, « expected ‘,’ or ‘;’ before ‘printf’ », signalé sur la ligne suivante.
  3. Éditeur de liens : déclarée, jamais définie ; « undefined reference to afficher_total'~», puiscollect2: error: ld returned 1 exit status`.
  4. Exécution : aucun message de gcc ; division entière par zéro, signal SIGFPE, statut 136 \(= 128 + 8\).

Terminal \(\rightarrow\)

$ ./p4; echo "statut $?"
Floating point exception
statut 136

Un premier programme et les outils

Un premier programme, pas à pas

Un premier programme, compilé avec les options du cours, suivi ligne après ligne.

Une rangée par ligne exécutée (les huit dernières) ; valeurs relevées sous gdb ; ·· : pas encore de valeur ; — : variable hors de portée.

Sortie \(\rightarrow\)

1 ticket(s) : 250 FCFA2 ticket(s) : 500 FCFA
3 ticket(s) : 750 FCFAfin

Lecture. prix \(=250\) (FCFA, un ticket de bus) ; total n’a pas encore de valeur, n n’existe pas encore.

Lecture. total \(=0\).

Lecture. Entrée dans la boucle : n = 1, puis le test n <= 3, vrai.

Lecture. total + prix est calculé, puis rangé dans total : 250.

Lecture. printf remplace %d par n puis par total : une ligne de sortie.

Lecture. Fin du tour : n++ porte n à 2 ; le test est vrai, un tour de plus.

Lecture. total + prix est calculé, puis rangé dans total : 500.

Lecture. printf remplace %d par n puis par total : une ligne de sortie.

Lecture. Fin du tour : n++ porte n à 3 ; le test est vrai, un tour de plus.

Lecture. total + prix est calculé, puis rangé dans total : 750.

Lecture. printf remplace %d par n puis par total : une ligne de sortie.

Lecture. n++ porte n à 4 ; le test 4 \(\le\) 3 est faux : sortie de la boucle, n cesse d’exister.

Lecture. Après la boucle, total garde sa valeur ; printf affiche la dernière ligne.

Compiler avec les avertissements

int carre(int x)
{
    int inutile;
    if (x > 0)
        return x * x;
}

int main(void)
{
    printf("%d\n", carre(4));
    return 0;
}

Terminal \(\rightarrow\)

$ gcc s6_avert.c -o s6_avert
$ gcc -std=c17 -Wall -Wextra s6_avert.c -o s6_avert
s6_avert.c: In function 'carre':
s6_avert.c:5:9: warning: unused variable 'inutile' [-Wunused-variable]
    5 |     int inutile;
      |         ^~~~~~~
s6_avert.c:8:1: warning: control reaches end of non-void function [-Wreturn-type]
    8 | }
      | ^

Lecture. Sans option, gcc ne dit rien. Avec -Wall -Wextra : une variable inutilisée, et une fonction qui peut finir sans return (si x <= 0, valeur rendue indéterminée). Options du cours : -std=c17 (norme), -Wall -Wextra, -O0 (sans optimisation), -g (pour gdb).

Arrêter le programme sous gdb

commande effet
break 8 arrêt avant la ligne 8
run lance le programme
print n valeur de n
next exécute une ligne
continue jusqu’au prochain arrêt
info locals variables locales
backtrace pile des appels

v_tickets.c, compilé avec -g ; dix commandes, dans cet ordre : break 8, run, print n, print total, next, print total, continue, print n, info locals, backtrace.

Dans le conteneur, gdb-dashboard s’affiche à chaque arrêt : dashboard -enabled off le masque.

Terminal \(\rightarrow\)

Breakpoint 1 at 0x118c: file v_tickets.c, line 8.

Breakpoint 1, main () at v_tickets.c:8
8          total = total + prix;
$1 = 1
$2 = 0
9          printf("%d ticket(s) : %d FCFA\n", n, total);
$3 = 250
1 ticket(s) : 250 FCFA

Breakpoint 1, main () at v_tickets.c:8
8          total = total + prix;
$4 = 2
n = 2
prix = 250
total = 250
#0  main () at v_tickets.c:8

Lecture. Arrêt avant la ligne 8, à chaque tour ; au 2e arrêt, n vaut 2 et total 250 : la ligne 8 n’est pas encore exécutée.

Une lecture hors du bloc, deux outils

int main(void)
{
    int *notes = malloc(4 * sizeof(int));
    for (int i = 0; i < 4; i++)
        notes[i] = 10 + i;
    int somme = 0;
    for (int i = 0; i <= 4; i++)
        somme = somme + notes[i];
    printf("somme = %d\n", somme);
    free(notes);
    return 0;
}

Résultat \(\rightarrow\)

somme = 46

Sortie d’erreur sous valgrind (extrait) \(\rightarrow\)

Invalid read of size 4
   at main (s7_hors.c:11)
 Address 0x4a7e050 is 0 bytes after a block of size 16 alloc'd
ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Sortie d’erreur avec -fsanitize=address (extrait) \(\rightarrow\)

ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000020
READ of size 4 at 0x502000000020 thread T0
    #0 main s7_hors.c:11
SUMMARY: AddressSanitizer: heap-buffer-overflow s7_hors.c:11 in main

Lecture. La seconde boucle lit notes[4], après le bloc de 16 octets : comportement indéfini. Sans outil, 46, qui semble juste. valgrind (sans recompiler) signale la lecture ; AddressSanitizer (-fsanitize=address) arrête le programme ligne 11.

Automatiser la compilation : make

Makefile

CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -O0 -g

all: bonjour

bonjour: bonjour.c
    $(CC) $(CFLAGS) -o bonjour bonjour.c

clean:
    rm -f bonjour

.PHONY: all clean

Terminal \(\rightarrow\)

$ make
gcc -std=c17 -Wall -Wextra -O0 -g -o bonjour bonjour.c
$ make
make: Nothing to be done for 'all'.
$ touch bonjour.c
$ make
gcc -std=c17 -Wall -Wextra -O0 -g -o bonjour bonjour.c
$ make clean
rm -f bonjour

Lecture. Une règle dit cible: dépendances, puis la commande, précédée d’une tabulation (\(\rightarrow\)). make compare les dates : bonjour plus récent que bonjour.c, rien à refaire ; après touch, la source est plus récente, la commande est relancée. .PHONY : all et clean ne sont pas des fichiers.

Exercice : ce qu’affiche ce programme

int main(void)
{
    int stock = 12;
    int jour = 0;
    while (stock > 0) {
        jour++;
        stock = stock - 5;
        printf("jour %d : %d\n", jour, stock);
    }
    printf("rupture : jour %d\n", jour);
    return 0;
}

Énoncé

  1. jour et stock à la fin de chaque tour.
  2. Le nombre de tours, et la raison de l’arrêt.
  3. Les lignes affichées.
  4. La dernière ligne si stock part de 10.

Correction

  1. tour jour stock stock > 0
    1 1 7 vrai
    2 2 2 vrai
    3 3 \(-3\) faux
  2. 3 tours : le test précède chaque tour ; après le 3e, stock \(=-3\), le test est faux.
  3. Ci-dessous : une ligne par tour, puis la ligne finale.
  4. \(10 \rightarrow 5 \rightarrow 0\) : 0 > 0 est faux après le 2e tour, « rupture : jour 2 ».

Résultat \(\rightarrow\)

jour 1 : 7
jour 2 : 2
jour 3 : -3
rupture : jour 3

Exercice : quel outil ?

Énoncé

  1. Voir la valeur de total au 3e tour d’une boucle.
  2. Savoir si une fonction peut se terminer sans return.
  3. Un programme lit après la fin d’un bloc alloué, sans s’arrêter.
  4. Un fichier modifié sur dix : ne recompiler que ce qui en dépend.
  5. Savoir si printf est défini dans un .o ou attendu d’ailleurs.
  6. Retrouver les mêmes adresses d’une exécution à l’autre.

Correction

  1. gdb : compiler avec -g, puis break, run, continue, print total.
  2. gcc -Wall : avertissement -Wreturn-type, « control reaches end of non-void function ».
  3. valgrind ./prog (sans recompiler), ou -fsanitize=address (à la compilation).
  4. make, qui compare les dates des cibles et de leurs dépendances.
  5. nm fichier.o : U printf, utilisé mais non défini.
  6. setarch -R ./prog, dans un conteneur lancé avec --security-opt seccomp=unconfined.

Synthèse

À retenir

À retenir

  1. Le C est le langage des noyaux, de l’embarqué et des interpréteurs ; il ne vérifie ni les indices ni la durée de vie des blocs : les erreurs de sécurité mémoire forment environ 70 % des failles graves relevées par Microsoft et Chromium (code C et C++).
  2. Né avec UNIX (1972-1973), normalisé en C89, C99, C11, C17 et C23 ; le cours suit C17 (-std=c17, __STDC_VERSION__ \(=\) 201710L).
  3. Sept séances : introduction et outils, syntaxe, représentation des nombres, mémoire, structures, allocation dynamique, pointeurs de fonctions.
  4. gcc enchaîne préprocesseur (.i), compilateur (.s), assembleur (.o) et éditeur de liens (exécutable) ; chaque phase a ses erreurs, et certaines erreurs n’apparaissent qu’à l’exécution.
  5. Options du cours : -std=c17 -Wall -Wextra -O0 -g ; un avertissement se lit et se corrige.
  6. gdb arrête le programme et lit ses variables ; valgrind et AddressSanitizer voient les accès mémoire invalides qu’une exécution normale cache ; make ne recompile que ce qui a changé.
  7. Le conteneur se lance avec -v "$(pwd)":/workspace et --security-opt seccomp=unconfined.

Prochaine séance

Séance 1 : syntaxe C complète (fondamentaux)

La syntaxe du C, instruction par instruction, avec ce que chaque ligne fait des variables : types et tailles sur la machine du cours, conversions implicites, opérateurs et priorités, contrôle, fonctions et passage par valeur, tableaux et chaînes, printf et scanf ; puis les phases de la compilation, reprises avec la compilation séparée et l’édition de liens, et les comportements que la norme ne définit pas.

À préparer

Le TP-1 : Docker installé, conteneur du cours lancé sur le dossier de travail, verifie_env.sh sans échec, un premier programme compilé avec les options du cours, une session gdb, un rapport de valgrind lu.

Références

  • R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., Pearson, 2016 : § 1.1 à 1.4 (programme, système de compilation, exécution), chap. 7 (édition de liens).
  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2e éd., Prentice Hall, 1988 : préface, introduction, chap. 1.
  • K. N. King, C Programming: A Modern Approach, 2e éd., Norton, 2008 : chap. 1 (histoire, forces et faiblesses du C), chap. 2 (compilation et édition de liens).
  • D. M. Ritchie, « The Development of the C Language », History of Programming Languages II, ACM, 1993.
  • ISO/IEC 9899:2018 (C17) : 5.1.1.2 (phases de traduction), 6.10.8.1 (__STDC_VERSION__) ; ISO/IEC 9899:2024 (C23) ; Rationale for International Standard — C, rév. 5.10, 2003.
  • Université de Washington : CSE 351, The Hardware/Software Interface ; CSE 333, Systems Programming ; documentation de Docker, docs.docker.com.
  • M. Miller, « Trends, challenges, and strategic shifts in the software vulnerability mitigation landscape », BlueHat IL, Microsoft, 2019 ; The Chromium Projects, « Memory safety », 2020.

Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, gdb 15.1, valgrind 3.22.0, make 4.3 ; environnement : image reconstruite à partir du Dockerfile du cours.