Séance 2 — Représentation numérique

Bases, entiers, complément à deux, bits, flottants IEEE~754

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

Ce que la séance construit

Les bits derrière les valeurs du C : comment un entier, signé ou non, et un réel s’écrivent en binaire sur un nombre fixe de bits, ce que deviennent ces bits quand le type change ou qu’un calcul déborde, et comment un programme les manipule un par un.

  • Passer d’une base à l’autre (décimal, binaire, hexadécimal) et lire un octet en deux chiffres hexadécimaux.
  • Donner la valeur d’un motif de bits lu en non signé et en complément à deux, et l’intervalle de chaque type.
  • Prévoir le résultat d’une conversion (signé ou non signé, extension, troncature) et d’une comparaison mixte.
  • Distinguer débordement défini (non signé), défini par l’implémentation (conversion) et indéfini (signé), et le détecter avant le calcul.
  • Tester, mettre à 1, mettre à 0, inverser et extraire des bits avec des masques et des décalages.
  • Encoder et décoder un float IEEE 754 (signe, exposant biaisé, mantisse), le lire dans un dump little-endian, et expliquer \(0{,}1 + 0{,}2 \neq 0{,}3\).

Systèmes de numération

Base 2 : la valeur d’un motif de bits

Une variable C occupe un nombre fixe de bits ; avant de parler de signe ou de virgule, il faut savoir ce que vaut une suite de 0 et de 1.

Notation binaire

En base 2, la position \(i\) pèse \(2^i\) :

\[ b_{n-1}\dots b_1 b_0 \;=\; \sum_{i=0}^{n-1} b_i\, 2^i, \qquad b_i \in \{0, 1\} \]

Le bit de poids fort (MSB, {most significant bit}) est à gauche, le bit de poids faible (LSB) à droite.

Exemple

\(1011_2 = 1\cdot 8 + 0\cdot 4 + 1\cdot 2 + 1\cdot 1 = 11\).

Décimal vers binaire

Diviser par 2 tant que le quotient n’est pas nul ; les restes, lus du dernier au premier, sont les bits du poids fort au poids faible : \(13 \rightarrow 1101_2\).

Lecture. \(n\) bits donnent \(2^n\) motifs : 256 sur un octet. Le programme suivant applique les divisions successives à 186.

Décimal vers binaire, pas à pas

Valeurs relevées sous gdb à chaque tour ; en rouge, le reste écrit dans b[i].

Sortie \(\rightarrow\)

186 = 10111010

Lecture. q \(=186\), les huit cases de b à 0, i \(=0\) : b[i] recevra le bit de poids \(2^i\).

Lecture. Tour 1 : \(186 = 2 \times 93 + 0\) ; le reste 0 va dans b[0] (poids \(2^0\)), q devient 93.

Lecture. Tour 2 : \(93 = 2 \times 46 + 1\) ; le reste 1 va dans b[1] (poids \(2^1\)), q devient 46.

Lecture. Tour 3 : \(46 = 2 \times 23 + 0\) ; le reste 0 va dans b[2] (poids \(2^2\)), q devient 23.

Lecture. Tour 4 : \(23 = 2 \times 11 + 1\) ; le reste 1 va dans b[3] (poids \(2^3\)), q devient 11.

Lecture. Tour 5 : \(11 = 2 \times 5 + 1\) ; le reste 1 va dans b[4] (poids \(2^4\)), q devient 5.

Lecture. Tour 6 : \(5 = 2 \times 2 + 1\) ; le reste 1 va dans b[5] (poids \(2^5\)), q devient 2.

Lecture. Tour 7 : \(2 = 2 \times 1 + 0\) ; le reste 0 va dans b[6] (poids \(2^6\)), q devient 1.

Lecture. Tour 8 : \(1 = 2 \times 0 + 1\) ; q vaut 0, la boucle s’arrête : le dernier reste est le bit de poids fort.

Lecture. k va de 7 à 0 : les restes s’affichent du dernier au premier, \(186 = 1011\,1010_2\).

Base 16 : un chiffre pour 4 bits

hex 0 1 2 3 4 5 6 7 8 9 a b c d e f
déc. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
bin. 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111

Binaire et hexadécimal

Grouper les bits par 4 depuis la droite : \(1011\,1010_2 \rightarrow\) b, a, soit 0xba. Un octet s’écrit donc toujours en deux chiffres hexadécimaux (%02x), de 00 à ff ; c’est la notation des dumps.

unsigned int n = 186;
unsigned int haut = (n >> 4) & 0xf;
unsigned int bas = n & 0xf;
printf("decimal     : %u\n", n);
printf("hexadecimal : 0x%02x  0x%02X\n", n, n);
printf("octal       : 0%o\n", n);
printf("chiffres    : %x puis %x\n", haut, bas);
printf("litteraux   : %d %d %d\n", 0xba, 0272, 186);

Résultat \(\rightarrow\)

decimal     : 186
hexadecimal : 0xba  0xBA
octal       : 0272
chiffres    : b puis a
litteraux   : 186 186 186

Lecture. En C, 0x introduit un littéral hexadécimal, un 0 en tête un littéral octal (0272), et %x, %X, %o les affichent. Les quatre bits de poids fort de 186 donnent b (\(11\)), les quatre de poids faible a (\(10\)).

Entiers non signés et complément à deux

Entiers non signés

Un type entier de \(w\) bits offre \(2^w\) motifs ; un type non signé les fait tous correspondre à des valeurs positives ou nulles.

Non signé sur \(w\) bits

