TP — Séance 4 — Structures et unions

Observer la disposition des structures, les unions et les structures sur le tas

TP de la séance 4 de Programmation C avancée : champs, alignement, padding, unions et allocation.
Auteur

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

NoteObjectif et organisation

Vérifier sur machine ce que le cours décrit : les offsets et le padding d’une structure, sa disposition vue par gdb, le piège de memcmp, le motif IEEE 754 d’un float vu par une union, une union discriminée et une liste chaînée vérifiées sous valgrind. Le TP compte six étapes. Chaque étape fournit un programme du dossier TP_S04_code_depart/ (avec un Makefile), à compiler et exécuter ; les questions demandent d’observer une sortie, de l’expliquer avec le cours, puis de modifier le programme. Les réponses se notent dans un fichier reponses.txt, avec les sorties copiées.

ImportantCommandes

Décompresser TP_S04_code_depart.zip, se placer dans le dossier TP_S04_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 etape1_disposition   # un seul programme (apres une modification)
make -B prog              # recompiler meme a jour
make clean                # retirer les executables
setarch -R ./prog         # execution sans alea des adresses
gdb -q ./prog             # debogueur (break, run, print, x)
valgrind --leak-check=full ./prog   # fuites et acces invalides au tas

Les adresses affichées diffèrent d’une machine à l’autre ; les questions portent sur les offsets, les tailles et les octets, qui n’en dépendent pas sur x86-64.

Étape 1 : la disposition d’une structure

Le programme etape1_disposition.c remplit une struct Commande après avoir mis tous ses octets à ee : les octets de padding restent ee et se voient dans le dump.

/* etape1_disposition.c -- TP Seance 4, etape 1 : la disposition d'une structure en memoire.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape1_disposition.c -o etape1_disposition */
#include <stdio.h>
#include <stddef.h>
#include <string.h>

struct Commande {
    char statut;
    double montant;
    short quantite;
    char code[3];
    int client;
};

/* affiche n octets a partir de p, 8 par ligne, avec l'adresse en tete de ligne */
void vider(const void *p, size_t n)
{
    const unsigned char *o = p;
    for (size_t i = 0; i < n; i++) {
        if (i % 8 == 0)
            printf("%s%p :", i ? "\n" : "", (const void *)(o + i));
        printf(" %02x", o[i]);
    }
    printf("\n");
}

int main(void)
{
    struct Commande c;
    memset(&c, 0xee, sizeof c);           /* le padding sera visible : ee */
    c.statut = 'V';
    c.montant = 12500.0;
    c.quantite = 3;
    memcpy(c.code, "TBA", 3);
    c.client = 4021;

    printf("sizeof(struct Commande) = %zu\n", sizeof(struct Commande));
    printf("offsetof statut   = %zu\n", offsetof(struct Commande, statut));
    printf("offsetof montant  = %zu\n", offsetof(struct Commande, montant));
    /* TODO 1 : afficher les offsets de quantite, code et client */
    vider(&c, sizeof c);
    /* TODO 2 : declarer struct CommandeCompacte (memes champs, autre ordre),
                afficher sa taille et ses offsets */
    return 0;
}
  1. Observer. Avant d’exécuter, calculer à la main l’offset de chaque champ et sizeof(struct Commande). Compiler, exécuter, et comparer avec les deux offsets affichés et la taille.
  2. Modifier. Compléter le TODO 1 (offsets de quantite, code et client) et vérifier les prédictions.
  3. Expliquer. Dans le dump, repérer chaque champ et chaque octet ee. Justifier les octets de montant (12500.0 au format IEEE 754) et ceux de client (4021, little-endian).
  4. Modifier. TODO 2 : déclarer struct CommandeCompacte, avec les mêmes champs rangés par alignement décroissant, et afficher sa taille et ses offsets. Combien d’octets de padding reste-t-il ?

Étape 2 : une structure vue par gdb

/* etape2_gdb.c -- TP Seance 4, etape 2 : examiner une structure avec gdb.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape2_gdb.c -o etape2_gdb */
#include <stdio.h>
#include <stddef.h>

struct Capteur {
    char type;
    int valeur;
    short code;
    char etat;
};

int main(void)
{
    struct Capteur cap = {'P', 1013, 0x2b1, 'A'};
    struct Capteur *p = &cap;
    p->valeur = p->valeur + 2;
    printf("sizeof = %zu, valeur = %d\n", sizeof cap, cap.valeur);
    return 0;
}

Compiler avec -g, puis lancer gdb -q ./etape2_gdb et exécuter les commandes suivantes :

