TP-1 — Séance 0 — Mise en place de l’environnement

Docker, conteneur du cours, éditeur, gcc, gdb, valgrind, AddressSanitizer, make

TP de la séance 0 de Programmation C avancée : pourquoi le C, histoire, plan des séances, environnement, chaîne de compilation, premier programme.
Auteur

Dr. El Hadji Bassirou TOURÉ · DMI/FST/UCAD

NoteObjectif et organisation

Installer l’environnement de travail de tout le cours et vérifier chacun de ses outils : Docker, l’image du cours, le conteneur lancé sur le dossier de travail, l’édition d’un fichier, la compilation avec les options du cours, gdb, valgrind, AddressSanitizer et make. Le TP compte sept étapes (2 à 3 heures). Le dossier TP_S00_code_depart/ fournit verifie_env.sh (contrôle de l’environnement), adresse.c, test_env.c, un Makefile à compléter et, pour VS Code, .devcontainer/devcontainer.json ; hello.c et calcul.c s’écrivent pendant le TP. Les questions demandent d’observer une sortie, de l’expliquer, puis de modifier un fichier ; les réponses se notent dans reponses.txt, avec les sorties copiées. Machine : processeur 64 bits (Intel, AMD ou Apple), virtualisation activée, 4 Gio de mémoire au moins (8 Gio sous Windows, valeur demandée par Docker Desktop), quelques Gio libres sur le disque (l’image occupe environ 1,1 Gio).

ImportantCommandes

Décompresser TP_S00_code_depart.zip, se placer dans le dossier TP_S00_code_depart/ qu’il crée, puis lancer le conteneur depuis ce dossier :

docker run -it --rm --security-opt seccomp=unconfined \
    -v "$(pwd)":/workspace elbachirtoure/c-advanced-course:v2.0     # lancer le conteneur

Windows, PowerShell : -v "${PWD}:/workspace" à la place de -v "$(pwd)":/workspace ; Git Bash et autres cas : étape 2 ci-dessous. Dans le conteneur, ce dossier est /workspace :

gcc -std=c17 -Wall -Wextra -O0 -g prog.c -o prog          # options du cours
gdb ./prog                                                # break, run, next, print, continue, quit
valgrind --leak-check=full ./prog                         # erreurs memoire et fuites
gcc -std=c17 -Wall -Wextra -O0 -g -fsanitize=address prog.c -o prog_asan
setarch -R ./prog                                         # sans alea des adresses
make ; make clean                                         # compilation automatisee

Étape 1 : installer Docker

Docker exécute des conteneurs : des systèmes Linux isolés, construits à partir d’une image. Sous Windows et macOS, Docker Desktop fait tourner ces conteneurs dans une machine virtuelle Linux légère ; sous Linux, Docker Engine les exécute directement. Les versions et les menus évoluent : en cas de doute, la référence est la page d’installation de docs.docker.com.

Windows

Système : Windows 11, 64 bits, version 23H2 (build 22631) ou plus récente (Windows 10 n’est plus maintenu par Microsoft) ; WSL en version 2.1.5 ou plus récente ; virtualisation matérielle activée dans le BIOS ou l’UEFI (VT-x chez Intel, AMD-V chez AMD).

  1. Ouvrir PowerShell en administrateur, installer WSL 2, puis redémarrer :

    wsl --install

    Si WSL est déjà installé : wsl --update, puis wsl --set-default-version 2.

  2. Télécharger Docker Desktop Installer.exe depuis docs.docker.com (Docker Desktop pour Windows), l’exécuter, et laisser cochée l’option Use WSL 2 instead of Hyper-V.

  3. Lancer Docker Desktop depuis le menu Démarrer (il ne démarre pas seul après l’installation) et accepter les conditions d’utilisation.

macOS

Docker Desktop accepte la version courante de macOS et les deux précédentes, avec 4 Gio de mémoire au moins.

  1. Télécharger Docker.dmg depuis docs.docker.com, dans la version qui correspond au processeur : Apple silicon (puces M1, M2, M3…) ou Intel (menu Pomme, À propos de ce Mac).
  2. Sur un Mac à puce Apple, installer Rosetta 2, recommandé par Docker : softwareupdate --install-rosetta.
  3. Ouvrir Docker.dmg, glisser Docker dans Applications, lancer Docker.app et accepter les conditions d’utilisation.