\[ B2U_w(\vec{x}) = \sum_{i=0}^{w-1} x_i\, 2^i \]

Intervalle : \([0,\; 2^w - 1]\) ; aucune valeur négative. \(\texttt{0xdb} = 1101\,1011_2 = 219\).

unsigned char o = 0xdb;
printf("0xdb = %u\n", o);
printf("unsigned char  : 0 .. %u\n", UCHAR_MAX);
printf("unsigned short : 0 .. %u\n", USHRT_MAX);
printf("unsigned int   : 0 .. %u\n", UINT_MAX);
printf("unsigned long  : 0 .. %lu\n", ULONG_MAX);

Résultat \(\rightarrow\)

0xdb = 219
unsigned char  : 0 .. 255
unsigned short : 0 .. 65535
unsigned int   : 0 .. 4294967295
unsigned long  : 0 .. 18446744073709551615

Lecture. <limits.h> donne les maximums de la machine du cours : \(2^8-1\), \(2^{16}-1\), \(2^{32}-1\) et \(2^{64}-1\) (unsigned long mesure 8 octets en LP64).

Quoi, pourquoi, quand ça échoue : le complément à deux

Quoi

Le bit de poids fort pèse \(-2^{w-1}\) au lieu de \(+2^{w-1}\) :

\[ B2T_w(\vec{x}) = -x_{w-1}\, 2^{w-1} + \sum_{i=0}^{w-2} x_i\, 2^i \]

Ce bit est le bit de signe (1 : négatif). Intervalle : \([-2^{w-1},\; 2^{w-1}-1]\), soit \([-128, 127]\) sur 8 bits.

Pourquoi

L’addition est la même que pour les non signés (modulo \(2^w\)) : le processeur n’a qu’un additionneur. Zéro n’a qu’une écriture, et \(-x\) s’obtient par \(\mathord{\sim}x + 1\).

Quand ça échoue

L’intervalle n’est pas symétrique : \(|T_{min}| = T_{max} + 1\). \(-T_{min}\) ne tient pas dans le type : sur un int, -INT_MIN est un débordement, donc un comportement indéfini.

Valeurs remarquables sur 8 bits

static void ligne(const char *nom, int8_t x)
{
    printf("%-5s %4d  0x%02x  ", nom, x, (uint8_t)x);
    for (int i = 7; i >= 0; i--)
        putchar('0' + (((uint8_t)x >> i) & 1));
    putchar('\n');
}
int main(void)
{
    ligne("0", 0);
    ligne("-1", -1);
    ligne("T_max", 127);
    ligne("T_min", -128);
    ligne("-3", -3);
    ligne("120", 120);
    return 0;
}

Résultat \(\rightarrow\)

0        0  0x00  00000000
-1      -1  0xff  11111111
T_max  127  0x7f  01111111
T_min -128  0x80  10000000
-3      -3  0xfd  11111101
120    120  0x78  01111000

Lecture d’un motif

\(-1\) : tous les bits à 1 (0xff) ; \(T_{max}\) : 0x7f ; \(T_{min}\) : 0x80. \(1111\,1101_2 = -128+125 = -3\) ; \(0111\,1000_2 = 120\).

Lecture. (uint8_t)x montre les bits de x sans changer le motif : c’est le même octet, lu sans signe. Sur 32 bits, un int va de \(-2^{31} = -2147483648\) à \(2^{31}-1 = 2147483647\).

Complément à deux d’un négatif, pas à pas

Bits relevés en mémoire sous gdb ; ligne du haut : le poids de chaque bit.

Sortie \(\rightarrow\)

~x = 0xfa        y = -5
~x + 1 = 0xfb    x + y = 0

Lecture. x \(=5 = 0000\,0101_2\) ; sur un type signé, le bit 7 (orange) pèse \(-128\).

Lecture. ~x inverse chacun des bits : \(1111\,1010_2\) (0xfa).

Lecture. \(+1\) : le bit 0 passe de 0 à 1, aucune retenue ; 0xfb.

Lecture. Mêmes bits lus en signé : \(-128 + 64+32+16+8+2+1 = -5\) (conversion définie par l’implémentation, gcc garde les bits).

Lecture. Affichage de ~x.

Lecture. Affichage de ~x + 1.

Lecture. y vaut \(-5\) : \(-x = \mathord{\sim}x + 1\).

Lecture. x + y se calcule en int : \(5 + (-5) = 0\). Sur 8 bits, \(0000\,0101 + 1111\,1011\) donne aussi 0 : la retenue sort du 8e bit.

Exercice : un dump, plusieurs lectures

Huit octets relevés en 0x00 sur une machine little-endian (l’octet de poids faible d’un objet est à l’adresse la plus basse, Séance 3) :

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 7f7f 8080 ffff 00 fefefe 010101 c8c8c8 3838
unsigned char *u = (unsigned char *)0x00;
signed char *s = (signed char *)0x00;
short *h = (short *)0x04;
int *n = (int *)0x04;

Énoncé

Donner (a) u[0] et s[0], (b) u[1] et s[1], (c) u[2] et s[2], (d) u[6] et s[6], (e) *h, (f) *n, en hexadécimal puis en décimal.

Correction

  1. 0x7f, bit 7 à 0 : 127 dans les deux lectures.
  2. 0x80 : 128 sans signe, \(-128\) en complément à deux.
  3. 0xff : 255, et \(-128+127 = \mathbf{-1}\).
  4. 0xc8 : 200, et \(200 - 256 = \mathbf{-56}\).
  5. 2 octets fe 01, poids faible d’abord : 0x01fe \(=\) 510.
  6. fe 01 c8 38 lus de +7 vers +4 : 0x38c801fe \(=\) 952631806, bit 31 à 0.

