TP — Séance 1 — Syntaxe C complète (fondamentaux)
Phases de la compilation, compilation séparée, types sur machine, débogage sous gdb, entrées et comportements indéfinis
Observer sur machine ce que la séance a décrit : les fichiers produits par chaque phase de la compilation, les symboles d’un fichier objet, les erreurs du compilateur et celles de l’éditeur de liens, les tailles des types, puis trouver sous gdb trois erreurs d’un programme, et rendre fiable la lecture d’une entrée. Le TP compte cinq étapes ; le dossier TP_S01_code_depart/ fournit les programmes, un Makefile et, dans tests/, les sorties attendues. Les programmes contiennent des TODO à compléter et des erreurs volontaires, annoncées. Les questions demandent d’observer une sortie, de l’expliquer avec le cours, puis de modifier le programme. Les réponses se notent dans reponses.txt, avec les sorties copiées.
Décompresser TP_S01_code_depart.zip, se placer dans le dossier TP_S01_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 : TP-1, étape 2. Dans le conteneur, ce dossier est /workspace :
make # compile tout (gcc -std=c17 -Wall -Wextra -O0 -g)
make test # etapes 2, 4 et 5 comparees a tests/attendu_*.txt
make phases # fichiers .i, .s, .o de l'etape 1 (et .s en -O2)
make asan # etape4_releves_asan (-fsanitize=address)
make ubsan # etape5_saisie_ubsan (-fsanitize=undefined)
make clean # retirer les executables et les fichiers produits
nm fichier.o # table des symboles
gdb -q ./prog # break, run, print, next, step, watch, backtrace, continue
Environnement : l’image Docker du cours (elbachirtoure/c-advanced-course:v2.0), qui fournit gcc, gdb et valgrind. Les adresses affichées diffèrent d’une machine à l’autre ; les questions portent sur les valeurs, les symboles et les messages.
Étape 1 : les phases de la compilation
etape1_phases.c convertit des montants en FCFA en euros à l’aide de deux macros : 1 euro vaut 655,957 FCFA, pris ici égal à 656 ; la conversion est une division entière (troncature).
/* etape1_phases.c -- TP Seance 1, etape 1 : les phases de la compilation */
#include <stdio.h>
#define FCFA_PAR_EURO 656
#define EN_EUROS(x) ((x) / FCFA_PAR_EURO)
int base_fcfa = 32800;
int conversions = 0;
int dernier;
int convertir(int fcfa)
{
conversions++;
dernier = EN_EUROS(fcfa);
return dernier;
}
int main(void)
{
int total = 0;
for (int k = 1; k <= 4; k++)
total += convertir(k * base_fcfa);
printf("%d conversions, %d euros\n", conversions, total);
return 0;
}- Observer.
make phases, puiswc -l etape1_phases.ietgrep -n 'dernier = ' etape1_phases.i. Combien de lignes le fichier préprocessé compte-t-il, et d’où viennent-elles ? Reste-t-il des directives ? Les nomsFCFA_PAR_EUROetEN_EUROSy figurent-ils encore ? Que devient la ligne 11 ? - Observer.
nm etape1_phases.o. Que signifient les lettresT,D,BetU? Pourquoibase_fcfaetconversions, toutes deux initialisées, n’ont-elles pas la même lettre ? - Expliquer. Compiler et lier (
make), puisnm etape1_phases: qu’est devenu le symboleU printf? Que sont les nouvelles valeurs placées devantmainetconvertir? - Observer. Compter les instructions
calldansetape1_phases.set dansetape1_phases_O2.s(grep -c call), et leurs lignes (wc -l). Quel appel a disparu en-O2, et pourquoi le programme fonctionne-t-il encore ? - Modifier. Ajouter
staticdevant la définition deconvertir, recompiler avec-cet relirenm: quelle lettre change, et que signifie ce changement pour un autre fichier du programme ?
Étape 2 : compilation séparée et erreurs
Le module frais.c calcule les frais d’un transfert d’argent par mobile ; frais.h le déclare, etape2_transferts.c l’utilise. Le Makefile compile chaque .c en .o (-c), puis lie les deux fichiers objets.
/* frais.h -- TP Seance 1, etape 2 : declarations du module frais.c */
#ifndef FRAIS_H
#define FRAIS_H
extern int nb_calculs;
long frais_transfert(long montant);
#endif/* etape2_transferts.c -- TP Seance 1, etape 2 : programme principal */
#include <stdio.h>
#include "frais.h"
int main(void)
{
long montants[4] = {3000, 12500, 48000, 150000};
for (int i = 0; i < 4; i++)
printf("%7ld FCFA : frais %ld\n", montants[i], frais_transfert(montants[i]));
printf("%d calculs\n", nb_calculs);
return 0;
}/* frais.c -- TP Seance 1, etape 2 : frais d'un transfert par mobile (FCFA) */
#include "frais.h"
int nb_calculs = 0;
long frais_transfert(long montant)
{
nb_calculs++;
/* TODO 2.1 : jusqu'a 5000 FCFA, frais fixes de 50 ;
jusqu'a 20000, 1 % du montant ;
au-dela, 200 plus 0,5 % du montant (calculs entiers) */
return 0;
}- Observer.
make etape2_transferts, puisnm frais.oetnm etape2_transferts.o. Pourquoifrais_transfertetnb_calculssont-ilsUdansetape2_transferts.o, alors que ce fichier se compile sans erreur ? Quel rôle joueexterndansfrais.h? - Modifier. Compléter le TODO 2.1 (bareme : 50 FCFA jusqu’à 5000 ; 1 % jusqu’à 20000 ; au-delà, 200 plus 0,5 %, en calcul entier). Vérifier avec
make test. Pourquoi l’avertissementunused parameterdu départ a-t-il disparu ? - Observer. Provoquer trois erreurs, l’une après l’autre, en notant à chaque fois le programme qui la signale (compilateur ou éditeur de liens) et le message : retirer le
;denb_calculs++;dansfrais.c; lieretape2_transferts.oseul (gcc etape2_transferts.o -o etape2_transferts) ; ajouterint nb_calculs = 7;après les#includedeetape2_transferts.c. - Expliquer. À quoi servent les lignes
#ifndef,#defineet#endifdefrais.h? Que se passerait-il, sans elles, si un fichier incluaitfrais.hdeux fois ?
Étape 3 : les types sur la machine
/* etape3_types.c -- TP Seance 1, etape 3 : tailles, signe de char */
#include <stdio.h>
#include <string.h>
#include <limits.h>
int main(void)
{
printf("char %zu octet(s), de %d a %d\n", sizeof(char), CHAR_MIN, CHAR_MAX);
printf("int %zu octet(s), de %d a %d\n", sizeof(int), INT_MIN, INT_MAX);
/* TODO 3.1 : afficher de meme short, long (LONG_MIN, LONG_MAX),
unsigned int (UINT_MAX), long long, float, double et size_t */
char c = 233;
printf("c = %d\n", c);
int solde = -2500;
size_t lg = strlen("Kanel");
/* erreur volontaire : comparaison d'un int et d'un size_t */
if (solde < lg)
printf("solde inferieur a %zu\n", lg);
else
printf("solde superieur ou egal a %zu\n", lg);
return 0;
}- Modifier. Compléter le TODO 3.1, puis comparer les tailles obtenues à celles du cours. Quelle taille aurait
longsous Windows 64 bits ? - Observer. Quelle valeur prend
c? Recompiler avec-funsigned-charet comparer. Pourquoi l’écart entre les deux valeurs est-il de 256 ? - Expliquer. Le programme contient une erreur volontaire : le test de la ligne 16 conclut que \(-2500\) est supérieur ou égal à 5. Lire l’avertissement de gcc et expliquer la conversion qui a lieu.
- Modifier. Corriger la comparaison, sans supprimer l’avertissement par une option, et vérifier que gcc ne dit plus rien.
Étape 4 : trois erreurs, sous gdb
etape4_releves.c cherche le plus grand de quatre soldes de comptes (tous négatifs, en FCFA) et la moyenne de cinq notes. Il contient trois erreurs volontaires : les résultats attendus sont \(-300\) et 12,80 (tests/attendu_etape4.txt).
/* etape4_releves.c -- TP Seance 1, etape 4 : trois erreurs, a trouver sous gdb */
#include <stdio.h>
int plus_grand(int tab[], int n)
{
int max = 0;
for (int i = 0; i <= n; i++) {
if (tab[i] > max)
max = tab[i];
}
return max;
}
double moyenne(int tab[], int n)
{
int somme = 0;
for (int i = 0; i < n; i++)
somme += tab[i];
return somme / n;
}
int main(void)
{
int soldes[4] = {-1500, -300, -4200, -800};
int notes[5] = {13, 16, 9, 15, 11};
printf("plus grand solde : %d\n", plus_grand(soldes, 4));
printf("moyenne : %.2f\n", moyenne(notes, 5));
return 0;
}- Observer. Exécuter le programme : quelles valeurs donne-t-il ?
- Observer. Sous gdb :
break 7 if i == 4,run,print i,print tab[i],print max,backtrace. Quelle case littab[i]pouri == 4? D’où vient la valeur affichée ? - Observer. Puis
delete,break 17,continue,print somme,print somme / n,print (double) somme / n. Qu’explique la différence entre les deux dernières valeurs ? - Observer.
make asanet exécuteretape4_releves_asan: quelle erreur est signalée, à quelle ligne, dans quelle variable ? - Modifier. Corriger les trois erreurs (l’initialisation de
max, la borne de la boucle, la division), puis vérifier avecmake testet sous AddressSanitizer. Avec la correctionmax = tab[0], vérifier sous gdb (watch max) quemaxne change que deux fois.
Étape 5 : lecture d’une entrée sans comportement indéfini
etape5_saisie.c lit des montants sur l’entrée standard et en fait le total. Le fichier tests/entree5.txt contient quatre lignes : 1500000000, 800000000, 250 et xyz.
/* etape5_saisie.c -- TP Seance 1, etape 5 : lecture de montants, total */
#include <stdio.h>
int main(void)
{
int m = 0;
int total = 0;
int n = 0;
/* erreur volontaire : resultat de scanf non teste, total dans un int */
for (int k = 0; k < 4; k++) {
scanf("%d", &m);
total += m;
n++;
}
/* TODO 5.1 : lire jusqu'a la fin de l'entree ou une valeur invalide,
en testant le resultat de scanf ; signaler une entree invalide ;
cumuler dans un long long */
printf("%d montants, total %d\n", n, total);
return 0;
}- Observer.
./etape5_saisie < tests/entree5.txt. Combien de montants sont annoncés, et quel total ? Lequel est compté deux fois, et pourquoi (que faitscanfdevant « xyz ») ? - Observer.
make ubsan, puis la même commande avecetape5_saisie_ubsan. Quelle opération est signalée, et pourquoi est-ce un comportement indéfini ? - Modifier. Compléter le TODO 5.1 : lire tant que
scanfrend 1, signaler une entrée invalide quand il rend 0, cumuler dans unlong long(format%lld). Vérifier avecmake test, puis sous-fsanitize=undefined. - Modifier. Vérifier le comportement sur une entrée sans erreur, terminée par la fin du fichier :
printf '250 1200 75\n' | ./etape5_saisie. Que rendscanfau dernier appel ?
Pour cette séance
Le cours
Les slides
À lire en ligne
À télécharger
- Code de départ du TP (ZIP, 5 Ko)