C1 — Représentation de l'informationDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 1 · Architecture des ordinateurs

Cours 1Représentation de l'information

Écrire en binaire tout ce qu'une machine manipule — entiers signés, flottants, caractères, images — et savoir où chaque codage atteint sa limite.

2 chapitres · 12 h de travail estimé

  1. 1. Systèmes de numération6 h
  2. 2. Codage des données6 h

Chapitre 1 · 6 h

Systèmes de numération

Binaire, octal, hexadécimal et conversions dans les deux sens ; arithmétique binaire ; poids des bits, octet et mot.

L'accent de ce site s'écrit #4f46e5. Trois octets : un peu de rouge, un peu de vert, beaucoup de bleu. Une adresse réseau s'écrit 192.168.1.1, un masque /24, et la mémoire d'un programme qui plante s'affiche en colonnes de 0x7ffd3a20. Aucune de ces notations n'est décorative : elles disent toutes la même chose, un nombre, dans la base qui rend ce nombre lisible.

Une machine ne connaît qu'un alphabet à deux lettres. Tout ce que ce cours étudiera — instructions, adresses, images, sons — est une suite de zéros et de uns. Ce premier chapitre apprend à lire cette suite, à la traduire, et à y calculer. C'est le socle : le complément à deux du chapitre 2, les tables de vérité du chapitre 3 et les modes d'adressage du chapitre 6 sont tous écrits dans cette langue.

Compter, c'est pondérer

Notre système décimal n'a rien d'évident, et le comprendre en tant que système est ce qui permet d'en changer. Écrire 3705, c'est écrire une somme :

3705 = 3 × 10³  +  7 × 10²  +  0 × 10¹  +  5 × 10⁰     = 3000     +  700      +  0        +  5

Deux ingrédients seulement. Une base bb — ici dix — qui fixe le nombre de chiffres disponibles, de 00 à b1b-1. Et une position, qui donne à chaque chiffre son poids : une puissance de la base, croissante de droite à gauche à partir de b0b^0.

Rien dans cette mécanique n'impose b=10b = 10. Le choix de dix est anatomique, pas mathématique. En base bb quelconque, un nombre écrit dn1d1d0d_{n-1} \dots d_1 d_0 vaut :

N=i=0n1di×biN = \sum_{i=0}^{n-1} d_i \times b^{\,i}

Et la propriété qui servira partout : avec nn chiffres en base bb, on écrit exactement bnb^n valeurs différentes, de 00 à bn1b^n - 1. Huit chiffres binaires donnent 28=2562^8 = 256 valeurs, donc de 0 à 255 — le 255 des masques réseau et des composantes de couleur n'est pas un nombre rond arbitraire, c'est la dernière valeur qui tient sur un octet.

Trois bases, trois raisons

Le cours n'en emploiera que trois, et chacune existe pour une raison distincte.

La base 2 est la seule que le matériel connaisse. Un fil est sous tension ou ne l'est pas ; un transistor conduit ou bloque. Deux états discernables de façon fiable — trois seraient techniquement possibles et le sont si peu qu'on n'en fait pas d'ordinateurs. Le chiffre binaire s'appelle un bit (binary digit).

La base 16, dite hexadécimale, est une commodité d'écriture pour les humains. Ses chiffres vont de 0 à 9 puis A à F, où A vaut 10 et F vaut 15. Son intérêt tient à une coïncidence : 16=2416 = 2^4, donc un chiffre hexadécimal vaut exactement quatre bits. Un octet s'écrit sur deux caractères au lieu de huit, sans le moindre calcul.

La base 8, octale, joue le même rôle avec 8=238 = 2^3 : un chiffre pour trois bits. On la rencontre surtout dans les permissions Unix — le chmod 755 du cours de systèmes est un nombre octal, trois groupes de trois bits.

DécimalBinaireOctalHexadécimal
0000000
5010155
91001119
10101012A
15111117F
16100002010
25511111111377FF

Retenez la dernière ligne : FF = 255 = un octet plein. Elle revient constamment.

Quiz · 1 question

Pourquoi l'hexadécimal est-il universellement employé pour afficher le contenu de la mémoire, plutôt que l'octal ou le décimal ?

  • Parce qu'il permet d'écrire de plus grands nombres avec moins de chiffres que toute autre basecompacité
  • Parce que 16 = 2⁴ : un chiffre hexadécimal vaut exactement 4 bits, donc un octet s'écrit sur 2 caractères sans aucun calculalignement sur l'octet
  • Parce que les processeurs calculent nativement en base 16calcul natif

Réponse : Le processeur ne calcule qu'en binaire ; l'hexadécimal n'existe que pour l'œil humain. Son avantage n'est pas la compacité en soi — la base 10 est plus compacte encore — mais l'ALIGNEMENT : 16 étant une puissance de 2, la conversion binaire ↔ hexadécimal se fait par simple découpage en groupes de 4 bits, sans division. Le décimal, lui, n'est pas une puissance de 2 : passer de 0b11010110 à 214 demande un vrai calcul, et l'inverse aussi. L'octal a la même propriété avec 3 bits, mais l'informatique s'organise en octets, donc en multiples de 4 bits.

Convertir dans les deux sens

Quatre procédés suffisent, et il faut les avoir tous automatisés.

Base bb → décimal : la somme pondérée. On applique la définition. Pour 1011₂ : 1×8+0×4+1×2+1×1=111{\times}8 + 0{\times}4 + 1{\times}2 + 1{\times}1 = 11. Pour 2AF₁₆ : 2×256+10×16+15=6872{\times}256 + 10{\times}16 + 15 = 687.

En pratique on emploie le schéma de Horner, qui évite de calculer les puissances : partir de 0, et pour chaque chiffre de gauche à droite, multiplier l'accumulateur par la base puis ajouter le chiffre. Pour 2AF₁₆ : 0×16+2=20{\times}16+2 = 2, puis 2×16+10=422{\times}16+10 = 42, puis 42×16+15=68742{\times}16+15 = 687. Trois multiplications, aucune puissance.

Décimal → base bb : les divisions successives. On divise par la base, on note le reste, on recommence sur le quotient jusqu'à zéro. Les restes, lus de bas en haut, donnent l'écriture.