Conversions entre signé et non signé

Même motif, deux interprétations

Entre un type signé et le type non signé de même taille, la conversion garde les bits et change leur lecture.

Règles (C17 6.3.1.3)

Vers un non signé : valeur modulo \(2^w\), toujours définie : \(T2U_w(x) = x + 2^w\) si \(x < 0\), \(x\) sinon.

Vers un signé, si la valeur n’y tient pas : défini par l’implémentation ; gcc garde les bits : \(U2T_w(u) = u - 2^w\) si \(u > T_{max}\), \(u\) sinon.

signed char sc = -3;
unsigned char uc = (unsigned char)sc;
int d = -1;
unsigned int ud = (unsigned int)d;
unsigned int g = 3000000000u;
int gi = (int)g;
printf("%d -> %u\n", sc, uc);
printf("%d -> %u\n", d, ud);
printf("%u -> %d\n", g, gi);

Résultat \(\rightarrow\)

-3 -> 253
-1 -> 4294967295
3000000000 -> -1294967296

Lecture. 0xfd vaut 253 sans signe et \(-3\) en complément à deux ; \(-1\) devient \(-1 + 2^{32} = 4294967295\), et \(3000000000 - 2^{32} = -1294967296\).

Comparaison signé / non signé, pas à pas

Bits relevés sous gdb ; numéros de bit au-dessus (31 à gauche).

Sortie \(\rightarrow\)

si >= ui
v = 4294967295

Lecture. si \(=-1\) : en complément à deux, les 32 bits à 1 (0xffffffff).

Lecture. ui \(=0\) : 32 bits à 0.

Lecture. int contre unsigned int : si est converti en unsigned int, mêmes bits, valeur \(2^{32}-1\) ; \(4294967295 < 0\) est faux.

Lecture. La branche else s’exécute ; gcc l’annonçait (-Wsign-compare).

Lecture. Même conversion dans une affectation : les bits sont copiés tels quels.

Lecture. \(-1 + 2^{32} = 4294967295\) : la valeur que la Séance 1 obtenait pour (unsigned)-1.

Pièges de comparaison

