TP — Séance 2 — Représentation numérique
Octets et bits, conversions, débordements, masques, IEEE 754, lecture portable
Écrire, compiler et tester un petit module repr.c qui manipule la représentation des nombres : afficher les octets et les bits d’une variable et les comparer à ce que montre gdb, convertir sans perdre de valeur, détecter un débordement avant le calcul et observer ce qu’en fait l’optimiseur, ranger des champs dans un mot de 16 bits, décomposer un float, lire un en-tête de fichier binaire indépendamment de l’endianness, puis faire taire -Wconversion. Durée : 2 h à 2 h 30, en sept étapes. Le dossier TP_S02_code_depart/ contient repr.h (les déclarations, à ne pas modifier sauf à l’étape 2), repr.c (les fonctions à compléter, repérées par TODO 1 à TODO 14), test_repr.c (les tests), six programmes d’observation etape*.c, l’en-tête d’image entete.bmp et un Makefile. Au départ, tout compile, mais make test signale 32 échecs ; à la fin, chaque étape doit afficher « ok ».
Décompresser TP_S02_code_depart.zip, se placer dans le dossier TP_S02_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 # gcc -std=c17 -Wall -Wextra -O0 -g, tous les programmes
make test # les tests, une ligne par etape
make ubsan # tests et etape 3 avec -fsanitize=undefined
make asan # tests avec AddressSanitizer
make valgrind # chaque programme sous valgrind
make optim # etape 3 bis, en -O0 puis en -O2
make clean # retirer les executables
setarch -R ./etape1_octets # adresses deterministes (sans alea)Étape 1 : octets et bits (TODO 1)
show_bytes est donnée : elle affiche les octets d’un objet dans l’ordre des adresses. bits_texte(v, w, buf) doit écrire dans buf les w bits de poids faible de v, du bit w - 1 au bit 0, avec une espace tous les 4 bits, puis '\0' : "1011 0011" pour v = 0xb3 et w = 8.
/* etape1_octets.c -- les octets et les bits de quelques variables */
#include <stdio.h>
#include "repr.h"
int main(void)
{
char buf[64];
int effectif = 3913;
int ecart = -3913;
unsigned short port = 8080;
double note = 15.75;
printf("effectif :");
show_bytes(&effectif, sizeof effectif);
printf("ecart :");
show_bytes(&ecart, sizeof ecart);
printf("port :");
show_bytes(&port, sizeof port);
printf("note :");
show_bytes(¬e, sizeof note);
bits_texte((uint32_t)effectif, 32, buf);
printf("effectif : %s\n", buf);
bits_texte((uint32_t)ecart, 32, buf);
printf("ecart : %s\n", buf);
return 0;
}- Observer. Avant d’écrire
bits_texte, lancer./etape1_octets. Dans quel ordre les octets deeffectifapparaissent-ils ? Qu’en déduire sur la machine ? - Expliquer. Retrouver à la main les octets de
ecartà partir de ceux deeffectif, puis ceux deport. - Modifier. Écrire
bits_texte, puis relancermake test(étape 1) et./etape1_octets. Vérifier sur les bits deecartla règle \(-x = \mathord{\sim}x + 1\). - Expliquer. Les 8 octets de
noteforment undouble: écrire le mot de 64 bits, isoler le signe, l’exposant (11 bits, biais 1023) et le début de la fraction, et retrouver 15,75.
Étape 1 bis : les mêmes octets sous gdb
gdb lit la mémoire du programme arrêté : x/4xb &v affiche 4 octets (b) en hexadécimal (x) à partir de l’adresse de v ; p/x v et p/t v affichent la valeur de v en hexadécimal et en binaire.
setarch -R gdb -q ./etape1_octets
(gdb) break 24
(gdb) run
(gdb) x/4xb &effectif
(gdb) p/x effectif
(gdb) x/4xb &ecart
(gdb) p/t ecart
(gdb) p/x port
(gdb) x/8xb ¬e- Observer. Comparer
x/4xb &effectifetp/x effectif: pourquoi l’ordre des chiffres diffère-t-il ? Lequel des deux correspond àshow_bytes? - Expliquer. Que donne
p/t ecart, et à quelle sortie debits_textecorrespond-il ? Pourquoip/x effectifn’affiche-t-il pas de zéros de tête, alors quex/4xbmontre les quatre octets ? - Observer. Les adresses de
effectif,ecartetnote: de combien d’octets se suivent-elles ? (La Séance 3 explique cette disposition.)
Étape 2 : conversions (TODO 2 et 3, erreur volontaire)
Modifier.
etend8(octet)doit rendre la valeur de l’octet lu en complément à deux (\(-128\) à 127) sans convertir vers un type signé de 8 bits : n’utiliser que des opérations sur des entiers non signés ou sur desint. Rappel : un octet \(b\) dont le bit 7 vaut 1 représente \(b - 256\).tronque8(v)rend les 8 bits de poids faible dev.Observer. La fonction
compte_sous(t, n, seuil)du départ compte les éléments detplus petits queseuil, et contient une erreur volontaire. La compilation affiche :Terminal :
$ make -s 2>&1 repr.c: In function 'compte_sous': repr.c:42:18: warning: comparison of integer expressions of different signedness: 'int' and 'unsigned int' [-Wsign-compare] 42 | if (t[i] < seuil) | ^Le test appelle
compte_sousavec{-40, 15, -2, 320, 7}etseuil = 10. Combien d’éléments devraient être comptés ? Combien le départ en compte-t-il, et pourquoi (type de la comparaison, valeur de-40convertie) ?Modifier. Corriger
compte_sous(et sa déclaration dansrepr.h) sans conversion explicite, puis vérifier que l’avertissement disparaît et que l’étape 2 passe.
Étape 3 : débordements (TODO 4 et 5)
/* etape3_montant.c -- montant d'une commande : prix unitaire (FCFA) multiplie par une quantite */
#include <stdio.h>
#include "repr.h"
int main(void)
{
int prix = 85000;
int quantite = 30000;
int montant = prix * quantite;
printf("montant = %d FCFA\n", montant);
return 0;
}Observer. Lancer
./etape3_montant, puismake ubsan. Quel montant s’affiche ? Que signale UBSan (l’option-fsanitize=undefined), à quelle ligne ? Retrouver la valeur affichée à partir du montant exact et de \(2^{32}\), et dire pourquoi elle n’est pas garantie.Modifier. Écrire
somme_sure(a, b, &r): rendre 0 sia + bne tient pas dans unint32_t, sinon ranger la somme dans*ret rendre 1. Le test doit être fait avant l’addition, et aucune opération ne doit déborder (utiliserINT32_MAXetINT32_MIN). Avant d’écrire le code, remplir à la main le tableau suivant, tiré des tests :a b la somme tient ? *r2147483000648-2147483000-6492147483000646-59Modifier. Écrire
produit_sur(a, b, &r), sur le même modèle, en calculant le produit dans unint64_t. Expliquer pourquoi ce produit ne déborde jamais.Modifier. Réécrire
etape3_montant.cavecproduit_sur: afficher le montant s’il tient dans unint, un message sinon, puis le montant exact calculé enlong long.make ubsanne doit plus rien signaler.
Étape 3 bis : l’optimisation change le résultat
/* etape3b_optim.c -- le meme source, compile en -O0 puis en -O2 */
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int deborde(int x)
{
int y = x + 1;
return y < x;
}
int main(void)
{
printf("deborde(INT_MAX) = %d\n", deborde(INT_MAX));
int tours = 0;
for (int8_t i = 0; i < 200 && tours < 300; i++)
tours++;
printf("tours = %d\n", tours);
return 0;
}make optim
objdump -d --no-show-raw-insn etape3b_O0 | sed -n '/<deborde>:/,/^$/p'
objdump -d --no-show-raw-insn etape3b_O2 | sed -n '/<deborde>:/,/^$/p'- Observer. Que rend
deborde(INT_MAX)en-O0, puis en-O2? Dans le code assembleur, retrouver l’addition et la comparaison en-O0; que reste-t-il dedebordeen-O2? - Expliquer. Pourquoi le compilateur a-t-il le droit de rendre toujours 0 ? Écrire une version de
debordequi ne dépend pas d’un comportement indéfini, et vérifier qu’elle rend 1 aux deux niveaux. - Expliquer. La boucle sur
int8_ts’arrête-t-elle sans le compteur de sécuritétours? Que signale gcc, et pourquoi la conditioni < 200est-elle toujours vraie ? Ce comportement est-il indéfini ?
Étape 4 : couleurs sur 16 bits (TODO 6 et 7)
Un écran 16 bits code une couleur en RGB565 : le rouge sur les bits 15 à 11, le vert sur les bits 10 à 5, le bleu sur les bits 4 à 0. Une composante de 8 bits perd ses bits de poids faible : le rouge garde ses 5 bits de poids fort, le vert 6, le bleu 5. etape4_drapeau.c code les trois couleurs du drapeau du Sénégal (vert (0x00, 0x85, 0x3f), jaune (0xfd, 0xef, 0x42), rouge (0xe3, 0x1b, 0x23)) et un ocre.
- Expliquer. Calculer à la main le code RGB565 de l’ocre
(0xcc, 0x77, 0x22), en hexadécimal. - Modifier. Écrire
rgb565, puisrouge565,vert565etbleu565, qui extraient chaque champ d’un code. Lancer./etape4_drapeauet vérifier le calcul de la question précédente. - Expliquer. Pourquoi
rouge565(rgb565(r, g, b))ne rend-il pasr? Que rend-il ?
Étape 5 : IEEE 754 (TODO 8 à 11)
Modifier.
decompose(f, &s, &e, &m)range le signe, l’exposant biaisé et la fraction def; les bits sont déjà copiés dansuparmemcpy.compose(s, e, m)fait l’inverse.categorie(f)rend"zero","denormalise","normalise","infini"ou"nan", d’après le tableau du cours.Expliquer. Vérifier à la main la décomposition de \(-2{,}75\) attendue par le test : \(s = 1\), \(e = 128\), \(m =\)
0x300000; puis la valeur decompose(0, 133, 0x480000). Compléter ensuite le tableau des valeurs queetape5_flottantsva afficher, avant de le lancer :valeur \(s\) \(e\) \(m\) catégorie -2.75f100.0fFLT_MAXINFINITYNANObserver. Lancer
./etape5_flottants. Pourquoi1e-40fn’a-t-il pas l’exposant \(-133\) que donnerait \(\log_2(10^{-40})\) ? Que vaut sa fraction ?Modifier.
proches(a, b, tol)rend 1 si \(|a - b| \le \mathit{tol} \times \max(|a|, |b|)\). Pourquoi une tolérance relative ? Que montre la dernière ligne deetape5_flottants?
Étape 6 : lire un en-tête BMP (TODO 12 à 14)
Un fichier BMP commence par un en-tête de 54 octets dont les entiers sont rangés en little-endian, quelle que soit la machine qui lit le fichier : signature "BM" à l’octet 0, taille du fichier (32 bits) à l’octet 2, position des pixels à l’octet 10, largeur et hauteur (32 bits) aux octets 18 et 22, nombre de bits par pixel (16 bits) à l’octet 28. entete.bmp contient l’en-tête d’une image de 30 sur 20 pixels.
od -A d -t x1 entete.bmp- Expliquer. Dans le dump de
od, retrouver à la main la taille du fichier, la largeur et la hauteur. - Modifier. Écrire
lire16le(p)etlire32le(p), qui assemblent les octetsp[0],p[1], … par décalages et|, l’octetp[0]étant celui de poids faible ; puisecrire32le(p, v), qui rangevdansp[0]àp[3]dans le même ordre. Aucune de ces fonctions n’utilisememcpyni de cast de pointeur. - Observer. Lancer
./etape6_bmp. Pourquoimemcpydonne-t-il ici la même largeur quelire32le? Que donnerait-il sur une machine big-endian, etlire32le? - Expliquer. Que valent les octets 18 à 21 après
ecrire32le(e + 18, 60)?
Étape 7 : -Wconversion
make clean && make CFLAGS+=-Wconversion- Observer. Quels avertissements apparaissent, dans quelles fonctions ? Celui de
moyenne8signale-t-il un vrai risque de perte ? - Modifier. Corriger
repr.cpour que la compilation avec-Wconversionn’affiche plus rien, en rendant explicites les conversions sûres (moyenne8, et celles que votre propre code a introduites), sans changer les résultats. - Vérifier.
make test: « tous les tests passent ». Puismake ubsan,make asanetmake valgrind: aucun rapport ne doit apparaître.
Pour cette séance
Le cours
Les slides
À lire en ligne
À télécharger
- Code de départ du TP (ZIP, 8 Ko)