Skip to content

Niveau 5 · Chapitre 5.3

Modes d’adressage

Toutes les façons dont une instruction peut dire où est son opérande — immédiat, registre, direct, indirect par registre, base + déplacement, index mis à l’échelle, relatif au PC, pile — leur correspondance avec le code C, leur coût en octets, l’adressage des cibles de branchement, et les différences entre x86, ARM64 et RISC-V.

Chaque opérande doit dire où se trouve sa valeur : dans l’instruction, dans un registre, ou quelque part en mémoire. Les différentes façons de le dire sont les modes d’adressage de l’ISA. C’est un choix de conception. Un jeu riche rend chaque instruction plus puissante mais plus difficile à décoder ; un jeu minimal garde le matériel simple mais demande plus d’instructions. Le niveau de l’assembleur a présenté les trois sortes d’opérandes. Ce chapitre regarde les modes eux-mêmes, d’une ISA à l’autre.

Les modes

ModeL’opérande est…Exemple x86Usage typique
immédiatdans l’instructionmov eax, 5constantes
registredans un registremov ebx, eaxvariables locales gardées en registre par le compilateur
direct (absolu)à une adresse fixe donnée dans l’instructionmov eax, DWORD PTR [0x404000]globales, dans du code dépendant de la position
indirect par registreà l’adresse contenue dans un registremov ecx, DWORD PTR [rsi]pointeurs : *p
base + déplacementà un registre plus une constantemov edx, DWORD PTR [rsi+8]locales ([rbp-4]), champs de structure (p->y)
base + index × échelle (+ déplacement)à base + index × 1, 2, 4 ou 8 (+ constante)mov r8d, DWORD PTR [rsi+rdi*4]tableaux : a[i], tableaux de structures
relatif au PCà un décalage du pointeur d’instructionmov r10d, DWORD PTR [rip+arr+4]globales en code indépendant de la position, cibles de branchement
pile (implicite)au sommet de la pilepush rax, pop r11appels, registres sauvegardés

Les noms de Tanenbaum diffèrent un peu : indexé pour un registre plus une constante, et basé-indexé pour deux registres plus une constante facultative. Ce sont les mêmes idées.

Voir l’adresse se calculer

Ce programme lit le même petit tableau de plusieurs façons. L’adresse de chaque opérande mémoire est calculée par l’AGU (unité de génération d’adresses) avant le chargement. Le simulateur s’ouvre au niveau µarch, où chaque étape montre ce calcul :

Live · Un tableau, plusieurs modes d’adressage
program— ▸ is the next instruction
  1. .data
  2. arr: .long 10, 20, 30, 40
  3. .text
  4. mov eax, 5 ; immédiat
  5. mov ebx, eax ; registre
  6. lea rsi, [rip+arr] ; relatif au PC : rsi = &arr
  7. mov ecx, DWORD PTR [rsi] ; indirect par registre : arr[0] = 10
  8. mov edx, DWORD PTR [rsi+8] ; base + déplacement : arr[2] = 30
  9. mov edi, 3
  10. mov r8d, DWORD PTR [rsi+rdi*4] ; base + index×4 : arr[3] = 40
  11. mov r9d, DWORD PTR [rsi+rdi*4-4] ; base + index×4 + déplacement : arr[2] = 30
  12. mov r10d, DWORD PTR [rip+arr+4] ; relatif au PC : arr[1] = 20
  13. push rax ; pile : écriture en rsp - 8
  14. pop r11 ; pile : lecture en rsp
step 0
Loading emulator…
Inside the CPU: the micro-operations and bus cycles of that instruction.

Pour mov r8d, DWORD PTR [rsi+rdi*4], l’AGU affiche rsi (0x404000) + rdi × 4 = 0x40400c, puis la lecture sur le bus renvoie 40. Le facteur d’échelle existe parce que les éléments d’un tableau font 1, 2, 4 ou 8 octets : l’index reste un numéro d’élément, et le matériel le multiplie gratuitement par la taille de l’élément.

Du C aux modes d’adressage

Les compilateurs font correspondre les expressions C à ces modes si directement qu’on peut souvent relire le C dans l’assembleur :

COpérande x86-64 typique
locale xun registre, ou [rbp-4] / [rsp+12]
*p[rax]
p->y (champ au décalage 8)[rax+8]
a[i] (int)[rax+rcx*4]
a[i].y (structures de 16 octets, y au décalage 8)shl rcx, 4 puis [rax+rcx+8] — l’échelle ne va que jusqu’à 8
globale g[rip+g]
&a[i]lea rdx, [rax+rcx*4] — la même adresse, sans le chargement

Cette dernière ligne explique l’existence de lea, et pourquoi les compilateurs s’en servent aussi pour de l’arithmétique ordinaire : il exécute l’addition et la mise à l’échelle de l’AGU sans toucher à la mémoire.

Ce que coûtent les modes en octets

Des modes plus riches, ce sont des instructions plus longues. Le même chargement, avec des adresses différentes :

Encoding · Un petit déplacement : un octetInput

objdump shows8b 56 08 mov edx, dword ptr [rsi + 0x8]

3 bytes
length
max 15 on x86
none
prefixes
b+d8
addressing
base + disp8
none
immediate
  • 8B → MOV r32, r/m32 (dst = src): ModRM.reg is the destination, ModRM.rm the source.

  • mod=01 memory + disp8 · reg=edx · rm=base rsi.

    01
    mod
    010
    reg
    110
    rm
    mod=01
    memory + disp8
    reg=010
    edx
    rm=110
    base rsi

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.

