Un CPU n’exécute pas des programmes au sens où on l’entend. Il exécute des instructions : de minuscules opérations fixes comme « copier cette valeur », « additionner ces deux nombres », « sauter à cette adresse ». Un programme en est une longue liste en mémoire, et le CPU les exécute l’une après l’autre, des milliards de fois par seconde.
Le langage d’assemblage est la forme lisible de ces instructions : une ligne de texte par instruction. C’est ce qu’affiche un désassembleur quand on ouvre un binaire, et ce que produit un compilateur à partir du C juste avant d’en faire du code machine.
Anatomie d’une instruction
loop: add eax, ecx ; eax = eax + ecx
| Partie | Ici | Rôle |
|---|---|---|
| étiquette (facultative) | loop: | un nom pour l’adresse de cette instruction, que les sauts peuvent viser |
| mnémonique | add | l’opération |
| opérandes | eax, ecx | ce sur quoi elle opère |
| commentaire | ; eax = eax + ecx | ignoré par l’assembleur |
Dans la syntaxe Intel utilisée sur ce site, le premier opérande est la destination. add eax, ecx signifie eax ← eax + ecx. La plupart des instructions x86 prennent deux opérandes, et la destination est aussi l’une des entrées : l’ancienne valeur de eax est remplacée par le résultat.
Trois sortes d’opérandes
Chaque opérande est l’une de ces trois choses :
- un registre, une case de stockage nommée à l’intérieur du CPU :
eax,rbx,r8… Le chapitre sur les registres les présente. - une valeur immédiate, une constante écrite dans l’instruction elle-même :
5,0xff,-1. - un opérande mémoire, écrit entre crochets : la valeur située à une adresse.
[rbp-4]signifie « la mémoire à l’adresserbpmoins 4 ». Les différentes façons de calculer cette adresse — base, index, facteur d’échelle, déplacement — sont les modes d’adressage du niveau ISA.
mov eax, 5 ; immédiat → registre
mov ecx, eax ; registre → registre
mov DWORD PTR [rbp-4], ecx ; registre → mémoire
mov edx, DWORD PTR [rbp-4] ; mémoire → registre
DWORD PTR donne la taille d’un accès mémoire : BYTE (8 bits), WORD (16), DWORD (32) ou QWORD (64). Avec un opérande registre, le nom du registre suffit à indiquer la taille — eax fait 32 bits — et le mot-clé est facultatif. Avec seulement un opérande mémoire et une valeur immédiate, il est obligatoire : mov [rbp-4], 5 ne dit pas s’il faut écrire 1, 2, 4 ou 8 octets, et l’assembleur le refuse pour taille d’opérande ambiguë.
Ce que modifie une instruction
Chaque instruction lit un peu d’état et en écrit un peu. Les effets possibles sont peu nombreux :
- les registres — le registre de destination reçoit une nouvelle valeur ;
- les drapeaux — les instructions arithmétiques et logiques notent des faits sur leur résultat : nul, négatif, débordement… (le chapitre sur les drapeaux) ;
- la mémoire — un rangement écrit des octets à une adresse ;
rip, le pointeur d’instruction — toujours, car il passe à l’instruction suivante, ou à la cible d’un saut.
Avancez pas à pas dans ce programme et observez chaque sorte de changement :
- mov ecx, 7 ; un registre
- mov eax, 5
- add eax, ecx ; un registre et les drapeaux
- mov DWORD PTR [rbp-4], eax ; la mémoire
mov ne fait que copier : il ne touche jamais aux drapeaux. add écrit eax = 12 et met à jour les drapeaux. Le dernier mov écrit 12 sur la pile, en [rbp-4]. Et rip avance à chaque pas.
Les règles
Le CPU n’implémente que certaines combinaisons d’opérandes, et l’assembleur refuse les autres. Voici celles que vous rencontrerez :
| Interdit | Pourquoi | À la place |
|---|---|---|
mov DWORD PTR [rbp-4], DWORD PTR [rbp-8] | au plus un opérande mémoire explicite par instruction | passer par un registre |
mov 5, eax | une valeur immédiate ne peut pas être une destination | mov eax, 5 |
mov eax, bx | les deux opérandes doivent avoir la même taille | movzx eax, bx (extension par zéro) |
mov [rbp-4], 5 | taille inconnue | mov DWORD PTR [rbp-4], 5 |
add rax, 0x100000000 | les immédiats font au plus 32 bits (étendus avec le signe), sauf dans mov | le charger d’abord dans un registre |
Ces cinq lignes sont refusées par un vrai assembleur : invalid operand for instruction ou ambiguous operand size. (Les instructions de chaîne comme movsb font exception : elles copient de mémoire à mémoire, avec les deux adresses implicites dans rsi et rdi.) La règle d’un seul opérande mémoire explique pourquoi le code compilé est plein de chargements dans des registres : le CPU calcule sur les registres, et la mémoire sert surtout à les remplir et à y ranger les résultats.
Deux façons d’écrire la même chose : Intel et AT&T
La même instruction machine peut s’écrire dans deux syntaxes d’assembleur. La syntaxe Intel est celle des manuels d’Intel, de NASM, MASM, IDA, Ghidra et de ce site. La syntaxe AT&T est la syntaxe traditionnelle des outils GNU : c’est ce qu’affichent par défaut objdump et gdb sous Linux.
| Intel | AT&T | |
|---|---|---|
| ordre des opérandes | destination, source | source, destination |
| registres | eax | %eax |
| immédiats | 5 | $5 |
| taille | d’après les opérandes, ou DWORD PTR | suffixe du mnémonique : b, w, l, q |
| mémoire | [rbx+rcx*8+16] | 16(%rbx,%rcx,8) |
Les trois mêmes instructions, désassemblées dans les deux syntaxes :
Intel AT&T
add eax, ecx addl %ecx, %eax
mov eax, 0x5 movl $0x5, %eax
mov rax, qword ptr [rbx + 8*rcx + 0x10] movq 0x10(%rbx,%rcx,8), %rax
Pour obtenir la syntaxe Intel avec les outils GNU : objdump -d -M intel, ou set disassembly-flavor intel dans gdb. Une fois qu’on sait que l’ordre est inversé, lire l’une ou l’autre n’est qu’une question d’habitude.
Derrière le texte : des octets
Le CPU ne voit jamais le texte. L’assembleur transforme chaque ligne en code machine, quelques octets qui encodent l’opération et ses opérandes :
01 c8 add eax, ecx
b8 05 00 00 00 mov eax, 0x5
48 8b 44 cb 10 mov rax, qword ptr [rbx + 8*rcx + 0x10]
c7 45 fc 0a 00 00 00 mov dword ptr [rbp - 0x4], 0xa
c3 ret
48 b8 88 77 66 55 44 33 22 11 movabs rax, 0x1122334455667788
Les instructions x86 font de 1 à 15 octets. ret tient en un octet, et un mov avec une constante de 64 bits en demande dix. Les constantes apparaissent en petit-boutiste dans les octets : 05 00 00 00 vaut 5. La disposition des bits — préfixes, opcode, ModRM, déplacement, immédiat — relève de l’architecture du jeu d’instructions, et le chapitre du niveau ISA sur les formats d’instructions la décortique. À ce niveau-ci, une ligne d’assembleur est une instruction, et cela suffit.
Combien d’instructions existe-t-il ?
Le x86-64 compte environ un millier de mnémoniques quand on inclut toutes les instructions SIMD et système, et bien plus de variantes de chacune. Mais le code réel en utilise un petit noyau. En comptant les instructions des versions x86-64 de quatre programmes ordinaires — ssh, vim, zip et curl, environ 764 000 instructions au total :
| Rang | Mnémonique | Part |
|---|---|---|
| 1 | mov | 31,7 % |
| 2 | call | 7,8 % |
| 3 | lea | 7,7 % |
| 4 | cmp | 6,6 % |
| 5 | je | 5,4 % |
| 6 | test | 5,3 % |
| 7 | pop | 4,4 % |
| 8 | push | 4,4 % |
| 9 | xor | 4,1 % |
| 10 | jmp | 4,0 % |
Ces quatre programmes utilisent 284 mnémoniques différents, mais les 10 premiers couvrent 81 % de leurs instructions et les 20 premiers 93 %. Près d’un tiers de tout, c’est mov : l’essentiel du travail d’un programme consiste à déplacer des données entre la mémoire et les registres. Apprendre quelques dizaines d’instructions suffit pour lire l’essentiel d’un désassemblage. Le chapitre suivant en fait le tour.
À retenir
- Une instruction est une opération élémentaire. L’assembleur l’écrit sous forme d’un mnémonique suivi d’opérandes, la destination en premier en syntaxe Intel.
- Les opérandes sont des registres, des immédiats ou de la mémoire (
[...]), avec une taille : byte, word, dword, qword. - Une instruction peut modifier des registres, les drapeaux, la mémoire et
rip— rien d’autre. - Les règles : au plus un opérande mémoire explicite, des tailles identiques, pas de destination immédiate, et des immédiats limités à 32 bits hors de
mov. - La syntaxe AT&T inverse les opérandes et ajoute
%,$et des suffixes de taille. - Chaque ligne s’assemble en 1 à 15 octets de code machine, et quelques dizaines de mnémoniques forment la quasi-totalité du code réel.