Linux (Ubuntu 22.04, 24.04 ou 26.04)

# 1. retirer les paquets non officiels qui entreraient en conflit
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc | cut -f1)
# 2. ajouter le depot de Docker
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
sudo apt update
# 3. installer Docker Engine
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 4. utiliser docker sans sudo (prend effet a la prochaine connexion, ou tout de suite avec newgrp)
sudo usermod -aG docker $USER
newgrp docker

Sous Debian, remplacer ubuntu par debian dans les deux adresses et ${UBUNTU_CODENAME:-$VERSION_CODENAME} par $VERSION_CODENAME. Pour les autres distributions (Fedora, Arch…), suivre la page d’installation de Docker Engine.

Vérification (les trois systèmes)

docker --version
docker run hello-world
  1. Observer. Quelle version de Docker est installée ? Le message de hello-world commence par « Hello from Docker! » : quelles étapes y sont décrites, et quelles lignes précèdent ce message lors de la première exécution ?
  2. Expliquer. Pourquoi Docker a-t-il besoin de WSL 2 sous Windows, ou d’une machine virtuelle sous macOS, pour exécuter un conteneur Linux ?

Étape 2 : récupérer l’image et lancer le conteneur

docker pull elbachirtoure/c-advanced-course:v2.0
docker images

Créer sur l’hôte un dossier de travail (par exemple tp_c), y décompresser TP_S00_code_depart.zip, qui crée tp_c/TP_S00_code_depart/, se placer dans ce dossier (cd tp_c/TP_S00_code_depart), puis lancer le conteneur depuis ce dossier :

terminal commande
Linux, macOS, WSL docker run -it --rm --security-opt seccomp=unconfined -v "$(pwd)":/workspace elbachirtoure/c-advanced-course:v2.0
Windows, PowerShell docker run -it --rm --security-opt seccomp=unconfined -v "${PWD}:/workspace" elbachirtoure/c-advanced-course:v2.0
Windows, Git Bash docker run -it --rm --security-opt seccomp=unconfined -v "/${PWD}":/workspace elbachirtoure/c-advanced-course:v2.0

-it ouvre un terminal interactif ; --rm supprime le conteneur quand son shell se termine ; -v monte le dossier courant de l’hôte sous /workspace ; --security-opt seccomp=unconfined autorise la désactivation de l’aléa des adresses (setarch -R, gdb), dont les séances suivantes ont besoin. Les guillemets protègent un chemin qui contient des espaces. Sous Windows, PowerShell (ou le Terminal Windows) convient ; dans un terminal Ubuntu de WSL, la commande Linux convient. Sous Git Bash, la barre oblique placée devant ${PWD} empêche la conversion automatique des chemins (sans elle, /workspace reste vide) ; si le message « the input device is not a TTY » apparaît, préfixer la commande par winpty.

Dans le conteneur :

pwd
ls
./verifie_env.sh
help-tp
  1. Observer. Dans quel dossier le shell démarre-t-il ? Quels fichiers voit ls ? Que dit verifie_env.sh : versions de gcc, gdb, valgrind et make, norme par défaut de gcc, setarch -R, -fsanitize=address ?
  2. Observer. Dans le conteneur, écrire echo essai > /workspace/essai.txt et echo essai > /root/essai.txt. Le premier fichier est-il visible sur l’hôte ? Quitter (exit), relancer le conteneur, et chercher les deux fichiers. Expliquer ce qui reste et ce qui disparaît.
  3. Expliquer. Relancer le conteneur sans --security-opt seccomp=unconfined et exécuter ./verifie_env.sh. Quelle ligne change ?
  4. Observer (Linux). Sur l’hôte, ls -l essai.txt : à qui appartient le fichier créé depuis le conteneur, et quelle conséquence pour le modifier ou le supprimer sur l’hôte ?

Étape 3 : éditer un fichier

L’éditeur de référence du cours est Vim : il est présent dans le conteneur et sur tout système Unix, y compris un serveur sans interface graphique. Vim a des modes : le mode NORMAL (commandes), dans lequel il s’ouvre, et le mode INSERT (saisie), qu’ouvre la touche i ; Échap revient toujours au mode NORMAL. Le dossier étant monté, les fichiers peuvent aussi être édités sur l’hôte (VS Code, par exemple) puis compilés dans le conteneur ; avec l’extension Dev Containers, VS Code ouvert sur le dossier TP_S00_code_depart y trouve .devcontainer/devcontainer.json et propose de s’ouvrir directement dans le conteneur (Reopen in Container).

