Skip to content

Niveau 5 · Chapitre 5.2

Formats et encodage des instructions

Comment une instruction devient des bits : opcodes et champs d’opérandes, longueur fixe ou variable, opcodes expansibles, et une visite champ par champ de l’encodage x86-64 — préfixes, REX, opcode, ModRM, SIB, déplacement, immédiat — avec ARM64 et RISC-V en contrepoint.

Au niveau de l’assembleur, une instruction est une ligne de texte. Le CPU, lui, lit des octets. L’ISA fixe la façon dont chaque instruction s’écrit en bits : quels bits choisissent l’opération, lesquels désignent les registres, où vont les constantes et les adresses. Cette disposition est le format d’instruction, et c’est l’une des choses les plus difficiles à changer une fois un processeur commercialisé. Le x86 décode encore des instructions disposées pour le 8086 en 1978.

Ce qu’une instruction doit dire

Toute instruction a besoin d’un opcode, le champ qui dit quoi faire. La plupart doivent aussi dire où sont leurs opérandes. Les ISA diffèrent par le nombre d’adresses d’opérandes que porte chaque instruction :

AdressesExempleStyle
0iadd (JVM)machine à pile : les opérandes sont implicitement au sommet d’une pile
1ADC #5 (6502)accumulateur : l’un des opérandes est toujours le registre accumulateur
2add eax, ecx (x86)la destination est aussi une source
3add w0, w1, w2 (ARM64)deux sources et une destination séparée

Moins d’adresses rendent chaque instruction plus courte, mais demandent plus d’instructions : calculer a = b + c prend une seule instruction à trois adresses, mais un chargement, une addition et un rangement sur une machine à pile. Les conceptions RISC modernes sont des machines à registres à trois adresses. Le x86 est à deux adresses, avec un opérande autorisé en mémoire.

Longueur fixe ou variable

  • Longueur fixe : toutes les instructions ont la même taille — 32 bits sur ARM64 et RISC-V de base. Le décodage est simple et rapide : l’instruction suivante commence toujours 4 octets plus loin, donc un CPU peut en décoder plusieurs à la fois. Mais les instructions simples gaspillent des bits, et les constantes doivent tenir dans les champs restants.
  • Longueur variable : une instruction prend autant d’octets qu’il lui en faut — de 1 à 15 sur x86. Le code est plus dense, ce qui aide le cache d’instructions. Mais le décodeur ne peut pas savoir où commence l’instruction suivante tant qu’il n’a pas déterminé la longueur de celle-ci.

Beaucoup d’ISA à longueur fixe ont ajouté une forme compressée de 16 bits pour la densité : Thumb sur l’ARM 32 bits, et l’extension C sur RISC-V.

Les concepteurs arbitrent entre plusieurs contraintes. Des instructions plus courtes, c’est plus d’instructions par ligne de cache et par cycle de lecture, et la bande passante mémoire est rare. Il faut de la place dans l’espace des opcodes pour chaque opération, et pour celles qu’on ajoutera au fil des décennies. Et chaque champ d’adresse doit être assez large pour les registres ou la mémoire qu’il désigne.

Les opcodes expansibles

Un format fixe peut quand même donner plus de bits d’opcode à certaines instructions qu’à d’autres. Prenons une instruction de 16 bits avec un opcode de 4 bits et trois champs de registre de 4 bits. Normalement, cela permet 16 instructions à trois registres. Mais si une valeur d’opcode, 1111, signifie « l’opcode continue dans le champ suivant », l’espace se subdivise :

Bits d’opcodeChamps d’opérandes restantsInstructions
4 (0000–1110)3 × 4 bits15 à trois adresses
8 (1111 0000–1111 1101)2 × 4 bits14 à deux adresses
12 (1111 1110 …, 1111 1111 …)1 × 4 bits31 à une adresse
16 (1111 1111 1111 …)aucun16 sans adresse

Chacune des 65 536 combinaisons de bits est utilisée : 15 × 4096 + 14 × 256 + 31 × 16 + 16 = 65 536. La même idée — des opcodes courts pour les instructions qui ont besoin de beaucoup de bits d’opérandes, des opcodes plus longs pour les autres — se retrouve dans presque tous les encodages réels, y compris les octets d’échappement du x86.

x86-64 : visite champ par champ

Une instruction x86-64 est une suite de champs facultatifs autour d’un opcode obligatoire :