Convertir 214 en binaire 214 ÷ 2 = 107  reste 0   ← bit de poids faible107 ÷ 2 =  53  reste 1 53 ÷ 2 =  26  reste 1 26 ÷ 2 =  13  reste 0 13 ÷ 2 =   6  reste 1  6 ÷ 2 =   3  reste 0  3 ÷ 2 =   1  reste 1  1 ÷ 2 =   0  reste 1   ← bit de poids fort Lecture de bas en haut : 11010110

L'erreur de débutant est de lire les restes de haut en bas. Le contrôle est immédiat : le premier reste obtenu est celui de la division par 2, donc il vaut 1 si et seulement si le nombre est impair — c'est forcément le bit le plus à droite.

Binaire ↔ hexadécimal : le découpage. Aucune arithmétique. On groupe les bits par quatre en partant de la droite, en complétant par des zéros à gauche si besoin, et on traduit chaque groupe.

11010110  →  1101 | 0110  →  D | 6  →  0xD60x4F46E5  →  0100 1111 0100 0110 1110 0101

Avec l'octal, même chose par groupes de trois. Et pour passer de l'hexadécimal à l'octal, on transite par le binaire : il n'existe pas de raccourci direct, 1616 et 88 n'étant pas puissances l'une de l'autre.

Calculer en binaire

L'addition binaire tient en quatre lignes, dont la dernière est la seule à retenir :

0 + 0 = 00 + 1 = 11 + 0 = 11 + 1 = 0  avec une retenue de 1 sur la colonne suivante

C'est exactement l'addition posée de l'école primaire, avec une retenue qui se déclenche à 2 au lieu de 10. Et c'est aussi, littéralement, le circuit du chapitre 4 : la colonne « somme » est un OU exclusif, la colonne « retenue » est un ET.

   1 1 1        ← retenues   0 1 0 1 1    (11) + 0 0 1 1 1    ( 7) ───────────   1 0 0 1 0    (18)

La multiplication est plus simple qu'en décimal, parce que la table de multiplication binaire ne contient que des 0 et des 1 : chaque ligne du produit posé est soit une copie décalée du premier facteur, soit une ligne de zéros. D'où un fait qui servira au chapitre 6 : multiplier par 2 revient à décaler tous les bits d'un rang vers la gauche, diviser par 2 à décaler vers la droite. Un processeur qui décale plutôt que de multiplier gagne beaucoup de temps, et un compilateur remplace systématiquement x * 8 par un décalage de 3.

La soustraction binaire posée existe, avec ses emprunts. On ne l'enseignera pas : le chapitre 2 montrera qu'aucune machine ne l'utilise. Elles additionnent l'opposé, et c'est tout l'intérêt du complément à deux.

Bit, octet, mot

Le bit est l'unité. Il ne se manipule presque jamais seul.

L'octet (byte) est le groupe de 8 bits, et l'unité d'adressage de toutes les machines courantes : la mémoire est un tableau d'octets, et chaque octet a une adresse. Un octet code 28=2562^8 = 256 valeurs.

Le mot (word) est la quantité que le processeur traite en une opération : 32 ou 64 bits aujourd'hui. C'est aussi, en général, la largeur des registres et du bus de données du chapitre 5. Attention au faux ami : la taille du mot dépend de la machine, celle de l'octet non.

Dernier point, source d'une confusion tenace. Les préfixes du système international valent mille : un kilooctet (ko) vaut 103=100010^3 = 1000 octets. Les préfixes binaires valent 1024 : un kibioctet (Kio) vaut 210=10242^{10} = 1024 octets. L'écart est de 2,4 % au kilo et grimpe à 7,4 % au téra — c'est toute la différence entre le disque « 1 To » du fabricant et les « 931 Gio » qu'affiche le système, et cela n'a rien d'une escroquerie : ce sont deux unités différentes portant des noms trop proches.

Quiz · 1 question

Combien de valeurs distinctes peut-on coder sur 12 bits, et combien de bits faut-il au minimum pour coder les 26 lettres de l'alphabet ?

  • 4096 valeurs, et 5 bits pour l'alphabet2¹² et 2⁵
  • 2048 valeurs, et 4 bits pour l'alphabet2¹¹ et 2⁴
  • 4096 valeurs, et 26 bits pour l'alphabetun bit par lettre

Réponse : Douze bits codent 2¹² = 4096 valeurs, de 0 à 4095. Pour l'alphabet, on cherche le plus petit n tel que 2ⁿ ≥ 26 : 2⁴ = 16 ne suffit pas, 2⁵ = 32 suffit — donc 5 bits, avec 6 combinaisons inutilisées. C'est le raisonnement de dimensionnement le plus fréquent du cours : il donnera la largeur du bus d'adresses au chapitre 5 et le nombre de bits de déplacement dans une page au chapitre 7. La troisième réponse confond codage binaire et codage « un bit par élément », qui gaspillerait 21 bits.

À vous

L'exercice implémente les deux conversions dans les deux sens, puis vérifie qu'elles sont réciproques sur une série de nombres. C'est le meilleur contrôle qui soit : une conversion juste dans un sens et fausse dans l'autre se repère immédiatement.

Attention au piège volontaire du squelette : les restes des divisions successives doivent être lus de bas en haut, et le code fourni les concatène dans le mauvais ordre.

Exercice de code

Réparez la conversion décimal → base b, et vérifiez que l'aller-retour est fidèle.

Point de départ

const CHIFFRES = "0123456789ABCDEF";

// Base b -> décimal, par le schéma de Horner : on parcourt les chiffres de
// gauche à droite, on multiplie l'accumulateur par la base, on ajoute.
function versDecimal(texte, base) {
  let n = 0;
  for (const c of texte.toUpperCase()) {
    n = n * base + CHIFFRES.indexOf(c);
  }
  return n;
}

