Skip to content

Fondamentaux · Chapitre F.6

Boutisme (endianness)

Quel octet d’un nombre multioctet vient en premier en mémoire : machines gros-boutistes et petit-boutistes, ce qu’utilisent les CPU actuels, l’ordre réseau et htons, l’ordre des octets dans les fichiers PNG, ELF et Mach-O, et un coup d’œil en direct aux octets d’un mot stocké.

La mémoire est adressée octet par octet, mais la plupart des nombres dépassent un octet. Un entier de 32 bits occupe quatre adresses consécutives. Lequel de ses quatre octets va à l’adresse la plus basse ? Il y a deux réponses naturelles, et de grandes machines ont utilisé l’une comme l’autre. Ce choix s’appelle l’ordre des octets, ou boutisme (endianness).

Gros-boutiste et petit-boutiste

Prenons la valeur de 32 bits 0x11223344, stockée à l’adresse 100. Son octet de poids fort est 0x11, son octet de poids faible 0x44.

  • Une machine gros-boutiste (big-endian) stocke d’abord l’octet de poids fort, à l’adresse la plus basse : le « gros bout » vient en premier.
  • Une machine petit-boutiste (little-endian) stocke d’abord l’octet de poids faible.
Adresse100101102103
gros-boutiste11223344
petit-boutiste44332211

Dans les deux cas, la valeur est la même, et l’adresse du mot est 100. Seule la disposition des octets change. À l’intérieur d’un octet, la question ne se pose pas : le logiciel ne peut pas adresser les bits un par un, un octet n’est donc qu’un nombre de 0 à 255 dans les deux cas.

Les noms viennent des Voyages de Gulliver, où deux nations se font la guerre pour savoir par quel bout casser un œuf à la coque. Danny Cohen les a empruntés en 1980 dans une note sur ce même débat, On Holy Wars and a Plea for Peace. Son constat tient toujours : aucun ordre n’est meilleur, mais les mélanger provoque de vrais bogues.

Pourquoi chaque ordre a ses partisans

Le gros-boutisme suit la façon dont on écrit les nombres : un vidage hexadécimal de la mémoire se lit de gauche à droite comme le nombre lui-même, et comparer deux nombres non signés gros-boutistes octet par octet, depuis l’adresse la plus basse, donne le même ordre que comparer leurs valeurs.

Le petit-boutisme a un avantage pour le matériel et pour le code de bas niveau : l’octet de poids faible d’une valeur est toujours à son adresse, quelle que soit sa taille. Lire une variable de 64 bits comme une variable de 32, 16 ou 8 bits, à la même adresse, donne ses 32, 16 ou 8 bits de poids faible. Et l’arithmétique sur plusieurs mots, qui commence par le côté de poids faible pour propager les retenues, commence à l’adresse la plus basse.

Aucun de ces avantages n’est décisif, et aujourd’hui le choix est surtout réglé par l’histoire.

Qui utilise quoi

ArchitectureOrdre des octets
x86, x86-64petit-boutiste
ARM, ARM64bi-boutiste, mais petit-boutiste en pratique : Android, iOS, macOS, Windows et les distributions Linux le font tous tourner en petit-boutiste
RISC-Vpetit-boutiste
mainframes IBM Zgros-boutiste
POWERbi-boutiste ; Linux est passé au petit-boutiste (ppc64le) avec le POWER8 en 2014, tandis qu’AIX et IBM i restent gros-boutistes
SPARC, anciens MIPS, 68000gros-boutiste

Les processeurs bi-boutistes peuvent fonctionner dans les deux ordres, choisis par un bit de contrôle que règle le système d’exploitation : sur ARM64, un bit d’un registre de contrôle système fixe l’ordre des accès aux données. Les exemples classiques de machines gros-boutistes étaient le SPARC et les mainframes IBM. Le SPARC a pratiquement disparu : en pratique, presque tous les CPU que vous rencontrerez sont petit-boutistes. Le gros-boutisme survit surtout dans les mainframes, les protocoles réseau et les formats de fichiers.

Voir les octets

Une instruction x86 qui charge une constante porte cette constante en elle-même, en mémoire, elle suit donc elle aussi l’ordre des octets de la machine. Voici l’encodage d’un mov sur 64 bits :

Encoding · Les octets d’une constante de 64 bitsInput

