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

TP de la séance 1 de Programmation C avancée : types, variables, opérateurs, contrôle, fonctions, tableaux, entrées-sorties, compilation.
Auteur

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

NoteObjectif et organisation

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.

ImportantCommandes

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;
}
  1. Observer. make phases, puis wc -l etape1_phases.i et grep -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 noms FCFA_PAR_EURO et EN_EUROS y figurent-ils encore ? Que devient la ligne 11 ?
  2. Observer. nm etape1_phases.o. Que signifient les lettres T, D, B et U ? Pourquoi base_fcfa et conversions, toutes deux initialisées, n’ont-elles pas la même lettre ?
  3. Expliquer. Compiler et lier (make), puis nm etape1_phases : qu’est devenu le symbole U printf ? Que sont les nouvelles valeurs placées devant main et convertir ?
  4. Observer. Compter les instructions call dans etape1_phases.s et dans etape1_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 ?
  5. Modifier. Ajouter static devant la définition de convertir, recompiler avec -c et relire nm : 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;
}
  1. Observer. make etape2_transferts, puis nm frais.o et nm etape2_transferts.o. Pourquoi frais_transfert et nb_calculs sont-ils U dans etape2_transferts.o, alors que ce fichier se compile sans erreur ? Quel rôle joue extern dans frais.h ?
  2. 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’avertissement unused parameter du départ a-t-il disparu ?
  3. 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 ; de nb_calculs++; dans frais.c ; lier etape2_transferts.o seul (gcc etape2_transferts.o -o etape2_transferts) ; ajouter int nb_calculs = 7; après les #include de etape2_transferts.c.
  4. Expliquer. À quoi servent les lignes #ifndef, #define et #endif de frais.h ? Que se passerait-il, sans elles, si un fichier incluait frais.h deux 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;
}
  1. Modifier. Compléter le TODO 3.1, puis comparer les tailles obtenues à celles du cours. Quelle taille aurait long sous Windows 64 bits ?
  2. Observer. Quelle valeur prend c ? Recompiler avec -funsigned-char et comparer. Pourquoi l’écart entre les deux valeurs est-il de 256 ?
  3. 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.
  4. 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;
}
  1. Observer. Exécuter le programme : quelles valeurs donne-t-il ?
  2. Observer. Sous gdb : break 7 if i == 4, run, print i, print tab[i], print max, backtrace. Quelle case lit tab[i] pour i == 4 ? D’où vient la valeur affichée ?
  3. 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 ?
  4. Observer. make asan et exécuter etape4_releves_asan : quelle erreur est signalée, à quelle ligne, dans quelle variable ?
  5. Modifier. Corriger les trois erreurs (l’initialisation de max, la borne de la boucle, la division), puis vérifier avec make test et sous AddressSanitizer. Avec la correction max = tab[0], vérifier sous gdb (watch max) que max ne 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;
}
  1. Observer. ./etape5_saisie < tests/entree5.txt. Combien de montants sont annoncés, et quel total ? Lequel est compté deux fois, et pourquoi (que fait scanf devant « xyz ») ?
  2. Observer. make ubsan, puis la même commande avec etape5_saisie_ubsan. Quelle opération est signalée, et pourquoi est-ce un comportement indéfini ?
  3. Modifier. Compléter le TODO 5.1 : lire tant que scanf rend 1, signaler une entrée invalide quand il rend 0, cumuler dans un long long (format %lld). Vérifier avec make test, puis sous -fsanitize=undefined.
  4. 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 rend scanf au dernier appel ?

Pour cette séance

Le cours

Les slides

À lire en ligne

À télécharger

Retour au sommet