// Décimal -> base b, par divisions successives.
function versBase(n, base) {
  if (n === 0) return "0";
  let sortie = "";
  while (n > 0) {
    const reste = n % base;
    sortie = sortie + CHIFFRES[reste];   // ← les restes s'accumulent... dans quel sens ?
    n = Math.floor(n / base);
  }
  return sortie;
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. versBase est fausse : les restes sont lus à l'envers. Corrigez-la.
// 2. Vérifiez que l'aller-retour redonne bien le nombre de départ.

const CAS = [
  { n: 214, base: 2,  attendu: "11010110" },
  { n: 214, base: 16, attendu: "D6" },
  { n: 687, base: 16, attendu: "2AF" },
  { n: 493, base: 8,  attendu: "755" },
  { n: 255, base: 2,  attendu: "11111111" },
  { n: 0,   base: 2,  attendu: "0" },
];

for (const c of CAS) {
  const ecrit = versBase(c.n, c.base);
  const relu = versDecimal(ecrit, c.base);
  const ok = ecrit === c.attendu && relu === c.n ? "  ok" : "  X attendu " + c.attendu;
  console.log(
    String(c.n).padStart(4) + " en base " + String(c.base).padStart(2) +
    " -> " + ecrit.padEnd(10) + " -> " + String(relu).padStart(4) + ok
  );
}

Solution

const CHIFFRES = "0123456789ABCDEF";

function versDecimal(texte, base) {
  let n = 0;
  for (const c of texte.toUpperCase()) {
    n = n * base + CHIFFRES.indexOf(c);
  }
  return n;
}

function versBase(n, base) {
  if (n === 0) return "0";
  let sortie = "";
  while (n > 0) {
    // Le reste obtenu est le chiffre de POIDS LE PLUS FAIBLE de ce qui
    // reste : il s'écrit donc à GAUCHE de ce qu'on a déjà produit.
    sortie = CHIFFRES[n % base] + sortie;
    n = Math.floor(n / base);
  }
  return sortie;
}

const CAS = [
  { n: 214, base: 2,  attendu: "11010110" },
  { n: 214, base: 16, attendu: "D6" },
  { n: 687, base: 16, attendu: "2AF" },
  { n: 493, base: 8,  attendu: "755" },
  { n: 255, base: 2,  attendu: "11111111" },
  { n: 0,   base: 2,  attendu: "0" },
];

for (const c of CAS) {
  const ecrit = versBase(c.n, c.base);
  const relu = versDecimal(ecrit, c.base);
  const ok = ecrit === c.attendu && relu === c.n ? "  ok" : "  X attendu " + c.attendu;
  console.log(
    String(c.n).padStart(4) + " en base " + String(c.base).padStart(2) +
    " -> " + ecrit.padEnd(10) + " -> " + String(relu).padStart(4) + ok
  );
}

En travaux pratiques

Travaux pratiques 1 · 2 h

Compter comme la machine

Convertir sans outil, écrire son propre convertisseur, et rencontrer la limite qui gouverne tout le reste du cours : un nombre de bits fixé.

Avant de commencer

  • De quoi compiler ou exécuter un petit programme, dans le langage de votre choix
  • Une calculatrice en mode programmeur, pour vérifier — pas pour travailler

Énoncé

  1. À la main d'abordConvertissez 173, 255 et 1000 en binaire, octal et hexadécimal, par divisions successives. Vérifiez ensuite seulement, avec la calculatrice. Indice : Divisez par la base, notez le reste, recommencez avec le quotient. Les restes se lisent du dernier au premier.
  2. Votre convertisseurÉcrivez une fonction qui convertit un entier positif vers une base entre 2 et 16, sans utiliser la fonction toute faite du langage. Testez-la sur les trois valeurs précédentes.
  3. Pourquoi l'hexadécimal existeAffichez la même valeur, 3 735 928 559, en binaire et en hexadécimal. Comptez les caractères de chaque écriture, puis dites combien de bits représente un seul chiffre hexadécimal.
  4. Trouver le murSimulez un compteur sur 8 bits : partez de 250 et ajoutez 1, dix fois de suite, en gardant toujours 8 bits. Notez la suite obtenue et expliquez ce qui se passe entre deux valeurs.
  5. Le vrai débordementEn C, déclarez une variable de type unsigned char valant 255 et ajoutez-lui 1. Puis faites la même chose avec un char signé valant 127. Comparez les deux résultats. Indice : Les deux occupent 8 bits, mais ne les interprètent pas de la même façon — c'est le sujet du chapitre suivant.
  6. Lire de la mémoire bruteCréez un fichier texte contenant le mot « OK », puis affichez-le avec un outil hexadécimal. Retrouvez chaque octet dans la table ASCII.

C'est réussi quand

  • Vous convertissez 173 en hexadécimal sans calculatrice, en moins d'une minute
  • Votre convertisseur donne AD pour 173 en base 16
  • Vous savez dire où passe le bit perdu quand 255 devient 0

Correction

Les divisions successives
173 en base 2                173 en base 16
173 / 2 = 86  reste 1        173 / 16 = 10  reste 13 (D)
86 / 2 = 43  reste 0         10 / 16 =  0  reste 10 (A)
43 / 2 = 21  reste 1
21 / 2 = 10  reste 1        lecture à rebours : AD
10 / 2 =  5  reste 0
5 / 2 =  2  reste 1
2 / 2 =  1  reste 0
1 / 2 =  0  reste 1

lecture à rebours : 1010 1101

Le même nombre, trois écritures. La VALEUR ne change pas — seule la façon de l'écrire change. C'est la distinction que le chapitre entier cherche à installer, et celle qui manque à quiconque dit « convertir un nombre en binaire » comme s'il changeait de nature.

Le convertisseurconvertir.c
void convertir(unsigned int n, int base) {
  const char *chiffres = "0123456789ABCDEF";
  char tampon[33];
  int i = 0;

  if (n == 0) { printf("0\n"); return; }

  while (n > 0) {          /* on empile les restes */
      tampon[i++] = chiffres[n % base];
      n /= base;
  }
  while (i > 0)            /* on les dépile : ordre inverse */
      putchar(tampon[--i]);
  putchar('\n');
}

Les restes sortent dans le mauvais ordre, d'où le tampon lu à rebours. C'est exactement une pile — vous la reverrez formalisée en Algorithmique 2, et câblée en matériel au chapitre 5 pour les appels de fonction.

Compacité de l'hexadécimal
3735928559 en binaire : 1101 1110 1010 1101 1011 1110 1110 1111  (32 caractères)
3735928559 en hexa    : DEADBEEF                                    ( 8 caractères)

1 chiffre hexadécimal = 4 bits, exactement

L'hexadécimal n'apporte rien de nouveau : il regroupe les bits par quatre. C'est pour cela qu'un vidage mémoire s'écrit en hexadécimal et jamais en décimal — on peut retrouver chaque bit de tête, ce qui est impossible avec du décimal.

Le compteur qui repart de zéro
250 251 252 253 254 255 0 1 2 3

1111 1111   (255)
+ 0000 0001
-----------
10000 0000   ← 9 bits, mais il n'y a que 8 fils
0000 0000   ← ce qui reste après la troncature

Le bit de retenue n'est pas arrondi, ni signalé, ni perdu par erreur : il n'y a physiquement aucun fil pour le porter. Le processeur le met de côté dans un DRAPEAU de retenue — vous câblerez ce drapeau au TP 4, et un débordement non testé restera silencieux. C'est l'origine directe de l'échec du vol Ariane 5 (1996).

Signé ou non signé : deux lectures des mêmes bits
unsigned char u = 255; u + 1  →  0      (retour à zéro)
signed   char s = 127; s + 1  →  -128   (retour au minimum)

les deux valent 1000 0000 en mémoire, ou 0000 0000 selon le cas :
les BITS sont identiques, l'INTERPRÉTATION diffère

Aucun octet ne porte en lui l'information « je suis signé ». C'est le TYPE, connu du seul compilateur, qui décide comment lire les bits. Le chapitre 2 explique pourquoi 127 + 1 donne -128 et pas autre chose.

Ce que la suite en fait

Ce chapitre n'a manipulé que des entiers positifs, et c'est sa limite : rien de ce qui précède ne sait écrire 5-5, ni 3,143{,}14, ni la lettre « é ». Le chapitre 2 lève ces trois restrictions, et découvre au passage que chaque codage a un domaine hors duquel il ment silencieusement.

La suite pondérée sert aussi ailleurs qu'en conversion. La somme di2i\sum d_i 2^i est exactement ce que calcule un décodeur au chapitre 4, et le découpage d'une adresse en « numéro de page » et « déplacement » du chapitre 7 n'est rien d'autre qu'une coupure de cette somme en deux morceaux.

À retenir

Flashcards · 4 cartes

Comment convertir un décimal en base b, et quelle est l'erreur classique ?
Par divisions successives par b : on divise, on note le reste, on recommence sur le quotient jusqu'à obtenir 0. L'erreur classique est de lire les restes dans l'ordre où on les obtient. Il faut les lire DE BAS EN HAUT : le premier reste est celui de la division par b, donc le chiffre de poids le plus faible, celui le plus à droite.
Pourquoi la conversion binaire ↔ hexadécimal ne demande-t-elle aucun calcul ?
Parce que 16 = 2⁴. Un chiffre hexadécimal correspond donc exactement à 4 bits, et la conversion se réduit à un découpage en groupes de 4 bits depuis la droite, chacun traduit indépendamment. Même chose en octal avec 3 bits, puisque 8 = 2³. Le décimal n'a pas cette propriété : 10 n'est pas une puissance de 2.
Combien de valeurs sur n bits, et comment dimensionner un codage ?
n bits codent 2ⁿ valeurs, de 0 à 2ⁿ − 1. Pour coder k éléments distincts, on cherche le plus petit n tel que 2ⁿ ≥ k. Un octet (8 bits) code 256 valeurs, de 0 à 255 — d'où le 255 des masques réseau et des composantes de couleur.
Quelle est la différence entre un kilooctet (ko) et un kibioctet (Kio) ?
1 ko = 10³ = 1000 octets (préfixe du système international) ; 1 Kio = 2¹⁰ = 1024 octets (préfixe binaire). L'écart est de 2,4 % au kilo et de 7,4 % au téra : c'est pourquoi un disque vendu « 1 To » affiche 931 Gio. Deux unités différentes aux noms trop proches, pas une tromperie.

Chapitre 2 · 6 h

Codage des données

Entiers non signés, complément à deux et débordement ; virgule flottante IEEE 754 ; ASCII et Unicode ; numérisation des images et des sons.

Le 4 juin 1996, trente-sept secondes après le décollage, Ariane 5 se disloque. La cause tient en une ligne de code héritée d'Ariane 4 : une vitesse horizontale, stockée en flottant sur 64 bits, est convertie en entier signé sur 16 bits. Ariane 5 vole plus vite que sa devancière, la valeur ne tient pas dans le domaine, la conversion déborde. Le calculateur bascule en mode erreur, le second calculateur — même logiciel, même donnée — tombe une milliseconde plus tard, et 370 millions de dollars partent en fumée.

Ce chapitre est celui de ce genre d'accidents. Le chapitre 1 a montré comment écrire des entiers positifs ; il n'a rien dit du signe, de la virgule, ni des lettres. Chaque codage qu'on va poser résout un de ces manques — et chacun a un domaine hors duquel il ment sans prévenir. Reconnaître ce domaine est l'objectif réel du chapitre.

Entiers non signés : le tour du compteur

Sur nn bits sans signe, on code les entiers de 00 à 2n12^n - 1. Rien de plus, rien de moins. Un octet va de 0 à 255, un mot de 32 bits jusqu'à 4 294 967 295.

Que se passe-t-il en dépassant ? Le bit qui déborde à gauche n'a nulle part où aller : il est perdu. Sur 8 bits, 255+1255 + 1 donne 100000000, dont seuls les huit bits de droite sont conservés — soit 00000000, c'est-à-dire 0. L'arithmétique machine n'est pas celle des entiers : c'est l'arithmétique modulo 2n2^n, celle d'un compteur kilométrique qui repasse à zéro.

Ce n'est pas une anomalie, c'est le comportement défini du matériel. Ce qui est dangereux, c'est qu'il soit silencieux : aucune exception, aucun message, juste un résultat faux.

Le complément à deux

Il faut maintenant coder les négatifs. La première idée qui vient est aussi la mauvaise : réserver le bit de poids fort au signe et coder la valeur absolue dans le reste. Ce codage, dit signe-valeur absolue, a deux défauts rédhibitoires. Il possède deux zéros, 00000000 et 10000000, qu'il faut traiter séparément partout. Et surtout, l'addition ne fonctionne plus : additionner (+5)(+5) et (5)(-5) bit à bit donne 10001010, soit 10-10, ce qui n'a aucun sens. Il faudrait un circuit de soustraction distinct du circuit d'addition.

Le complément à deux résout les deux problèmes d'un coup, et c'est pourquoi toutes les machines l'emploient. Son principe : garder l'addition binaire telle quelle, et choisir le codage des négatifs pour qu'elle tombe juste.

L'idée est celle du compteur. Sur nn bits on compte modulo 2n2^n ; reculer de 5 revient donc à avancer de 2n52^n - 5. On définit le codage de x-x comme celui de 2nx2^n - x :

Sur 8 bits (2⁸ = 256)   +5  →  00000101  −5  →  256 − 5 = 251  →  11111011

Vérifions que l'addition marche, sans aucun circuit spécial :

   00000101   (+5) + 11111011   (−5) ──────────  100000000   → le 9ᵉ bit déborde et disparaît   00000000   → 0. Exact.

En pratique on n'effectue jamais la soustraction 2nx2^n - x. On utilise la règle des deux gestes, strictement équivalente : inverser tous les bits, puis ajouter 1.

   5  =  00000101   ~   →  11111010      (inversion bit à bit)   +1  →  11111011      = −5

La règle est involutive : l'appliquer à 5-5 redonne +5+5. C'est le meilleur contrôle d'une copie.

Quatre conséquences à connaître par cœur.

Le bit de poids fort est le bit de signe : 0 pour positif ou nul, 1 pour négatif. Ce n'est pas une convention plaquée, c'est une conséquence du codage.

Le domaine est dissymétrique. Sur nn bits on code de 2n1-2^{n-1} à +2n11+2^{n-1}-1 : sur 8 bits, de 128-128 à +127+127. Il y a un négatif de plus que de positifs, parce que zéro occupe une place du côté positif. Conséquence pratique : (128)-(-128) n'est pas représentable sur 8 bits, et la règle des deux gestes appliquée à 128-128 redonne 128-128.

L'extension de signe. Pour élargir un nombre de 8 à 32 bits, on ne complète pas par des zéros mais par le bit de signe répété. Sinon 5-5 deviendrait 251. C'est exactement l'instruction que le chapitre 6 nommera lb contre lbu.

Les mêmes bits, deux lectures. 11111011 vaut 251 non signé et 5-5 signé. Le motif ne porte pas son interprétation : c'est l'instruction du processeur qui décide, et c'est la raison pour laquelle le chapitre 6 distingue les comparaisons signées des non signées.

Quiz · 1 question

Sur 8 bits en complément à deux, on additionne 100 et 50. Que vaut le résultat lu par le processeur, et pourquoi ?

  • 150 : le résultat est correct, il tient sur 8 bitspas de débordement
  • −106 : la somme dépasse +127, le bit de signe bascule à 1 et le résultat devient négatifdébordement signé
  • 0 : le débordement remet le compteur à zéroremise à zéro

Réponse : 100 + 50 = 150, or le domaine signé sur 8 bits s'arrête à +127. Le motif obtenu est 10010110, dont le bit de poids fort vaut 1 : lu en complément à deux, il vaut 150 − 256 = −106. Le résultat tient parfaitement sur 8 bits — il n'y a aucune retenue sortante — mais il est faux, et c'est justement ce qui rend le débordement SIGNÉ traître : il ne se détecte pas par la retenue finale. Le processeur le repère autrement, en comparant la retenue entrante et la retenue sortante du bit de signe. Note que le même motif lu en NON signé vaut 150, valeur correcte : le débordement dépend de l'interprétation.

Détecter le débordement

Il faut distinguer deux cas, que le matériel signale par deux indicateurs différents.

Le débordement non signé se produit quand une retenue sort du bit de poids fort. Le processeur lève l'indicateur de retenue (carry).

Le débordement signé se produit quand le résultat sort de [2n1,2n11][-2^{n-1},\, 2^{n-1}-1]. Il se détecte ainsi : la retenue entrant dans le bit de signe diffère de la retenue qui en sort. Le processeur lève l'indicateur de débordement (overflow). Une règle plus simple à retenir en TD : additionner deux nombres de même signe et obtenir un résultat de signe opposé est un débordement signé — et additionner deux nombres de signes contraires ne peut jamais déborder.

Les deux sont indépendants : une même addition peut déborder dans un cas et pas dans l'autre. C'est bien pourquoi le processeur maintient deux indicateurs, et pourquoi le compilateur choisit l'instruction de branchement selon que la variable était déclarée signée ou non.

Virgule flottante : IEEE 754

Coder 3,143{,}14 demande un autre principe : la notation scientifique en binaire. Tout réel non nul s'écrit ±1,m×2e\pm 1{,}m \times 2^{e}, et la norme IEEE 754 range ces trois morceaux dans un mot. En simple précision, sur 32 bits :

ChampBitsRôle
Signe10 pour positif, 1 pour négatif
Exposant8e+127e + 127, biaisé pour rester positif
Mantisse23la partie après la virgule de 1,m1{,}m

Deux astuces méritent d'être comprises. Le bit implicite : puisque la partie entière de la notation normalisée vaut toujours 1, on ne la stocke pas — 23 bits stockés donnent 24 bits de précision. Et l'exposant biaisé : au lieu d'un exposant signé, on stocke e+127e+127, ce qui permet de comparer deux flottants positifs comme s'ils étaient des entiers, en un seul circuit.

La double précision, sur 64 bits, suit le même schéma avec 11 bits d'exposant et 52 de mantisse. Certains motifs sont réservés : exposant tout à 1 et mantisse nulle codent ±\pm\infty, exposant tout à 1 et mantisse non nulle codent NaN (not a number), et l'exposant tout à 0 code zéro et les nombres dénormalisés.

Reste la propriété qui coûte le plus cher en pratique : un flottant binaire ne peut pas représenter exactement 0,10{,}1. En base 2, 1/101/10 est un développement infini périodique, exactement comme 1/31/3 en base 10. La machine stocke donc une valeur voisine, et :

0.1 + 0.2 = 0.30000000000000004

Ce n'est pas un bogue, c'est un arrondi, et il est inévitable dans tout codage à précision finie. La conséquence est une règle absolue : on ne compare jamais deux flottants avec une égalité stricte. On teste que leur écart est inférieur à une tolérance. Et en informatique financière, on n'emploie pas de flottants du tout — on compte en centimes, avec des entiers.

Caractères : d'ASCII à Unicode

Un caractère est un nombre, et rien d'autre. La difficulté n'a jamais été technique, elle a été de se mettre d'accord sur la table.

ASCII (1963) code 128 caractères sur 7 bits : les chiffres à partir de 48, les majuscules à partir de 65, les minuscules à partir de 97. Deux détails qui servent en TD : l'écart majuscule → minuscule vaut exactement 32, soit un seul bit à basculer ; et le caractère '7' vaut 55, pas 7 — d'où le − '0' de toutes les conversions caractère vers chiffre.

ASCII ne connaît ni le é, ni le ß, ni le grec. Les pages de codes sur 8 bits, comme Latin-1, ont ajouté 128 caractères chacune, avec le résultat prévisible : un même octet signifiait autre chose d'une page à l'autre, et tout échange mal étiqueté produisait des é — c'est l'origine des accents cassés qu'on voit encore.

Unicode sépare enfin deux questions que tout le monde confondait. Il attribue à chaque caractère un point de code unique et abstrait (U+00E9 pour é), sans dire comment le stocker. Le stockage relève d'un encodage, et le dominant est UTF-8, à taille variable : 1 octet pour l'ASCII — qui reste donc valide tel quel —, 2 à 4 octets au-delà.

D'où une conséquence que le chapitre 7 du cours de systèmes retrouvera : en UTF-8, le nombre d'octets n'est pas le nombre de caractères. Le mot « été » occupe 5 octets pour 3 caractères. Couper une chaîne au milieu d'un caractère produit un octet invalide.

Images et sons : échantillonner puis quantifier

Le monde est continu, la machine discrète. Toute numérisation fait donc deux choses, et chacune perd de l'information.

Échantillonner, c'est découper : une image en pixels, un son en instants de mesure. Pour le son, le théorème de Shannon impose d'échantillonner à plus du double de la fréquence maximale à conserver — d'où les 44,1 kHz du disque compact, un peu plus du double des 20 kHz audibles.

Quantifier, c'est arrondir chaque échantillon sur un nombre fini de niveaux. La profondeur est ce nombre de bits : 8 bits par composante pour une image courante — d'où les 256 niveaux de rouge du #4f46e5 du chapitre 1 —, 16 bits par échantillon pour le CD.

Le calcul de taille en découle directement, et c'est l'exercice type :

Image 1920 × 1080, 3 composantes de 8 bits  1920 × 1080 × 3 = 6 220 800 octets ≈ 5,9 Mio par image, non compressée Son stéréo, 44 100 Hz, 16 bits, 1 minute  44 100 × 2 × 2 × 60 = 10 584 000 octets ≈ 10 Mio

Ces ordres de grandeur expliquent à eux seuls l'existence de la compression — et la distinction entre compression sans perte (PNG, FLAC), qui restitue l'original bit pour bit, et avec perte (JPEG, MP3), qui jette ce que la perception ne remarque pas.

Quiz · 1 question

Un programme compare if (0.1 + 0.2 == 0.3) et le test échoue. Quelle est l'analyse correcte ?

  • Le processeur a un défaut d'arrondi que les versions récentes corrigentdéfaut matériel
  • 0,1 et 0,2 n'ont pas de représentation binaire exacte : leur somme est légèrement décalée, donc on ne compare jamais deux flottants par égalité strictecodage à précision finie
  • Il faut passer en double précision, ce qui rend la comparaison exacteplus de bits

Réponse : En base 2, 1/10 est un développement infini périodique, comme 1/3 en base 10 : aucune mantisse finie ne peut le contenir. La machine stocke donc les plus proches voisins représentables, et leur somme tombe à 0.30000000000000004. Passer en double précision déplace l'erreur sans la supprimer — il y a plus de bits, mais toujours un nombre fini. La règle qui en découle est absolue : on teste que l'écart est inférieur à une tolérance, jamais l'égalité. Et pour de l'argent, on n'emploie pas de flottants : on compte en centimes avec des entiers.

À vous

L'exercice code le complément à deux sur 8 bits dans les deux sens, puis détecte le débordement signé. Les trois fonctions sont courtes ; ce qui compte est de faire tomber les cas limites, et ils sont volontairement présents dans le jeu de tests : 128-128, dont l'opposé n'existe pas, et 100+50100+50, qui déborde sans produire la moindre retenue sortante.

Exercice de code

Complétez le décodage en complément à deux, puis détectez le débordement signé.

Point de départ

const N = 8;
const MODULO = 1 << N;          // 256
const MIN = -(1 << (N - 1));    // −128
const MAX = (1 << (N - 1)) - 1; // +127

const bits = (motif) => motif.toString(2).padStart(N, "0");

// Entier signé -> motif de N bits. Un négatif se code comme 2^N + x.
function coder(x) {
  if (x < MIN || x > MAX) return null;   // hors domaine : incodable
  return x < 0 ? MODULO + x : x;
}

// Motif de N bits -> entier signé.
function decoder(motif) {
  return motif;   // ← faux : cette lecture ignore le bit de signe
}

// Addition machine : on additionne, on tronque à N bits, et on regarde si
// le résultat SIGNÉ est celui attendu.
function additionner(a, b) {
  const brut = (coder(a) + coder(b)) % MODULO;
  return { motif: brut, valeur: decoder(brut) };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Corrigez decoder : si le bit de poids fort vaut 1, le motif code un
//    négatif, et sa valeur est motif − 2^N.
// 2. Écrivez deborde(a, b) : vrai si a + b sort de [MIN, MAX].
//    Indice : deux nombres de signes contraires ne débordent jamais.

function deborde(a, b) {
  return false;   // ← à écrire
}

const CAS = [
  { a: 5, b: -5 }, { a: 100, b: 50 }, { a: -100, b: -50 },
  { a: -128, b: 1 }, { a: 127, b: -1 }, { a: 60, b: 60 },
];

for (const { a, b } of CAS) {
  const r = additionner(a, b);
  const exact = a + b;
  const attendu = exact < MIN || exact > MAX;
  const marque = r.valeur === exact ? "  ok" : "  X débordement";
  const detecte = deborde(a, b) === attendu ? "" : "   (deborde() se trompe)";
  console.log(
    (a + " + " + b).padEnd(12) + "= " + String(r.valeur).padStart(5) +
    "  " + bits(r.motif) + marque + detecte
  );
}

console.log("opposé de -128 :", coder(128) === null ? "incodable sur 8 bits" : "?");

Solution

const N = 8;
const MODULO = 1 << N;
const MIN = -(1 << (N - 1));
const MAX = (1 << (N - 1)) - 1;

const bits = (motif) => motif.toString(2).padStart(N, "0");

function coder(x) {
  if (x < MIN || x > MAX) return null;
  return x < 0 ? MODULO + x : x;
}

function decoder(motif) {
  // Le bit de poids fort EST le bit de signe : s'il vaut 1, le motif code
  // le négatif motif − 2^N. C'est l'exacte réciproque de coder().
  return motif >= MODULO / 2 ? motif - MODULO : motif;
}

function additionner(a, b) {
  const brut = (coder(a) + coder(b)) % MODULO;
  return { motif: brut, valeur: decoder(brut) };
}

function deborde(a, b) {
  // Deux signes contraires ne peuvent pas déborder : la somme est comprise
  // entre les deux opérandes. Le débordement n'arrive donc que sur deux
  // nombres de même signe dont la somme change de signe.
  const s = a + b;
  if (a >= 0 !== b >= 0) return false;
  return s < MIN || s > MAX;
}

const CAS = [
  { a: 5, b: -5 }, { a: 100, b: 50 }, { a: -100, b: -50 },
  { a: -128, b: 1 }, { a: 127, b: -1 }, { a: 60, b: 60 },
];

for (const { a, b } of CAS) {
  const r = additionner(a, b);
  const exact = a + b;
  const attendu = exact < MIN || exact > MAX;
  const marque = r.valeur === exact ? "  ok" : "  X débordement";
  const detecte = deborde(a, b) === attendu ? "" : "   (deborde() se trompe)";
  console.log(
    (a + " + " + b).padEnd(12) + "= " + String(r.valeur).padStart(5) +
    "  " + bits(r.motif) + marque + detecte
  );
}

console.log("opposé de -128 :", coder(128) === null ? "incodable sur 8 bits" : "?");

En travaux pratiques

Travaux pratiques 2 · 3 h

Coder autre chose que des entiers positifs

Manipuler les trois codages qui trahissent le plus souvent le programmeur : complément à deux, virgule flottante, UTF-8 — et voir chacun échouer en direct.

Avant de commencer

  • Le TP 1 : conversions et notion de largeur fixe
  • Un compilateur C, et un outil d'affichage hexadécimal

Énoncé

  1. Coder un négatifCodez -5 sur 8 bits en complément à deux, à la main. Vérifiez votre résultat en additionnant 5 et -5 sur 8 bits : vous devez trouver zéro. Indice : Inverser tous les bits, puis ajouter 1.
  2. L'asymétrieSur 8 bits signés, écrivez la plus grande et la plus petite valeur. Calculez ensuite l'opposé de la plus petite avec la méthode de l'étape 1. Que trouvez-vous, et pourquoi ?
  3. Le test qui mentEn C, écrivez une boucle qui part de 0 et ajoute 1 à un int tant que la valeur est positive. Compilez sans optimisation, exécutez, et expliquez la sortie.
  4. Décomposer un flottantAffichez la représentation binaire d'un float valant 0.1, et retrouvez signe, exposant et mantisse. Reconstruisez la valeur exacte qu'elle représente réellement. Indice : Un union entre un float et un unsigned int permet de lire les mêmes octets des deux façons.
  5. La comparaison qui échoueTestez si 0.1 + 0.2 vaut 0.3. Affichez le résultat avec vingt décimales. Écrivez ensuite la comparaison correcte, et dites ce qui fixe le seuil.
  6. Le texte, en octetsCréez un fichier contenant « éléphant », affichez-le en hexadécimal et comptez les octets. Comparez au nombre de caractères. Coupez ensuite le fichier au milieu d'un caractère accentué et rouvrez-le.
  7. Détecter une erreurAjoutez un bit de parité paire à un octet, puis inversez un bit et vérifiez la détection. Inversez-en deux : que se passe-t-il ?

C'est réussi quand

  • 5 + (-5) donne 0000 0000 sans que vous ayez traité le signe à part
  • Vous savez expliquer pourquoi 0.1 n'est pas représentable exactement
  • « éléphant » compte 8 caractères et 10 octets, et vous savez lesquels
  • Vous savez énoncer la limite de la parité en une phrase

Correction

Complément à deux, et pourquoi il est choisi
  5 = 0000 0101
inversion = 1111 1010
  + 1   = 1111 1011  ←  -5

vérification :
0000 0101   ( 5)
+ 1111 1011   (-5)
-----------
10000 0000   → tronqué à 8 bits → 0000 0000

C'est TOUT l'intérêt du complément à deux : le même circuit additionneur traite les positifs et les négatifs, sans aucun test de signe. Vous le câblerez au TP 4 en ajoutant simplement un XOR et une retenue d'entrée. Une représentation en signe et valeur absolue exigerait, elle, un circuit de soustraction séparé.

L'asymétrie, et le nombre qui est son propre opposé
sur 8 bits signés : de -128 à +127

-128 = 1000 0000
inversion = 0111 1111
    + 1 = 1000 0000  ←  -128 de nouveau !

en C : -INT_MIN est un comportement INDÉFINI

Il y a 256 combinaisons pour 255 valeurs symétriques plus zéro : une valeur négative n'a pas d'opposé. abs(INT_MIN) renvoie un nombre négatif sur la plupart des machines, et c'est une source réelle de failles — un contrôle de la forme « si abs(n) < taille » se laisse contourner par cette seule valeur.

Le débordement signé, et le compilateur
for (int i = 0; i > -1; i++) ;   /* boucle « tant que positif » */

gcc -O0 : s'arrête après 2 147 483 647 (retour à -2147483648)
gcc -O2 : ne s'arrête JAMAIS

/* le compilateur a le droit de supposer que le débordement
 signé n'arrive pas : il en déduit que i > -1 est toujours vrai */

Le débordement d'un entier SIGNÉ est un comportement indéfini en C ; celui d'un NON SIGNÉ est parfaitement défini (modulo 2^n). D'où la règle pratique : un compteur qui peut déborder se déclare non signé, et un débordement se teste AVANT l'opération, jamais après.

0.1, en vrai
float 0.1  →  0x3DCCCCCD
signe 0 | exposant 0111 1011 (123 → 123-127 = -4) | mantisse 1.6

valeur réelle stockée : 0.100000001490116119384765625

0.1 + 0.2 = 0.30000000000000004440892098500626
0.1 + 0.2 == 0.3  →  faux

/* la comparaison correcte */
if (fabs(a - b) < 1e-9) ...

En base 2, 0,1 est périodique — exactement comme 1/3 en base 10. Aucun nombre fini de bits ne le représente. Le seuil ne se choisit pas au hasard : il dépend de l'ordre de grandeur des valeurs comparées, ce qui rend la comparaison RELATIVE préférable dès que les grandeurs varient. Corollaire : jamais de flottant pour de l'argent — des centimes en entiers.

Le texte n'est pas une suite de caractères d'un octet
« éléphant »  →  C3 A9 6C C3 A9 70 68 61 6E 74
                  ^^^^^      ^^^^^
                   é          é   : DEUX octets chacun

8 caractères, 10 octets

fichier coupé au milieu du é : é remplacé par  (octet invalide)

strlen compte des OCTETS, pas des caractères. Couper une chaîne UTF-8 à une position arbitraire produit une séquence invalide — c'est la cause la plus fréquente des caractères de remplacement dans les interfaces. UTF-8 est conçu pour qu'on puisse toujours retrouver le début d'un caractère : un octet de continuation commence par 10.

Ce que la parité ne fait pas
octet 0110 1001 → quatre 1 → bit de parité paire : 0

un bit inversé  : 0111 1001 → cinq 1  → parité 1 attendue → DÉTECTÉ
deux bits       : 0111 1101 → six 1   → parité 0 attendue → INVISIBLE

La parité détecte un nombre IMPAIR d'erreurs, et ne corrige rien. Les mémoires ECC utilisent un code de Hamming, qui corrige une erreur et détecte les doubles, au prix de bits supplémentaires. Toute détection d'erreur est un compromis entre redondance et garantie — il n'existe pas de code qui détecte tout.

Ce que la suite en fait

Le complément à deux n'est pas qu'une convention d'écriture : c'est ce qui permet à une machine de n'avoir qu'un seul circuit d'addition pour additionner et soustraire. Le chapitre 4 construira ce circuit porte par porte, et l'on verra que soustraire revient à inverser une entrée et à forcer la retenue initiale à 1 — deux fils, pas un second additionneur.

Le chapitre 6 retrouvera tout le reste : l'extension de signe dans les instructions de chargement, la distinction signé/non signé dans les branchements, et les décalages qui multiplient par des puissances de deux. Et le domaine de valeurs, ici présenté sur 8 bits, deviendra celui du bus d'adresses au chapitre 5 : 2322^{32} adresses, soit 4 Gio d'espace adressable — la vraie raison de la bascule vers le 64 bits.

À retenir

Flashcards · 5 cartes

Comment obtient-on l'opposé d'un nombre en complément à deux, et pourquoi ce codage a-t-il été choisi ?
On inverse tous les bits puis on ajoute 1 — règle involutive, l'appliquer deux fois redonne le nombre. Il a été choisi parce qu'il n'a qu'un seul zéro et surtout parce que l'addition binaire ordinaire donne le bon résultat sur les négatifs : une seule unité d'addition suffit au processeur, sans circuit de soustraction séparé.
Quel est le domaine du complément à deux sur n bits, et quelle asymétrie faut-il connaître ?
De −2ⁿ⁻¹ à +2ⁿ⁻¹ − 1, soit −128 à +127 sur 8 bits. Il y a un négatif de plus que de positifs, parce que zéro occupe une place du côté positif. Conséquence : l'opposé de −128 n'est pas représentable sur 8 bits, et la règle inversion + 1 appliquée à −128 redonne −128.
Comment détecte-t-on un débordement signé, et pourquoi la retenue sortante ne suffit-elle pas ?
La retenue sortante signale le débordement NON signé. Le débordement signé se détecte quand la retenue entrant dans le bit de signe diffère de celle qui en sort — ou, en pratique : additionner deux nombres de même signe et obtenir un signe opposé. 100 + 50 sur 8 bits déborde en signé (résultat −106) sans produire aucune retenue sortante.
Pourquoi 0,1 + 0,2 ne vaut-il pas exactement 0,3 en flottant, et qu'en déduire ?
Parce que 1/10 est un développement binaire infini périodique : aucune mantisse finie ne le contient exactement. La machine calcule sur les plus proches voisins représentables. On ne compare donc jamais deux flottants par égalité stricte — on teste que l'écart est sous une tolérance — et on n'emploie pas de flottants pour de l'argent : on compte en centimes, en entiers.
Que sépare Unicode que les pages de codes confondaient, et qu'implique UTF-8 ?
Unicode sépare le POINT DE CODE (le numéro abstrait du caractère, U+00E9 pour é) de l'ENCODAGE (la façon de le stocker en octets). UTF-8 est un encodage à taille variable : 1 octet pour l'ASCII, 2 à 4 au-delà. Donc le nombre d'octets n'est pas le nombre de caractères — « été » occupe 5 octets pour 3 caractères — et couper une chaîne au hasard peut produire un octet invalide.