(gdb) break 17
(gdb) run
(gdb) ptype struct Capteur
(gdb) print cap
(gdb) print sizeof(cap)
(gdb) x/12xb &cap
(gdb) print &cap.valeur
(gdb) print &cap.code
(gdb) next
(gdb) print p->valeur
(gdb) print *p
  1. Observer. Copier la sortie de x/12xb &cap. Repérer type, valeur, code et etat, et les octets de padding.
  2. Expliquer. Retrouver l’offset de valeur et celui de code à partir des adresses affichées par print &cap.valeur et print &cap.code.
  3. Expliquer. Les octets de padding valent-ils 0 ? Que dit le cours de leur contenu ?
  4. Observer. Après next, quelle valeur prend p->valeur, et quels octets du dump ont changé (relancer x/12xb &cap) ?

Étape 3 : comparer des structures

Le programme etape3_comparer.c compare un relevé construit en mémoire et un relevé reçu sous forme de 12 octets. Il contient une erreur volontaire : la comparaison par memcmp.

/* etape3_comparer.c -- TP Seance 4, etape 3 : comparer des structures, mesurer le cout du padding.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape3_comparer.c -o etape3_comparer
   Erreur volontaire : la comparaison par memcmp ne donne pas le resultat attendu. */
#include <stdio.h>
#include <string.h>

struct Releve {
    char station;       /* initiale de la station */
    int pluie;          /* en dixiemes de mm */
    char qualite;
};

int egal_memcmp(const struct Releve *a, const struct Releve *b)
{
    return memcmp(a, b, sizeof *a) == 0;
}

/* TODO 1 : ecrire int egal_champs(const struct Releve *a, const struct Releve *b) */

int main(void)
{
    struct Releve a;
    memset(&a, 0x00, sizeof a);
    a.station = 'M';
    a.pluie = 1250;
    a.qualite = 'B';
    /* trame recue d'une autre machine : 12 octets, padding compris */
    unsigned char trame[12] = {0x4d, 0x3f, 0x3f, 0x3f, 0xe2, 0x04, 0x00, 0x00,
                               0x42, 0x3f, 0x3f, 0x3f};
    struct Releve b;
    memcpy(&b, trame, sizeof b);
    printf("egal_memcmp : %d\n", egal_memcmp(&a, &b));
    /* TODO 2 : afficher egal_champs(&a, &b) */
    /* TODO 3 : afficher la taille d'un tableau de 100000 struct Releve, puis celle d'une version
                reordonnee des champs */
    return 0;
}
  1. Observer. Compiler et exécuter. Les deux relevés ont-ils les mêmes champs ? Que répond egal_memcmp ?
  2. Expliquer. Situer, dans trame, les octets de padding de struct Releve. Pourquoi memcmp conclut-elle à une différence ?
  3. Modifier. TODO 1 et 2 : écrire egal_champs, qui compare champ par champ, et afficher son résultat.
  4. Modifier. TODO 3 : afficher la taille d’un tableau de 100 000 struct Releve, puis celle d’une version aux champs réordonnés. Quelle économie, en octets et en pourcentage ?

Étape 4 : un float vu par une union

/* etape4_union.c -- TP Seance 4, etape 4 : les octets d'un float vus par une union.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape4_union.c -o etape4_union */
#include <stdio.h>

union FloatInt {
    float f;
    unsigned int u;
    unsigned char octets[4];
};

/* TODO 2 : ecrire int est_little_endian(void) avec une union { unsigned int ; unsigned char[4] } */

int main(void)
{
    float valeurs[] = {2.0f, -0.5f, 0.1f, 1000.0f};
    union FloatInt x;
    for (int k = 0; k < 4; k++) {
        x.f = valeurs[k];
        printf("f = %g\n", x.f);
        printf("  motif : 0x%08x\n", x.u);
        /* TODO 1 : afficher les 4 octets de x.octets, du premier au dernier */
    }
    return 0;
}
  1. Modifier. TODO 1 : afficher les 4 octets de x.octets pour chaque valeur.
  2. Expliquer. Pour 2.0f, -0.5f et 1000.0f, décomposer le motif affiché en signe, exposant et mantisse (IEEE 754 sur 32 bits, biais 127), et retrouver la valeur.
  3. Expliquer. Pourquoi le motif de 0.1f se termine-t-il par cd, et non par cc ? Que vaut exactement le float rangé ?
  4. Modifier. TODO 2 : écrire est_little_endian avec une union, et l’appeler à la fin de main. Expliquer le résultat avec l’ordre des octets affichés en (a).

Étape 5 : une union discriminée

Le programme etape5_variant.c range des valeurs de types différents dans un tableau de Variant. Il contient une erreur volontaire : var_liberer ne libère rien.