Chaque ligne affiche une expression et sa valeur (VOIR(e) imprime #e, puis (int)(e)) :

Compilateur \(\rightarrow\)

s3_pieges.c:10:22: warning: comparison of integer expressions of different signedness: 'unsigned int' and 'int' [-Wsign-compare]

Résultat \(\rightarrow\)

-1 < 0U                        0
-1 > -2                        1
2147483647 > -2147483647 - 1   1
2147483647U > -2147483647 - 1  0
2147483647U > -2147483648      1
sizeof(-2147483648)            8

Règle

Si un opérande est unsigned int et l’autre int, l’int est converti en unsigned int (conversions arithmétiques usuelles, C17 6.3.1.8).

L’expression -2147483648

C’est - appliqué à 2147483648, qui ne tient pas dans un int : en C17, 2147483648 est un long (8 octets en LP64). La comparaison se fait alors en long et vaut 1. Pour \(T_{min}\), écrire -2147483647 - 1 ou INT_MIN.

Lecture. -1 < 0U : \(-1\) devient 4294967295, faux. 2147483647U > -2147483647 - 1 : \(T_{min}\) devient \(2^{31} = 2147483648\), faux. Sur une machine 32 bits en C90, -2147483648 était un unsigned long : le résultat dépend de la norme et du modèle.

Extension et troncature

Changer de largeur

Une conversion change aussi le nombre de bits : vers un type plus large, des bits s’ajoutent à gauche ; vers un type plus étroit, des bits disparaissent.

Extension, de \(w\) à \(w' > w\) bits

Non signé : des 0 à gauche ({zero extension}) ; 0xfa devient 0x00fa.

Signé : le bit de signe est recopié ({sign extension}) ; 0xfa (\(-6\)) devient 0xfffa, toujours \(-6\).

La valeur est conservée dans les deux cas.

Troncature, de \(w\) à \(w' < w\) bits

Seuls les \(w'\) bits de poids faible restent : valeur modulo \(2^{w'}\) vers un non signé (défini), et, vers un signé trop petit, résultat défini par l’implémentation (gcc : les mêmes bits lus en complément à deux).

Promotions

Dans une expression, char et short sont d’abord étendus en int (Séance 1) : chaque promotion est une extension.

Extension de 8 à 32 bits, pas à pas

Bits relevés sous gdb ; en orange, le bit de signe.

Sortie \(\rightarrow\)

s = -6   z = 250   r = 250

Lecture. c \(=-6\) : \(1111\,1010_2\) sur 8 bits (bit de signe à 1).

Lecture. Extension de signe : le bit de signe de c est recopié dans les 24 bits ajoutés ; s vaut toujours \(-6\).

Lecture. u \(=250\) : les mêmes 8 bits que c, lus sans signe.

Lecture. Extension par zéros : un type non signé reçoit des 0 à gauche ; z \(=250\).

Lecture. Le cast (uint8_t) passe d’abord c en non signé (250), puis l’extension ajoute des zéros : l’ordre décide.

Lecture. s : \(-6\).

Lecture. z : 250.

Lecture. r : 250, et non 0xfffffffa.

Troncature, pas à pas

Bits relevés sous gdb ; hachurés : les bits de poids fort qui ne sont pas recopiés.

Sortie \(\rightarrow\)

t = -25536   c = -56

Lecture. big \(=40000 =\) 0x00009c40 : 32 bits, bit 31 à 0.

Lecture. Troncature : t garde les 16 bits de poids faible (0x9c40), les 16 autres sont perdus ; le bit 15, à 1, devient le bit de signe.

Lecture. k \(=200 =\) 0x000000c8.

Lecture. 8 bits gardés : 0xc8 \(= 1100\,1000_2\), bit de signe à 1.

Lecture. \(40000 - 2^{16} = -25536\) : le résultat de (short)40000 en Séance 1.

Lecture. \(200 - 2^{8} = -56\) : vers un type signé trop petit, la conversion est définie par l’implémentation (C17 6.3.1.3 §3) ; gcc garde les bits.

Exercice : conversions implicites

Énoncé. Prévoir les quatre lignes affichées, en nommant la conversion appliquée (machine du cours, LP64).

int x = -1;
unsigned int y = 0;
printf("a) %d\n", x < y);
signed char sc = -10;
unsigned int ui = sc;
printf("b) %u\n", ui);
int big = 0x12345678;
signed char small = big;
printf("c) %d\n", small);
if (x > sizeof(int))
    printf("d) -1 > 4\n");
else
    printf("d) -1 <= 4\n");

Correction

  1. x converti en unsigned int : \(4294967295 < 0\) est faux, 0.
  2. Extension de signe : 0xf6 devient 0xfffffff6, lu sans signe : \(2^{32} - 10 =\) 4294967286.
  3. 0x12345678 ne tient pas dans un signed char : conversion définie par l’implémentation ; gcc garde l’octet 0x78, bit 7 à 0 : 120.
  4. sizeof est un size_t (unsigned long) : x devient \(2^{64} - 1\) : -1 > 4.

Débordements

Débordement non signé : défini

Un résultat qui ne tient pas sur \(w\) bits déborde ; la norme ne traite pas de la même façon les types non signés et signés.

Modulo \(2^w\) (C17 6.2.5 §9)

Un calcul sur des non signés ne déborde jamais au sens de la norme : le résultat est réduit modulo \(2^w\).

\[ \text{résultat} = (\text{valeur mathématique}) \bmod 2^w \]

Sur unsigned int : UINT_MAX + 1 \(= 0\), 0u - 1 \(= 4294967295\). Sur uint8_t, même résultat, mais par la conversion au retour : \(255+1 \to 0\), \(0-1 \to 255\), \(200+100 \to 44\).

Détection

Avant le calcul : a + b dépasse UINT_MAX si et seulement si b > UINT_MAX - a.

Après le calcul : une somme non signée plus petite qu’un de ses opérandes signale une retenue perdue.

Petits types

uint8_t et int8_t sont promus en int : le calcul se fait sur 32 bits, puis le résultat est converti au retour dans la variable.

Addition avec débordement, pas à pas

Bits relevés sous gdb ; les retenues et le 9e bit sont calculés à partir de a et b.

Sortie \(\rightarrow\)

s = 44   retenue perdue

Lecture. a \(=200 = 1100\,1000_2\).

Lecture. b \(=100 = 0110\,0100_2\).

Lecture. Addition colonne par colonne, retenues en rouge ; \(300\) demande 9 bits : le 9e (poids 256) ne tient pas dans s, il est perdu.

Lecture. a + b se calcule en int (300, sans débordement) ; la conversion vers uint8_t réduit modulo 256 : 44, toujours défini (C17 6.3.1.3 §2).

Lecture. Une somme non signée plus petite qu’un opérande trahit la retenue perdue : \(44 < 200\).

Lecture. Test vrai : la retenue est détectée après coup, sans comportement indéfini.

Débordement signé : indéfini

int8_t c = 127;
c = c + 1;
printf("c = %d\n", c);
int x = INT_MAX;
int y = x + 1;
printf("y = %d = 0x%08x\n", y, (unsigned int)y);

Résultat \(\rightarrow\)

c = -128
y = -2147483648 = 0x80000000

C17 6.5 §5

Si le résultat d’une opération sur des signés n’est pas représentable dans son type, le comportement est indéfini : aucun résultat n’est garanti, et le compilateur peut supposer que cela n’arrive pas.

Sortie d’erreur avec -fsanitize=undefined \(\rightarrow\)

s5_signe.c:11:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

Lecture. c + 1 se calcule en int (128, sans débordement) ; c’est la conversion vers int8_t qui donne \(-128\), définie par l’implémentation. x + 1, en int, déborde : indéfini. Ici 0x7fffffff + 1 donne 0x80000000, le \(-2147483648\) de la Séance 1, et -fsanitize=undefined le signale à la ligne 11 ; en Séance 1, gcc avait déjà remplacé x + 1 < x par « faux ».

Détecter avant de calculer

int deborde(int a, int b)
{
    return (b > 0 && a > INT_MAX - b)
        || (b < 0 && a < INT_MIN - b);
}

int main(void)
{
    int a = 2000000000;
    int b = 1000000000;
    printf("deborde : %d\n", deborde(a, b));
    int r;
    int o = __builtin_add_overflow(a, b, &r);
    printf("builtin : %d\n", o);
    return 0;
}

Résultat \(\rightarrow\)

deborde : 1
builtin : 1

Signé

a + b déborde si b > 0 && a > INT_MAX - b ou si b < 0 && a < INT_MIN - b : ces deux soustractions ne débordent jamais. gcc fournit aussi __builtin_add_overflow.

Failles réelles

CVE-2009-1385 : un {integer underflow} sur une longueur de trame, dans le pilote réseau e1000 de Linux, permettait de faire paniquer le noyau à distance.

Lecture. \(2\,000\,000\,000 > 2\,147\,483\,647 - 1\,000\,000\,000\) : la somme ne tiendrait pas dans un int, elle n’est pas calculée. __builtin_add_overflow range la somme réduite modulo \(2^{32}\) dans r et rend 1.

Exercice : additions sur 8 bits

type opération octet valeur
uint8_t \(240 + 20\) 0x04 4
uint8_t \(250 + 10\) 0x04 4
uint8_t \(5 - 9\) 0xfc 252
uint8_t \(100 - 150\) 0xce 206
int8_t \(127 + 1\) 0x80 -128
int8_t \(-128 - 1\) 0x7f 127
int8_t \(100 + 20\) 0x78 120
int8_t \(-100 - 50\) 0x6a 106

Énoncé

Pour chaque ligne, donner l’octet résultat et sa valeur, puis dire si le résultat est défini, défini par l’implémentation ou indéfini. Que vaut -x pour int x = INT_MIN ?

Correction

  1. Calcul en int : 260 deux fois ; conversion vers uint8_t, modulo 256 : 4, défini (C17 6.3.1.3 §2).
  2. \(-4 + 256 = 252\) ; \(-50 + 256 = 206\) : défini, par la même conversion.
  3. Calcul en int : 128 et \(-129\) ; conversion vers int8_t : \(128-256 = -128\), \(-129 + 256 = 127\), défini par l’implémentation.
  4. 120 tient ; \(-150 + 256 = 106\) (implémentation).
  5. \(-(-2147483648) = 2147483648\) ne tient pas dans un int : débordement, indéfini, aucune valeur à donner.

Opérations bit à bit

Opérateurs bit à bit et masques

Les opérateurs de la Séance 1 agissent colonne par colonne ; un masque choisit les bits sur lesquels agir.

opérateur bit résultat
a & b 1 si les deux bits valent 1
a | b 1 si l’un au moins vaut 1
a ^ b 1 si les deux bits diffèrent
~a l’inverse de chaque bit
a << k bits décalés de \(k\) vers les poids forts
a >> k bits décalés de \(k\) vers les poids faibles

Quatre gestes sur le bit \(k\)

Tester : (x >> k) & 1

Mettre à 1 : x | (1u << k)

Mettre à 0 : x & ~(1u << k)

Inverser : x ^ (1u << k)

Extraire \(n\) bits à partir de \(k\) : (x >> k) & ((1u << n) - 1)

Types non signés

Les masques s’écrivent en non signé (1u) : 1 << 31 est indéfini sur un int, et >> d’un négatif dépend de l’implémentation.

Masques, pas à pas

Bits relevés sous gdb ; la rangée « masque » montre le masque de la ligne exécutée.

Sortie \(\rightarrow\)

0 a   a7 a1 25

Lecture. x \(=\) 0xa5 \(= 1010\,0101_2\).

Lecture. Tester : x >> 3 amène le bit 3 en position 0, & 1 garde ce seul bit : 0.

Lecture. Mettre à 1 : | avec un masque qui n’a que le bit 1 ; les autres bits sont recopiés.

Lecture. Mettre à 0 : & avec le complément du masque (tous les bits à 1 sauf le bit 2).

Lecture. Inverser : ^ avec le masque change le bit 7, et lui seul.

Lecture. Extraire un champ : x >> 4 amène les bits 7 à 4 en bas, & 0x0f efface le reste : 0xa.

Lecture. Le bit 3 testé et le champ extrait.

Lecture. x n’a jamais changé : chaque opération produit une nouvelle valeur.

Décalages, pas à pas

Bits relevés sous gdb ; en rouge, les bits entrés par le décalage.

Sortie \(\rightarrow\)

-96 -6
ffffffa0
0ffffffa
80

Lecture. s \(=-96\) \(=\) 0xffffffa0.

Lecture. >> d’un négatif : défini par l’implémentation (C17 6.5.7 §5) ; gcc recopie le bit de signe (décalage arithmétique) : \(-96/16=-6\).

Lecture. Mêmes bits, type non signé : \(2^{32}-96\).

Lecture. >> d’un non signé : des 0 entrent à gauche (décalage logique) ; \(\lfloor 4294967200/16 \rfloor\).

Lecture. << 4 : des 0 entrent à droite, \(5 \times 2^4 = 80\) ; sur un signé, << est indéfini si le résultat ne tient pas (C17 6.5.7 §4).

Lecture. Le signe est conservé par >> sur int (gcc).

Lecture. u : les bits de s.

Lecture. Sur unsigned, >> divise par \(2^4\) sans tenir compte du bit 31.

Lecture. m affiché : 80. Un décalage de 32 positions ou plus, ou d’un nombre négatif, serait indéfini (C17 6.5.7 §3).

Exercice : masques sur un registre de 16 bits

uint16_t r = 0x5a3c; : ses 16 bits, du bit 15 au bit 0 :

15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
00 1 00 1 11 00 11 00 00 00 11 11 1 1 0 00

Énoncé

  1. (r >> 4) & 0xf ;
  2. (r >> 13) & 1 ;
  3. r | 0x0001 ;
  4. r & ~0x0f00 ;
  5. r ^ 0x8000 ;
  6. le nombre de bits à 1.

Correction

  1. Bits 7 à 4 : \(0011_2\), soit 3 (le chiffre hexadécimal 3).
  2. Bit 13 : 0 (5 \(= 0101_2\) pour les bits 15 à 12).
  3. Bit 0 mis à 1 : 0x5a3d.
  4. Bits 11 à 8 mis à 0 : 0x503c.
  5. Bit 15 inversé : 0xda3c.
  6. \(2 + 2 + 2 + 2 =\) 8 (un chiffre hexadécimal à la fois).

Flottants IEEE 754

Nombres binaires fractionnaires

Les entiers ne couvrent ni les très grands nombres (\(1{,}5\times10^{11}\) m), ni les très petits (\(9{,}11\times10^{-31}\) kg), ni les fractions. Une virgule fixe gaspille des bits : la partie entière ne sert à rien pour \(0{,}000001\), la partie fractionnaire pour \(10^{15}\) ; la virgule doit « flotter ».

Bits après la virgule

\[ b_m\dots b_0\,,\,b_{-1}\dots b_{-n} = \sum_{i=-n}^{m} b_i\, 2^i \]

À droite de la virgule, les poids sont \(\frac12\), \(\frac14\), \(\frac18\), …

\(101{,}11_2 = 4 + 1 + 0{,}5 + 0{,}25 = 5{,}75\). Un nombre n’a une écriture binaire finie que s’il est de la forme \(k/2^n\) : \(0{,}1\) n’en a pas (\(0{,}0\,0011\,0011\dots_2\)).

double v = 5.75;
printf("%a\n", v);
printf("%g\n", 0x1.7p2);
printf("%.20f\n", 0.1);

Résultat \(\rightarrow\)

0x1.7p+2
5.75
0.10000000000000000555

Lecture. %a affiche un double en hexadécimal : 0x1.7p+2 se lit \(1{,}0111_2 \times 2^2 = 101{,}11_2\). Le double le plus proche de 0,1 vaut \(0{,}1000000000000000055\dots\)

Quoi, pourquoi, quand ça échoue : IEEE 754

Quoi

\(V = (-1)^s \times M \times 2^E\). Un float ({binary32}) : 1 bit de signe \(s\), 8 bits d’exposant \(e\), 23 bits de fraction \(f\) ; \(E = e - 127\) (exposant biaisé), \(M = 1{,}f\) (le 1 de tête n’est pas stocké). Un double : 1, 11 et 52 bits, biais 1023.

Pourquoi

La précision devient relative : 24 bits significatifs (environ 7 chiffres décimaux) pour un float, 53 (environ 16) pour un double, quelle que soit la grandeur du nombre, de \(10^{-38}\) à \(10^{38}\) en float.

Quand ça échoue

La plupart des décimaux sont arrondis (0,1) ; un petit terme ajouté à un grand disparaît ; l’addition n’est pas associative ; un résultat trop grand devient \(\pm\infty\), une opération sans valeur donne NaN.

Encoder 13,0 en float

  1. Signe : \(13{,}0 > 0\), \(s = 0\).
  2. Binaire : \(13 = 1101_2\).
  3. Normaliser : \(1101{,}0_2 = 1{,}101_2 \times 2^3\), \(E = 3\).
  4. Exposant biaisé : \(3 + 127 = 130 = 1000\,0010_2\).
  5. Fraction : 101 suivi de 20 zéros (le 1 de tête est implicite).
  6. Assembler signe, exposant, fraction.
0 10000010 10100000000000000000000
\(s\) \(e = 130\) \(f\) (23 bits)

Groupes de 4 : 0100 0001 0101 0000 … \(=\) 0x41500000. Vérification : \(1{,}101_2 \times 2^3 = 1{,}625 \times 8 = 13\).

    float f = 13.0f;
    uint32_t u;
    memcpy(&u, &f, sizeof u);
    printf("0x%08x :", u);
    show_bytes((byte_pointer)&f, sizeof f);

Résultat \(\rightarrow\)

0x41500000 : 00 00 50 41

Lecture. show_bytes (Bryant et O’Hallaron, § 2.1) affiche les octets dans l’ordre des adresses : 00 00 50 41, l’octet de poids faible d’abord.

Un float en mémoire, pas à pas

Adresses simplifiées ; sur la machine, f est en 0x7fffffffebe4, u 4 octets plus loin.

Sortie \(\rightarrow\)

u = 0xc15a0000
1 130 0x5a0000
E = 3

Lecture. Les 4 octets de f, adresse croissante : 00 00 5a c1 ; little-endian : le poids faible d’abord.

Lecture. memcpy recopie les 4 octets dans un entier : u \(=\) 0xc15a0000, lu de l’octet +3 vers l’octet +0.

Lecture. Signe : u >> 31, le bit 31 : 1, nombre négatif.

Lecture. Exposant biaisé : bits 30 à 23, (u >> 23) & 0xff \(= 1000\,0010_2 = 130\) ; \(E = 130-127 = 3\).

Lecture. Fraction : les 23 bits de poids faible, \(101101\) puis 17 zéros ; \(M = 1{,}101101_2 = 1{,}703125\).

Lecture. u affiché en hexadécimal : l’ordre des chiffres est celui des poids, pas celui de la mémoire.

Lecture. Les trois champs.

Lecture. \(V = (-1)^1 \times 1{,}703125 \times 2^3 = -13{,}625\).

Zéros, infinis, NaN, dénormalisés

catégorie \(e\) \(f\) valeur
zéro 0 0 \(\pm 0\)
dénormalisé 0 \(\ne 0\) \((-1)^s\, 0{,}f \times 2^{-126}\)
normalisé 1 à 254 tout \((-1)^s\, 1{,}f \times 2^{e-127}\)
infini 255 0 \(\pm\infty\)
NaN 255 \(\ne 0\) {not a number}

NaN

NaN n’est égal à rien, pas même à lui-même : x == x est faux. Tester avec isnan(x). \(+0\) et \(-0\) sont égaux pour ==, mais n’ont pas les mêmes bits.

Résultat \(\rightarrow\)

+0      00000000
-0      80000000
+inf    7f800000
-inf    ff800000
0/0     ffc00000
FLT_MAX 7f7fffff 3.40282e+38
FLT_MIN 00800000 1.17549e-38
denorm  00000001 1.4013e-45
nan == nan : 0
isnan(nan) : 1
+0 == -0   : 1

Lecture. \(1/0\) donne \(+\infty\) (7f800000). \(0/0\) donne ici ffc00000 : un NaN dont le bit de signe vaut 1, celui que produit le processeur. Le plus petit dénormalisé vaut \(2^{-149} \approx 1{,}4 \times 10^{-45}\).

Exercice : des float dans un dump

Machine little-endian ; float *f = (float *)0x00;

Addr +0 +1 +2 +3 +4 +5 +6 +7
0x00 0000 0000 d0d0 c0c0 0000 0000 4848 c1c1
0x08 0000 0000 8080 7f7f 0000 0000 c0c0 7f7f

Énoncé

  1. Le mot de 32 bits de f[0] ;
  2. ses champs \(s\), \(e\), \(f\) ;
  3. sa valeur ;
  4. la valeur de f[1] ;
  5. celle de f[2] ;
  6. celle de f[3].

Correction

  1. 00 00 d0 c0 lus de +3 vers +0 : 0xc0d00000.
  2. \(1100\,0000\,1101\,0000\dots\) : \(s = 1\), \(e = 1000\,0001_2 = 129\), \(f = 101\) puis des zéros.
  3. \(E = 2\), \(M = 1{,}101_2 = 1{,}625\) : \(-1{,}625 \times 4 =\) \(-6{,}5\).
  4. 0xc1480000 : \(e = 130\), \(M = 1{,}1001_2 = 1{,}5625\) : \(-12{,}5\).
  5. 0x7f800000 : \(e = 255\), \(f = 0\) : \(+\infty\).
  6. 0x7fc00000 : \(e = 255\), \(f \ne 0\) : NaN.

0,1 + 0,2 en mémoire, pas à pas

Adresses simplifiées ; un double par ligne ; sur la machine, a est en 0x7fffffffebe0.

Sortie \(\rightarrow\)

c = 0.30000000000000004
t = 0.29999999999999999
c == t : 0

Lecture. 0,1 n’a pas d’écriture binaire finie : a reçoit le double le plus proche (0x3fb999999999999a).

Lecture. 0,2 de même : même mantisse, exposant plus grand de 1 (b9 devient c9).

Lecture. La somme exacte des deux approximations est arrondie au double le plus proche : ...333334.

Lecture. Le double le plus proche de 0,3 est ...333333.

Lecture. 17 chiffres significatifs montrent l’écart : c est au-dessus de 0,3.

Lecture. t est juste au-dessous de 0,3.

Lecture. Les deux mots ne diffèrent que d’une unité sur l’octet de poids faible (34 contre 33 en +0), soit \(2^{-54} \approx 5{,}6\times10^{-17}\) : == rend 0.

Absorption, accumulation, comparaison

float a = 0.1f + 0.2f;
float b = 0.3f;
printf("float  : %d\n", a == b);
double c = 0.1 + 0.2;
printf("double : %d\n", c == 0.3);
float x = 3.14f;
float big = 1e10f;
printf("%f %f\n", (x + big) - big, x + (big - big));
float acc = 0.0f;
for (int i = 0; i < 1000000; i++)
    acc += 0.1f;
printf("%.2f\n", acc);
printf("%d\n", fabs(c - 0.3) < 1e-9);

Résultat \(\rightarrow\)

float  : 1
double : 0
0.000000 3.140000
100958.34
1

Lecture. En float, 0.1f + 0.2f et 0.3f sont arrondis au même float : l’égalité est vraie, par chance ; en double, elle est fausse. 3.14f est absorbé par \(10^{10}\) (en float, deux voisins de \(10^{10}\) sont distants de 1024). Un million d’ajouts de 0.1f donnent 100958,34 au lieu de \(100\,000\). Comparer deux réels se fait avec une tolérance : fabs(c - 0.3) < 1e-9.

Quand l’arrondi coûte cher

long n = (long)(0.1 * 8388608);
double stocke = n / 8388608.0;
double erreur = 0.1 - stocke;
long tics = 100L * 3600 * 10;
double derive = erreur * tics;
printf("erreur par tic : %.3e s\n", erreur);
printf("apres 100 h    : %.4f s\n", derive);
printf("distance       : %.0f m\n", derive * 1676);

Résultat \(\rightarrow\)

erreur par tic : 9.537e-08 s
apres 100 h    : 0.3433 s
distance       : 575 m

Patriot, Dhahran, 25 février 1991

Dixièmes de seconde multipliés par 0,1, rangé avec 23 bits après la virgule (registre à virgule fixe de 24 bits). Après 100 h : dérive de 0,34 s, soit 575 m parcourus par le Scud ; fenêtre de poursuite décalée de 687 m (GAO). Scud non intercepté : 28 soldats tués.

Softmax en IA

\(e^{1000} = +\infty\), et \(\infty/\infty\) donne NaN (-nan : bit de signe à 1) ; soustraire d’abord le maximum (s8_softmax.c) :

naif   : -nan
stable : 0.665241

Lecture. IA : float16 (1, 5, 10 bits), bfloat16 (1, 8, 7 bits), int8 quantifié.

Exercice : prévoir la sortie

Énoncé. Prévoir les six lignes affichées (quatre questions), et justifier chaque valeur.

float a = 0.7f + 0.1f;
float b = 0.8f;
printf("a == b ? %d\n", a == b);
double c = 0.7 + 0.1;
printf("c == 0.8 ? %d\n", c == 0.8);
float x = 1e30f * 1e10f;
printf("x = %f\n", x);
printf("isinf(x) = %d\n", isinf(x) != 0);
float y = x - x;
printf("y == y ? %d\n", y == y);
printf("isnan(y) = %d\n", isnan(y) != 0);

Correction

  1. 1 : la somme float est arrondie sur 0.8f (0x1.99999ap-1).
  2. 0 : en double, 0.79999999999999993 contre 0.80000000000000004.
  3. \(10^{40} >\) FLT_MAX : inf, isinf : 1.
  4. \(\infty - \infty\) : NaN ; y == y : 0, isnan : 1.

Résultat \(\rightarrow\)

a == b ? 1
c == 0.8 ? 0
x = inf
isinf(x) = 1
y == y ? 0
isnan(y) = 1

Synthèse

Bonnes pratiques

Types de taille fixe

<stdint.h> : int8_t … int64_t, uint8_t … uint64_t ; <inttypes.h> pour les afficher : printf("%" PRIu32 "\n", n).

Comparaisons mixtes

Tester le signe d’abord : i >= 0 && (unsigned)i < n ; ne pas mélanger int et size_t dans une condition.

Calculs

Vérifier avant une addition signée ; manipuler les bits sur des non signés ; comparer des réels avec une tolérance ; préférer double à float.

Options de gcc

gcc -std=c17 -Wall -Wextra -Wconversion \
    -fsanitize=undefined -g prog.c

-Wsign-compare est inclus dans -Wextra ; -Wconversion signale les conversions qui peuvent perdre des bits.

À retenir

À retenir

  1. Un octet, c’est 8 bits, deux chiffres hexadécimaux ; un motif de \(w\) bits vaut \(\sum b_i 2^i\) en non signé, et le bit de poids fort pèse \(-2^{w-1}\) en complément à deux.
  2. \(-x = \mathord{\sim}x + 1\) ; \(-1\) a tous ses bits à 1 ; \([-2^{w-1}, 2^{w-1}-1]\) n’est pas symétrique.
  3. Entre signé et non signé de même taille, les bits restent, la lecture change ; un int comparé à un unsigned devient unsigned.
  4. Extension : zéros (non signé) ou bit de signe recopié (signé) ; troncature : les bits de poids faible restent.
  5. Non signé : modulo \(2^w\), défini ; conversion vers un signé trop petit : définie par l’implémentation ; débordement signé : indéfini.
  6. Masques : & teste ou efface, | met à 1, ^ inverse, >> et & extraient un champ.
  7. IEEE 754 : \((-1)^s \times 1{,}f \times 2^{e-127}\) en float ; le dump est little-endian ; 0,1 est arrondi, == entre réels est un piège.

Prochaine séance

Séance 3 : organisation de la mémoire

Les octets de cette séance prennent une adresse : mot machine et endianness, pointeurs et indirection, tableaux et arithmétique des pointeurs, chaînes, pile et tas, malloc et free, et show_bytes pour afficher n’importe quel objet.

À préparer

Le TD et le TP de la séance 2 : conversions de base, dumps little-endian à lire, conversions et débordements à prévoir, masques, encodage IEEE 754.

Références

  • R. E. Bryant, D. R. O’Hallaron, Computer Systems: A Programmer’s Perspective, 3e éd., Pearson, 2016 : § 2.1 (hexadécimal, show_bytes, opérations bit à bit), § 2.2 (entiers non signés, complément à deux, conversions, extension, troncature), § 2.3 (arithmétique, débordements), § 2.4 (IEEE 754, arrondi).
  • B. W. Kernighan, D. M. Ritchie, The C Programming Language, 2e éd., Prentice Hall, 1988 : § 2.7 (conversions), § 2.9 (opérateurs bit à bit).
  • K. N. King, C Programming: A Modern Approach, 2e éd., Norton, 2008 : chap. 7 (types de base, conversions), § 20.1 (opérateurs bit à bit).
  • ISO/IEC 9899:2018 (C17) : 6.2.5 §9 (non signés modulo \(2^w\)), 6.2.6.2 (représentation des entiers), 6.3.1.3 (conversions entières), 6.3.1.8 (conversions usuelles), 6.4.4.1 (type des littéraux), 6.5 §5 (débordement), 6.5.7 (décalages), annexe F (IEEE 754).
  • U.S. GAO, Patriot Missile Defense: Software Problem Led to System Failure at Dhahran, Saudi Arabia, IMTEC-92-26, 1992.

Mesures de la séance : x86-64, Linux 6.18, gcc 13.3.0 (-std=c17 -Wall -Wextra -O0 -g), glibc 2.39, gdb 15.1 ; adresses rendues déterministes par setarch -R.