Aller au contenu principal

Programmation C · L2 · Section 3/12

Codage et mémoire

Progression

Points d’expérience : XPSérie de jours consécutifs : · —Progression du module : — / —compris

#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 :

cc

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 :

cc

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 :

cc

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 :

cc

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 :

cc

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 :

cc

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 :

cc

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) :

code

1CA FE 00 03 01 02 03 04 41 42 432decode: magic=CAFE len=3 id=01020304 payload=ABC3verif: OK

Si 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.

Pourquoi la lecture x = *((unsigned char *)&x) est-elle définie ?
Pourquoi la lecture x = *((unsigned char *)&x) est-elle définie ?