À essayer : Tapez une instruction dans le champ (ou passez en octets hexadécimaux), ou cliquez sur un exemple ; cliquez sur un champ pour voir ses bits.

objdump shows48 b8 88 77 66 55 44 33 22 11 movabs rax, 0x1122334455667788

10 bytes
length
max 15 on x86
48
prefixes
REX
none
addressing
no ModRM byte
imm64
immediate
  • REX = 0100WRXB. W: 64-bit operands.

    0100
    fixed
    1
    W
    0
    R
    0
    X
    0
    B
    fixed=0100
    marks a REX prefix (0x40–0x4F)
    W=1
    64-bit operand size
    R=0
    reg field not extended
    X=0
    index not extended
    B=0
    rm/base not extended
  • B8+r → MOVABS r64, imm64 (dst = src): the only x86 instruction with a full 64-bit immediate (10 bytes).

    10111
    opcode
    000
    reg
    opcode=10111
    B8+r
    reg=000
    rax

64-bit mode, Intel syntax. Where several encodings are valid, this picks the one clang emits; the text is what objdump -d -M intel prints. Branch targets are written relative to the instruction: jmp $+0x10.

Après le préfixe 48 et l’opcode b8, les huit octets de la constante apparaissent à l’envers : 88 77 66 55 44 33 22 11, octet de poids faible en premier. Tous les désassembleurs montrent ainsi les valeurs immédiates et les adresses x86.

Il en va de même pour les données. Le programme ci-dessous stocke 0x11223344 en mémoire, puis le relit en entier, octet par octet et par moitié de 16 bits :

Live · Quel octet est à l’adresse la plus basse ?

À essayer : Appuyez sur Step pour exécuter une instruction, Run pour animer ou Continue pour aller au bout ; les boutons L2 à L7 changent de niveau, vers le bas ou le haut.

program· ▸ is the next instruction
  1. .data
  2. word: .long 0x11223344
  3. port: .short 0
  4. .text
  5. mov eax, DWORD PTR [rip+word] ; the whole word: 0x11223344
  6. movzx ebx, BYTE PTR [rip+word] ; the byte at the lowest address
  7. movzx ecx, BYTE PTR [rip+word+3] ; the byte at the highest address
  8. movzx esi, WORD PTR [rip+word] ; the two lowest bytes
  9. mov edx, 443 ; 0x01BB
  10. rol dx, 8 ; htons(443): swap the two bytes
  11. mov WORD PTR [rip+port], dx
step 0
Loading emulator…
The instructions the compiler generated, and the registers, flags and stack they change.

Le mot entier se relit 0x11223344. L’octet à l’adresse du mot vaut 0x44, et celui à l’adresse + 3 vaut 0x11 : petit-boutiste. Le chargement de 16 bits à la même adresse obtient 0x3344, la moitié de poids faible : l’avantage petit-boutiste vu plus haut. Les trois dernières instructions sont le sujet de la section suivante. Le chapitre sur les variables fait la même expérience depuis le C, avec un pointeur d’octet, et la vue d’ensemble de l’ISA charge 8 octets depuis une adresse non alignée et les obtient assemblés dans l’ordre petit-boutiste.

Le problème : échanger des données entre machines

L’ordre des octets est invisible tant que les données restent sur une machine. Il compte quand les octets voyagent : sur un réseau, ou dans un fichier écrit sur une machine et lu sur une autre.

Supposons qu’une machine gros-boutiste envoie un enregistrement (le nom "Ada" suivi de l’entier de 32 bits 1815, 0x00000717) octet par octet à une machine petit-boutiste. Les octets arrivent dans l’ordre où ils ont été envoyés. La chaîne est intacte, parce qu’un texte est une suite d’octets isolés. L’entier ne l’est pas : les octets 00 00 07 17, lus en petit-boutiste, valent 0x17070000, soit 386 334 720. Permuter les octets de chaque groupe de 4 réparerait l’entier et brouillerait la chaîne. Il n’y a pas de correction aveugle : celui qui lit les données doit savoir quels octets forment quel champ multioctet, et permuter exactement ceux-là.

La solution pratique est celle qu’utilisent tous les protocoles et formats de fichiers : le format fixe l’ordre des octets, et chaque programme convertit chaque champ multioctet en lecture comme en écriture.

L’ordre réseau