ChampTailleRôle
préfixes0–4 octetsmodifient l’instruction : 66 opérandes de 16 bits, f0 lock, f3 rep…
REX0–1 octet0x40–0x4f : W = opérandes de 64 bits ; R, X, B = 4ᵉ bit des numéros de registre (pour r8–r15)
opcode1–3 octetsl’opération ; 0f est l’octet d’échappement qui ouvre une seconde table d’opcodes
ModRM0–1 octetmod (2 bits) : registre ou mémoire, et taille du déplacement ; reg (3) : un registre ou une extension d’opcode ; rm (3) : l’autre opérande
SIB0–1 octetscale (2), index (3), base (3), pour les adresses comme [base + index×échelle]
déplacement0, 1 ou 4 octetsla constante d’une adresse, comme le 8 de [rbx+8]
immédiat0, 1, 2 ou 4 octets (8 pour movabs)un opérande constant

Le total est plafonné à 15 octets. L’opcode lui-même dit peu de chose : le décodeur doit le reconnaître entièrement avant même de savoir si un octet ModRM suit. Le texte de Tanenbaum donne 0xFF comme octet d’échappement ; c’est en réalité 0x0F. 0xFF est un opcode ordinaire, utilisé pour inc, dec, call, jmp et push sur la mémoire.

Tapez une instruction dans l’encodeur : il montre chaque champ, avec les bits de ModRM et de SIB détaillés :

Encoding · add avec un opérande mémoireInput

objdump shows03 44 8b 08 add eax, dword ptr [rbx + 4*rcx + 0x8]

4 bytes
length
max 15 on x86
none
prefixes
b+i×s+d8
addressing
base + index×scale + disp8
none
immediate
  • 03 → ADD r32, r/m32 (dst += src): ModRM.reg is the destination, ModRM.rm the source.

  • mod=01 memory + disp8 · reg=eax · rm=SIB.

    01
    mod
    000
    reg
    100
    rm
    mod=01
    memory + disp8
    reg=000
    eax
    rm=100
    100: a SIB byte follows
  • Scale-Index-Base: address = base + index × scale + disp.

    10
    scale
    001
    index
    011
    base
    scale=10
    ×4
    index=001
    rcx
    base=011
    rbx

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.

03 44 8b 08 : l’opcode 03 signifie « ajouter un opérande registre-ou-mémoire à un registre de 32 bits ». Le ModRM 44 vaut 01 000 100 : mod 01 indique la mémoire avec un déplacement de 8 bits, reg 000 est eax, et rm 100 annonce un octet SIB. Le SIB 8b vaut 10 001 011 : échelle ×4, index rcx, base rbx. Puis vient le déplacement, 08.

L’encodage a ses bizarreries, et chacune se voit dans un désassemblage :

  • mov eax, DWORD PTR [rsp+8] exige un octet SIB (8b 44 24 08), car rm = 100, le code de rsp, a été réservé pour dire « un SIB suit ».
  • mov eax, DWORD PTR [rbp] exige un déplacement nul (8b 45 00), car mod 00 avec rm = 101 a été réservé à l’adressage relatif à rip.
  • r8–r15 exigent un octet REX pour fournir le quatrième bit de registre, et r12 hérite en plus de la règle SIB de rsp :
Encoding · Un préfixe REX et une bizarrerie héritéeInput

objdump shows45 8b 04 24 mov r8d, dword ptr [r12]

4 bytes
length
max 15 on x86
45
prefixes
REX
b
addressing
base
none
immediate
  • REX = 0100WRXB. R: extends ModRM.reg; B: extends ModRM.rm / SIB.base / opcode reg.

    0100
    fixed
    0
    W
    1
    R
    0
    X
    1
    B
    fixed=0100
    marks a REX prefix (0x40–0x4F)
    W=0
    default size
    R=1
    ModRM.reg + 8 → r8d
    X=0
    index not extended
    B=1
    SIB.base + 8 → base r12
  • 8B → MOV r32, r/m32 (dst = src): ModRM.reg is the destination, ModRM.rm the source.

  • mod=00 memory, no displacement · reg=r8d · rm=SIB.

    00
    mod
    000
    reg
    100
    rm
    mod=00
    memory, no displacement
    reg=000
    r8d
    rm=100
    100: a SIB byte follows
  • Scale-Index-Base: address = base + disp (needed only because r12 as a base is encoded rm=100, which means "SIB follows").

    00
    scale
    100
    index
    100
    base
    scale=00
    ×1
    index=100
    100: no index
    base=100
    r12 (with REX.B)

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.