Encoding · Un déplacement plus grand : quatre octetsInput

objdump shows8b 86 00 10 00 00 mov eax, dword ptr [rsi + 0x1000]

6 bytes
length
max 15 on x86
none
prefixes
b+d32
addressing
base + disp32
none
immediate
  • 8B → MOV r32, r/m32 (dst = src): ModRM.reg is the destination, ModRM.rm the source.

  • mod=10 memory + disp32 · reg=eax · rm=base rsi.

    10
    mod
    000
    reg
    110
    rm
    mod=10
    memory + disp32
    reg=000
    eax
    rm=110
    base rsi

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.

[rsi+8] loge son déplacement dans un octet (8b 56 08). [rsi+0x1000] en demande quatre (8b 86 00 10 00 00). Un index mis à l’échelle ajoute un octet SIB : mov r8d, DWORD PTR [rsi+rdi*4] s’écrit 44 8b 04 be. Et en mode 64 bits, une adresse absolue comme [0x404000] ne peut s’encoder qu’à travers un octet SIB sans base (8b 04 25 00 40 40 00), parce que l’encodage court qui signifiait « adresse absolue » en mode 32 bits a été réaffecté à l’adressage relatif à rip. C’est l’une des raisons pour lesquelles le code 64 bits accède à ses globales par [rip+…].

Adresser les cibles de branchement

Les branchements ont aussi besoin d’adresses, avec leurs propres modes :

  • Relatif au PC : jmp, jcc et call encodent normalement un décalage signé depuis l’instruction suivante, sur 8 bits pour les cibles proches et 32 bits sinon. Le code fonctionne où qu’il soit chargé, ce qui rend possibles le code indépendant de la position et l’ASLR.
  • Indirect par registre ou mémoire : jmp rax, call QWORD PTR [rax+16]. La cible est calculée à l’exécution. C’est ainsi que fonctionnent les tables de saut des switch, les pointeurs de fonction et les appels virtuels du C++, et c’est pourquoi ils ont besoin d’un prédicteur de branchements indirects.
  • Pile : ret prend sa cible au sommet de la pile.

D’autres ISA font d’autres choix

  • RISC-V est une architecture load/store pure : l’arithmétique ne travaille que sur des registres, et chargements et rangements n’ont qu’un seul mode mémoire, registre de base + décalage de 12 bits. Lire a[i] demande trois instructions : décaler i de 2, ajouter la base, puis lw avec un décalage nul. Le décodeur reste trivial, et le compilateur fait le reste.
  • ARM64 est aussi load/store, mais ses chargements acceptent un registre de décalage mis à l’échelle (ldr w0, [x1, x2, lsl #2] lit a[i] en une instruction). Il a aussi des modes pré- et post-indexés qui mettent à jour le registre de base au passage : ldr x0, [x1], #8 charge, puis avance x1 de 8, ce qui est idéal pour parcourir un tableau.
  • x86 autorise un opérande mémoire sur la plupart des instructions arithmétiques (add eax, [rsi+rdi*4]), avec la forme d’adresse la plus riche des trois, mais au plus un opérande mémoire par instruction.

La liberté avec laquelle modes et instructions se combinent s’appelle l’orthogonalité. Dans une ISA totalement orthogonale, chaque instruction accepte chaque mode pour chaque opérande. Les anciens PDP-11 et VAX s’en approchaient. Le x86 en est loin — un seul opérande mémoire, des registres imposés pour certaines instructions. Les RISC load/store contournent la question en réservant la mémoire aux chargements et aux rangements.

Un mode qui n’existe plus : le code auto-modifiant

Avant l’adressage indirect par registre, les programmes parcouraient les tableaux en réécrivant l’adresse dans leurs propres instructions à chaque itération. Von Neumann lui-même l’avait suggéré. Cela a disparu du code ordinaire : cela empêche de partager le code entre processus, entre en conflit avec des caches d’instructions et de données séparés, et devient impossible sur une mémoire marquée non modifiable.

Mais le code qui écrit du code existe toujours. Les compilateurs JIT génèrent du code machine à l’exécution, et les exécutables compressés ou obfusqués déchiffrent leur vrai code avant d’y sauter. Le x86 détecte les écritures sur des instructions déjà dans le pipeline et les vide automatiquement. ARM exige que le programme synchronise explicitement son cache d’instructions avec le nouveau code. Et les deux doivent composer avec les règles W^X de l’OS : rendre la mémoire modifiable, puis exécutable, mais jamais les deux à la fois.

À retenir

  • Un mode d’adressage dit où est un opérande : dans l’instruction (immédiat), dans un registre, à une adresse fixe (direct), à l’adresse d’un registre (indirect par registre), à un registre plus une constante (base + déplacement), à base + index × échelle, relativement au PC, ou sur la pile.
  • Ces modes correspondent directement au C : locales, *p, p->champ, a[i] et globales ont chacun une forme caractéristique, et lea calcule l’adresse sans charger.
  • Des modes plus riches coûtent des octets : déplacements de 1 ou 4 octets, octet SIB, et adressage relatif à rip en mode 64 bits.
  • Les branchements utilisent des décalages relatifs au PC, des cibles indirectes (tables de saut, appels virtuels) et la pile (ret).
  • RISC-V offre un seul mode mémoire, ARM64 ajoute des formes mises à l’échelle et auto-indexées, et x86 autorise des opérandes mémoire dans l’arithmétique. L’orthogonalité mesure la liberté de combinaison entre modes et instructions.

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