Les protocoles d’Internet ont choisi le gros-boutisme, appelé ordre réseau (network byte order) : chaque champ multioctet d’un en-tête IP, TCP ou UDP (adresses, ports, longueurs) est envoyé octet de poids fort en premier. La bibliothèque C fournit des fonctions de conversion : htons et htonl (host to network, court et long), ntohs et ntohl dans l’autre sens.

Sur une machine gros-boutiste, elles ne font rien. Sur une machine petit-boutiste, elles permutent les octets. Le port 443, 0x01BB, doit partir sur le réseau sous la forme des octets 01 BB ; dans un registre petit-boutiste, c’est la valeur 0xBB01, donc htons(443) renvoie 47 873, comme le confirme socket.htons(443) en Python sur le M2. C’est ce que fait le rol dx, 8 de la démo ci-dessus : faire tourner une valeur de 16 bits de 8 bits échange ses deux octets. C’est exactement l’instruction que clang génère pour htons sur x86-64 ; pour les valeurs de 32 bits, il utilise bswap, l’instruction d’inversion d’octets du x86. Sur ARM64, les mêmes fonctions se compilent en rev16 et rev. Les puces x86 récentes ont aussi movbe, un chargement ou un rangement qui permute les octets au passage.

L’ordre des octets dans les fichiers

Les formats de fichiers font le même choix, et un vidage hexadécimal le montre. PNG est gros-boutiste : voici les premiers octets d’une image de 640 × 480. La largeur et la hauteur sont les champs de 4 octets aux positions 16 et 20 :

00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0280 0000 01e0 0802 0000 00ba b34b  ...............K

00 00 02 80 vaut 640 et 00 00 01 e0 vaut 480. Lue par erreur en petit-boutiste, la largeur vaudrait 2 147 614 720.

Les formats d’exécutables indiquent explicitement leur ordre des octets. Un fichier ELF commence par 7f 45 4c 46 (\x7F puis ELF) ; son sixième octet vaut 1 pour petit-boutiste, 2 pour gros-boutiste. Le début de /bin/ls dans un conteneur Linux ARM64 :

000000 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
000010 03 00 b7 00

02 signifie 64 bits et 01 petit-boutiste ; à la position 0x12, le type de machine b7 00 vaut 0x00B7, 183, le code d’AArch64, stocké octet de poids faible en premier.

macOS montre les deux ordres dans un même fichier. /bin/ls y est un binaire universel qui contient une version x86-64 et une version ARM64, et son en-tête est gros-boutiste : il commence par ca fe ba be, le nombre magique 0xCAFEBABE. Chaque version qu’il contient est un fichier Mach-O dans l’ordre propre à la machine, qui commence par cf fa ed fe : le nombre magique 0xFEEDFACF écrit en petit-boutiste. Le chapitre sur les fichiers exécutables décrit ces formats.

À retenir

  • Le boutisme est l’ordre des octets d’une valeur multioctet en mémoire. Le gros-boutisme met l’octet de poids fort à l’adresse la plus basse ; le petit-boutisme y met l’octet de poids faible. 0x11223344 est stocké 44 33 22 11 sur une machine petit-boutiste.
  • x86-64, ARM64 (en pratique) et RISC-V sont petit-boutistes ; IBM Z est gros-boutiste ; les CPU bi-boutistes comme ARM et POWER peuvent fonctionner dans les deux sens.
  • L’ordre des octets ne compte que quand les données passent d’une machine à l’autre ou dans des fichiers. Formats et protocoles fixent l’ordre, et les programmes convertissent chaque champ multioctet : tout permuter aveuglément casse le texte.
  • L’ordre réseau est gros-boutiste ; htons/htonl convertissent, en se compilant en rol/bswap sur x86 et en rev16/rev sur ARM64. htons(443) vaut 47 873 sur une machine petit-boutiste.
  • PNG est gros-boutiste ; ELF et Mach-O déclarent leur ordre dans leur en-tête ; un binaire universel macOS enveloppe des programmes petit-boutistes dans un en-tête gros-boutiste.

Fondamentaux

  1. F.1Niveaux d’abstraction et brève histoire des ordinateurs
  2. F.2Binaire et hexadécimal
  3. F.3Complément à deux et entiers signés
  4. F.4Virgule flottante (IEEE 754)
  5. F.5Caractères, ASCII et Unicode
  6. F.6Boutisme (endianness)
  7. F.7Parité, codes de Hamming et correction d’erreurs
  8. F.8Unités : kilo, kibi et compagnie