TP — Séance 4 — Structures et unions
Observer la disposition des structures, les unions et les structures sur le tas
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.
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;
}- 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. - Modifier. Compléter le TODO 1 (offsets de
quantite,codeetclient) et vérifier les prédictions. - Expliquer. Dans le dump, repérer chaque champ et chaque octet
ee. Justifier les octets demontant(12500.0au format IEEE 754) et ceux declient(4021, little-endian). - 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 = ∩
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
- Observer. Copier la sortie de
x/12xb &cap. Repérertype,valeur,codeetetat, et les octets de padding. - Expliquer. Retrouver l’offset de
valeuret celui decodeà partir des adresses affichées parprint &cap.valeuretprint &cap.code. - Expliquer. Les octets de padding valent-ils 0 ? Que dit le cours de leur contenu ?
- Observer. Après
next, quelle valeur prendp->valeur, et quels octets du dump ont changé (relancerx/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;
}- Observer. Compiler et exécuter. Les deux relevés ont-ils les mêmes champs ? Que répond
egal_memcmp? - Expliquer. Situer, dans
trame, les octets de padding destruct Releve. Pourquoimemcmpconclut-elle à une différence ? - Modifier. TODO 1 et 2 : écrire
egal_champs, qui compare champ par champ, et afficher son résultat. - 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;
}- Modifier. TODO 1 : afficher les 4 octets de
x.octetspour chaque valeur. - Expliquer. Pour
2.0f,-0.5fet1000.0f, décomposer le motif affiché en signe, exposant et mantisse (IEEE 754 sur 32 bits, biais 127), et retrouver la valeur. - Expliquer. Pourquoi le motif de
0.1fse termine-t-il parcd, et non parcc? Que vaut exactement lefloatrangé ? - Modifier. TODO 2 : écrire
est_little_endianavec une union, et l’appeler à la fin demain. 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;
}- Observer. Compiler et exécuter. Pourquoi les deux textes ne s’affichent-ils pas ? Expliquer
sizeof(Variant)(offset deval, padding). - 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. - 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. - Expliquer. Pourquoi
var_textecopie-t-elle la chaîne au lieu de ranger l’adresse reçue ? Que se passerait-il si unVariantde typeVAR_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;
}- Observer. Compiler et exécuter sous valgrind. Combien d’octets sont perdus directement et indirectement ? Expliquer la différence entre les deux.
- Modifier. TODO 3 : corriger
list_destroy, en lisantnextavant de libérer chaque nœud, et vérifier sous valgrind. - Modifier. TODO 1 et 2 : écrire
list_push_backetlist_pop_front. Dansmain, ajouter 30 et 40 en fin de liste, retirer la tête, et afficher la liste après chaque étape. - Expliquer. Pourquoi
list_push_backparcourt-elle toute la liste ? Proposer un champ à ajouter àListpour 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
- Code de départ du TP (ZIP, 5 Ko)