45 8b 04 24 : le REX 45 a R à 1 (la destination est le registre 8 + 0 = r8d) et B à 1 (la base est 8 + 4 = r12). L’octet SIB n’est là que parce que les trois bits bas de r12, 100, sont les mêmes que ceux de rsp.

Quelques autres motifs à reconnaître :

  • 48 83 c0 01 est add rax, 1. 48 est REX.W pour des opérandes de 64 bits, et 83 est la forme avec un immédiat de 8 bits étendu avec le signe : les petites constantes prennent un octet au lieu de quatre.
  • 0f af c1 est imul eax, ecx. 0f renvoie à la table d’opcodes sur deux octets, où vivent la plupart des instructions ajoutées après le 8086.
  • 66 89 d8 est mov ax, bx. Le préfixe 66 fait passer une instruction de 32 bits à 16 bits.
  • f0 83 07 01 est lock add DWORD PTR [rdi], 1 : un incrément atomique.

ARM64 et RISC-V : des champs fixes

Dans un format fixe de 32 bits, chaque champ est au même endroit dans toutes les instructions de son type. Le add w0, w1, w2 de l’ARM64 vaut 0x0b020020 :

Bits3130–2928–2423–222120–1615–109–54–0
Champsfop, S01011shift0Rmimm6RnRd
Valeur0 (32 bits)00add (registre décalé)0002010

Le add a0, a1, a2 du RISC-V vaut 0x00c58533, au format R : funct7 | rs2 | rs1 | funct3 | rd | opcode = 0000000 | 01100 | 01011 | 000 | 01010 | 0110011. Les numéros de registre a2 = 12, a1 = 11 et a0 = 10 se lisent directement dans les bits. Un décodeur RISC-V sait où se trouve chaque champ de registre avant même de savoir ce que fait l’instruction, et peut donc commencer à lire les registres tôt. C’est tout l’intérêt d’un format régulier.

Pourquoi c’est important pour lire des binaires

Comme les instructions x86 ont une longueur variable, un désassembleur doit trouver où chacune commence. Décodez à partir du mauvais octet et vous obtenez un autre flux d’instructions, tout à fait valide en apparence :

Encoding · Cinq octets, une instructionInput

decodes tomov eax, 0x909090c3

5 bytes
length
max 15 on x86
none
prefixes
none
addressing
no ModRM byte
imm32
immediate
  • B8+r → MOV r32, imm32 (dst = src): the register is in the low 3 bits of the opcode: no ModRM byte.

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

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.

Ces cinq octets sont mov eax, 0x909090c3. Sautez le premier octet — décodez c3 90 90 90 — et la même mémoire se lit comme un ret suivi de nop. Des instructions cachées dans d’autres instructions : c’est pourquoi les désassembleurs peuvent être trompés par des techniques d’anti-désassemblage, et c’est là que la programmation orientée retour (ROP) trouve beaucoup de ses « gadgets » : des séquences d’octets utiles au milieu du propre code d’un programme. Sur ARM64 ou RISC-V, avec des instructions fixes et alignées, c’est impossible : chaque instruction commence sur une frontière de 4 octets.

À retenir

  • Un format d’instruction fixe l’emplacement de l’opcode et des champs d’opérandes. Les ISA vont des machines sans adresse (à pile) aux machines à trois adresses (à registres).
  • Les formats à longueur fixe (ARM64, RISC-V : 32 bits) se décodent facilement en parallèle. Les formats à longueur variable (x86 : 1 à 15 octets) sont plus denses mais doivent être décodés l’un après l’autre.
  • Les opcodes expansibles donnent des opcodes courts aux instructions qui ont besoin de beaucoup de bits d’opérandes, et des opcodes longs aux autres.
  • Le x86-64 encode une instruction en préfixes, REX, opcode, ModRM, SIB, déplacement, immédiat. Ses bizarreries — l’octet SIB pour rsp, le déplacement nul pour rbp, REX pour r8–r15 — se voient dans chaque désassemblage.
  • La longueur variable permet de cacher des instructions dans d’autres, ce qui compte pour les désassembleurs et les exploits.

Dans ce niveau

  1. 5.1Ce que garantit l’ISA : modèle mémoire, alignement et ordrePrévu
  2. 5.2Formats et encodage des instructions
  3. 5.3Modes d’adressage
  4. 5.4x86, ARM et RISC-V côte à côtePrévu
  5. 5.5Déroutements, interruptions et exceptionsPrévu
  6. 5.6Les types que comprend le matérielPrévu
  7. 5.7VLIW, EPIC et l’ItaniumPrévu