Programmation C · L2 · Section 3/12
Codage et mémoire
Progression
#Codage binaire et mémoire
Travailler en C impose de visualiser la mémoire : chaque type correspond à une représentation binaire précise, et l’oublier est la source de bogues subtils qui ne se manifestent que sur une autre machine, ou sous -O2. Ce chapitre explore l’endianness, la représentation des entiers, l’alignement des structures et la sérialisation reproductible d’un bout à l’autre.
#1. Endianness et représentation des entiers
Les architectures little endian stockent l’octet de poids faible à la plus petite adresse, les big endian font l’inverse. Pour un unsigned x = 0x01020304, la mémoire contient 04 03 02 01 en little endian et 01 02 03 04 en big endian. Le code de détection classique :
1int is_little_endian(void) {2 unsigned int x = 1;3 return *((unsigned char *)&x) == 1;4}Cette lecture est définie par la norme : accéder à la représentation d’un objet par un lvalue de type caractère (char, signed char, unsigned char) est toujours autorisé. C’est l’exception qui rend ce genre d’outillage portable ; y accéder via un pointeur vers un autre type serait, lui, un comportement indéfini.
Entiers signés : depuis C23, la norme impose le complément à deux ; les versions antérieures le laissaient en choix d’implémentation, mais il était déjà universel en pratique. La règle qui compte pour vos programmes :
1unsigned char p = 250;2p += 10; /* défini : p == 4, l'arithmétique non signée enveloppe modulo 256 */3 4int y = INT_MAX;5y += 1; /* comportement indéfini : débordement signé, à éliminer */Le débordement non signé est un outil (compteurs CRC, hachage) ; le débordement signé est un bug que le compilateur est en droit d’exploiter. UBSan (-fsanitize=undefined) le traque.
#2. Alignement et disposition des structures
Un processeur lit plus efficacement une donnée à une adresse multiple de sa taille : on dit qu’elle est alignée. Le compilateur insère donc du remplissage (padding) entre les champs, et sizeof le reflète :
1struct S { char a; int b; char c; }; /* x86-64 typique : 1 + 3 (pad) + 4 + 1 + 3 (pad) = 12 */2struct T { int b; char a; char c; }; /* x86-64 typique : 4 + 1 + 1 + 2 (pad) = 8 */Vérifiez sur votre machine avec un petit programme : printf("%zu %zu %zu %zu\n", sizeof(struct S), sizeof(struct T), _Alignof(int), _Alignof(struct T));. Réordonner les champs par taille décroissante est l’optimisation la plus simple et la plus sûre qui existe en C : elle ne change aucun comportement, seulement l’empreinte mémoire.
Les bitfields (uint32_t mantisse:23;) permettent de packer des drapeaux, mais l’ordre d’allocation des champs à bits dépend de l’implémentation : jamais pour un format de fichier ou de réseau. Pour disséquer un float IEEE-754 de façon portable :
1#include <stdint.h>2#include <string.h>3 4float f = 1.5f;5uint32_t bits;6memcpy(&bits, &f, sizeof bits); /* copie d'octets : toujours défini */7uint32_t signe = bits >> 31; /* 0 */8uint32_t exposant = (bits >> 23) & 0xFFu; /* 127 : biais +0 */9uint32_t mantisse = bits & 0x7FFFFFu; /* 0x400000 : 0,5 en base 2 */10printf("s=%u e=%u m=%#x\n", signe, exposant, mantisse);memcpy entre la vue float et la vue uint32_t lit la même suite d’octets sans jamais réinterpréter une valeur à travers un pointeur inadapté. La représentation bitfield équivalente reste utile à connaître comme illustration :
1typedef struct {2 uint32_t mantisse:23;3 uint32_t exposant:8;4 uint32_t signe:1;5} float_bits; /* disposition non portable : illustration uniquement */#3. Lecture brute : afficher la mémoire
Un utilitaire d’affichage hexadécimal est l’instrument de référence pour vérifier une sérialisation :
1#include <stdio.h>2#include <stddef.h>3 4void dump_hex(const void *buffer, size_t len) {5 const unsigned char *bytes = buffer;6 for (size_t i = 0; i < len; ++i) {7 printf("%02X ", bytes[i]);8 if ((i + 1) % 16 == 0) putchar('\n');9 }10 putchar('\n');11}const void * en paramètre dit exactement ce qu’il faut : lecture seule, type quelconque, longueur explicite car un pointeur nu ne porte aucune information de taille.
#4. Sérialisation avec ordre d’octets garanti
Sur un réseau ou dans un fichier destiné à une autre machine, l’endianness locale ne doit jamais fuiter. <arpa/inet.h> fournit htonl/htons (hôte vers réseau, big endian) et leurs inverses :
1#include <arpa/inet.h>2 3uint32_t net_id = htonl(id); /* uint32_t : ordre réseau garanti */4uint16_t net_len = htons(len); /* uint16_t : idem */Disposez ces valeurs dans un unsigned char tampon[N] (par décalages ou memcpy depuis les valeurs converties), émettez le tampon, puis décodez côté réception avec ntohl/ntohs. Les tailles uint16_t/uint32_t garantissent le nombre d’octets écrits, indépendamment de la plateforme.
#Exercice : sérialiser puis relire un Message
Définissez struct Message { uint16_t magic; uint16_t len; uint32_t id; } avec magic = 0xCAFE, len = 3, id = 0x01020304, suivis d’un payload "ABC". Écrivez serial.c qui encode le tout en ordre réseau dans un tampon, l’affiche avec dump_hex, le décode en une copie locale et vérifie champ par champ.
Sortie attendue (la même sur toute machine) :
1CA FE 00 03 01 02 03 04 41 42 432decode: magic=CAFE len=3 id=01020304 payload=ABC3verif: OKSi votre hexadécimal commence par FE CA, vous avez omis htons sur magic.
Piste de correction
Écrivez chaque champ converti dans une variable locale (uint16_t m = htons(0xCAFE);) puis memcpy(tampon + off, &m, 2); avec un offset qui avance. Côté décodage, copiez dans des locaux puis appliquez ntohs/ntohl. La vérification finale compare les valeurs décodées aux valeurs d’origine et affiche verif: OK ; toute divergence doit quitter avec un code non nul pour rester testable en script.