3.1 Premiers pas

vim hello.c, puis i, saisir le programme ci-dessous, Échap, puis :wq et Entrée (enregistrer et quitter) ; vérifier avec cat hello.c.

#include <stdio.h>

int main(void)
{
    printf("Hello World\n");
    return 0;
}

3.2 Édition et navigation

Rouvrir hello.c. En mode NORMAL : j, k, h, l déplacent le curseur, gg et G vont au début et à la fin. Sur la ligne du printf, o ouvre une ligne en dessous (mode INSERT) : y écrire printf("Vim works!\n");, puis Échap. yy copie une ligne, p la colle, dd la supprime, u annule, Ctrl+r refait ; gg=G réindente tout le fichier. Enregistrer (:wq).

3.3 Recherche et remplacement

Créer calcul.c :

#include <stdio.h>

int main(void)
{
    int x = 10;
    int y = 10;
    int z = x + y;
    printf("Result: %d\n", z);
    return 0;
}

Rouvrir le fichier : /int puis Entrée cherche « int », n et N passent à l’occurrence suivante ou précédente ; :%s/10/20/g remplace tous les 10 par 20. Enregistrer.

i, a, o, O insérer avant, après, ligne dessous, dessus /motif, ?motif chercher vers le bas, vers le haut
Échap retour au mode NORMAL n, N occurrence suivante, précédente
h j k l, w, b gauche, bas, haut, droite ; mot suivant, précédent :s/a/b/, :%s/a/b/g remplacer sur la ligne, dans le fichier
gg, G, :42 début, fin, ligne 42 :w, :q, :wq enregistrer, quitter, les deux
x, dd, dw supprimer caractère, ligne, mot :q! quitter sans enregistrer
yy, p, P copier la ligne, coller après, avant >>, <<, gg=G indenter, désindenter, tout réindenter
u, Ctrl+r annuler, refaire % accolade ou parenthèse correspondante

En cas de blocage : Échap plusieurs fois, puis :q!. Le tutoriel vimtutor (25 à 30 minutes) reprend ces commandes.

  1. Observer. Copier dans reponses.txt le contenu final de hello.c (deux printf) et celui de calcul.c.
  2. Observer. Vim n’affiche rien après :%s/10/20/g : il n’annonce un changement qu’au-delà de deux lignes modifiées (option report). Annuler (u), taper :set report=0, refaire le remplacement : que dit Vim ?

Étape 4 : compiler et exécuter

gcc -std=c17 -Wall -Wextra -O0 -g hello.c -o hello
./hello
echo $?
gcc -std=c17 -Wall -Wextra -O0 -g calcul.c -o calcul && ./calcul
gcc hello.c
ls
  1. Observer. Que contient la sortie de hello et de calcul ? Que vaut $? après ./hello ? Quel fichier gcc hello.c a-t-il produit ?
  2. Expliquer. Le rôle de chacune des options -std=c17, -Wall, -Wextra, -O0, -g et -o.
  3. Modifier. Remplacer return 0; par return 3; dans hello.c, recompiler, exécuter, et relire $?, puis remettre return 0;. Que représente cette valeur ?
  4. Modifier. Ajouter int inutile; au début de main et recompiler avec les options du cours : quel message apparaît ? Le retirer ensuite.

Étape 5 : gdb, valgrind, adresses

gcc -g hello.c -o hello_debug
gdb ./hello_debug
(gdb) break main
(gdb) run
(gdb) next
(gdb) print "GDB works!"
(gdb) quit

Dans l’image du cours, gdb affiche à chaque arrêt le tableau de bord de gdb-dashboard (sections Assembly, Registers, Source, Stack, Variables) ; dashboard -enabled off le masque, dashboard -enabled on le rétablit.

valgrind ./hello
gcc -std=c17 -Wall -Wextra -O0 -g adresse.c -o adresse
./adresse
./adresse
setarch -R ./adresse
setarch -R ./adresse
/* adresse.c -- adresse d'une variable locale (TP-1, etape 5) */
#include <stdio.h>