/* etape5_variant.c -- TP Seance 4, etape 5 : une union discriminee.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape5_variant.c -o etape5_variant
   Erreur volontaire : var_liberer ne libere rien (valgrind signale des fuites). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef enum { VAR_INT, VAR_DOUBLE, VAR_TEXTE } VarType;

typedef struct {
    VarType type;
    union {
        int i;
        double d;
        char *s;
    } val;
} Variant;

Variant var_int(int i)
{
    Variant v;
    v.type = VAR_INT;
    v.val.i = i;
    return v;
}

Variant var_double(double d)
{
    Variant v;
    v.type = VAR_DOUBLE;
    v.val.d = d;
    return v;
}

Variant var_texte(const char *s)    /* garde une copie de s, allouee sur le tas */
{
    Variant v;
    v.type = VAR_TEXTE;
    v.val.s = malloc(strlen(s) + 1);
    if (v.val.s != NULL)
        strcpy(v.val.s, s);
    return v;
}

void var_afficher(const Variant *v)
{
    switch (v->type) {
    case VAR_INT:
        printf("entier %d\n", v->val.i);
        break;
    case VAR_DOUBLE:
        printf("reel %g\n", v->val.d);
        break;
    case VAR_TEXTE:
        /* TODO 1 : afficher le texte */
        break;
    }
}

void var_liberer(Variant *v)
{
    (void)v;    /* TODO 2 : liberer le texte si le variant en contient un */
}

int main(void)
{
    Variant t[4];
    t[0] = var_int(-17);
    t[1] = var_texte("Bignona");
    t[2] = var_double(36.6);
    t[3] = var_texte("Richard-Toll");
    printf("sizeof(Variant) = %zu\n", sizeof(Variant));
    for (int k = 0; k < 4; k++)
        var_afficher(&t[k]);
    for (int k = 0; k < 4; k++)
        var_liberer(&t[k]);
    return 0;
}
  1. Observer. Compiler et exécuter. Pourquoi les deux textes ne s’affichent-ils pas ? Expliquer sizeof(Variant) (offset de val, padding).
  2. Observer. Exécuter sous valgrind --leak-check=full. Combien de blocs sont perdus, de quelle taille, et quelle ligne les a alloués ? Justifier les tailles.
  3. Modifier. TODO 1 et 2 : afficher le texte, puis libérer la copie dans var_liberer ; vérifier que valgrind n’indique plus aucune fuite.
  4. Expliquer. Pourquoi var_texte copie-t-elle la chaîne au lieu de ranger l’adresse reçue ? Que se passerait-il si un Variant de type VAR_TEXTE était copié par affectation, puis les deux copies libérées ?

Étape 6 : une liste chaînée

Le programme etape6_liste.c gère une liste d’entiers. Il contient une erreur volontaire : list_destroy ne libère pas les nœuds.

/* etape6_liste.c -- TP Seance 4, etape 6 : une liste chainee.
   Compilation : gcc -std=c17 -Wall -Wextra -O0 -g etape6_liste.c -o etape6_liste
   Erreur volontaire : list_destroy ne libere pas les noeuds (valgrind signale des fuites). */
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

typedef struct {
    Node *head;
    size_t size;
} List;

List *list_create(void)
{
    List *l = malloc(sizeof *l);
    if (l != NULL) {
        l->head = NULL;
        l->size = 0;
    }
    return l;
}

void list_push_front(List *l, int data)
{
    Node *n = malloc(sizeof *n);
    if (n == NULL)
        return;
    n->data = data;
    n->next = l->head;
    l->head = n;
    l->size++;
}

/* TODO 1 : void list_push_back(List *l, int data), ajout en fin de liste */
/* TODO 2 : int list_pop_front(List *l), retire le premier noeud et rend sa valeur */

void list_print(const List *l)
{
    printf("[%zu]", l->size);
    for (const Node *p = l->head; p != NULL; p = p->next)
        printf(" %d", p->data);
    printf("\n");
}

void list_destroy(List *l)
{
    free(l);    /* TODO 3 : liberer d'abord les noeuds */
}

int main(void)
{
    List *l = list_create();
    list_push_front(l, 20);
    list_push_front(l, 10);
    list_print(l);
    printf("sizeof(Node) = %zu, sizeof(List) = %zu\n", sizeof(Node), sizeof(List));
    list_destroy(l);
    return 0;
}
  1. Observer. Compiler et exécuter sous valgrind. Combien d’octets sont perdus directement et indirectement ? Expliquer la différence entre les deux.
  2. Modifier. TODO 3 : corriger list_destroy, en lisant next avant de libérer chaque nœud, et vérifier sous valgrind.
  3. Modifier. TODO 1 et 2 : écrire list_push_back et list_pop_front. Dans main, ajouter 30 et 40 en fin de liste, retirer la tête, et afficher la liste après chaque étape.
  4. Expliquer. Pourquoi list_push_back parcourt-elle toute la liste ? Proposer un champ à ajouter à List pour l’éviter, et dire ce qu’il faut alors modifier dans les autres fonctions.

Pour cette séance

Le cours

Les slides

À lire en ligne

À télécharger

Retour au sommet