int main(void)
{
    int x = 2026;
    printf("x = %d, adresse de x : %p\n", x, (void *)&x);
    return 0;
}
  1. Observer. Sur quelle ligne gdb s’arrête-t-il après break main et run, puis après next ? Qu’affiche print "GDB works!" ?
  2. Observer. Dans le rapport de valgrind, que disent les lignes HEAP SUMMARY, total heap usage et ERROR SUMMARY ? D’où vient le bloc alloué, alors que hello.c n’appelle pas malloc ?
  3. Expliquer. Pourquoi l’adresse de x change-t-elle d’une exécution à l’autre, et pas sous setarch -R ?

Étape 6 : automatiser avec make

# Makefile -- TP-1 : compilation de hello et de test_env avec les options du cours
CC = gcc
CFLAGS = -std=c17 -Wall -Wextra -O0 -g
LDFLAGS =

# Cibles
TARGETS = hello test_env

# Regle par defaut
all: $(TARGETS)

# Compilation de hello
hello: hello.c
    $(CC) $(CFLAGS) -o hello hello.c $(LDFLAGS)

# TODO 6.1 : regle pour test_env, sur le modele de hello

# Nettoyage
clean:
    rm -f $(TARGETS) *.o

# Recompilation complete
rebuild: clean all

# Cibles qui ne sont pas des fichiers
.PHONY: all clean rebuild
  1. Observer. Avant de compléter le TODO, lancer make : test_env est compilé quand même. Quelle commande make a-t-il lancée pour lui, et d’où vient-elle ?
  2. Modifier. Compléter le TODO 6.1 par une règle sur le modèle de celle de hello, puis make clean et make.
  3. Observer. Lancer make une seconde fois, puis touch hello.c et make, enfin make clean et make rebuild. Qu’est-ce qui est recompilé à chaque fois, et pourquoi ?
  4. Observer. Remplacer la tabulation de la commande de hello par des espaces et relancer make. Quel message apparaît ? Remettre la tabulation.

Étape 7 : valider l’installation

/* test_env.c -- verification de l'environnement du cours (TP-1, etape 7) */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    printf("=== Test de l'environnement ===\n");

    /* Test 1 : compilation */
    printf("[OK] Compilation reussie\n");

    /* Test 2 : allocation dynamique */
    int *ptr = malloc(10 * sizeof(int));
    if (ptr != NULL) {
        printf("[OK] malloc fonctionne\n");
        free(ptr);
    }

    /* Test 3 : arithmetique */
    int result = 42 + 27;
    if (result == 69) {
        printf("[OK] Arithmetique correcte\n");
    }

    printf("\nEnvironnement pret pour le cours !\n");
    return 0;
}
gcc -std=c17 -Wall -Wextra -O0 -g test_env.c -o test_env
./test_env
valgrind --leak-check=full ./test_env
  1. Observer. Les trois tests affichent-ils [OK] ? valgrind signale-t-il une fuite ?
  2. Modifier. Copier test_env.c dans test_env_fuite.c et y supprimer la ligne free(ptr);. Compiler, puis lancer valgrind avec --leak-check=full : combien d’octets sont perdus, dans combien de blocs, alloués à quelle ligne ?
  3. Observer. Compiler test_env_fuite.c avec -fsanitize=address (exécutable fuite_asan) et l’exécuter : que signale AddressSanitizer, et avec quel statut le programme se termine-t-il (echo $?) ?
  4. Expliquer. Lancer valgrind ./fuite_asan, puis gdb ./fuite_asan et run. Que se passe-t-il dans chaque cas, et qu’en conclure sur l’usage de ces outils ?
NotePoints clés
  • Lancer le conteneur depuis le dossier de travail, avec -v "$(pwd)":/workspace et --security-opt seccomp=unconfined ; seuls les fichiers de /workspace survivent à exit.
  • Éditer avec Vim dans le conteneur, ou sur l’hôte ; compiler dans le conteneur avec -std=c17 -Wall -Wextra -O0 -g.
  • make recompile ce qui a changé ; gdb arrête et inspecte ; valgrind et AddressSanitizer surveillent la mémoire, un outil à la fois.

Pour cette séance

Le cours

Les slides

À télécharger

Retour au sommet