Assembleur x86 vs x64 : différences clés
La différence entre l'assembleur x86 et x64 : registres, conventions d'appel, adressage RIP-relatif, la red zone, et pourquoi cela compte en reversing.
Si vous faites du reverse engineering de binaires, la confusion la plus fréquente consiste à mélanger les règles du code 32 bits et 64 bits. Ils se ressemblent — mêmes mnémoniques, même registre de drapeaux, même modèle CPU global — mais les conventions sous-jacentes diffèrent suffisamment pour que lire du x64 avec un modèle mental x86 vous induise silencieusement en erreur. Ce guide couvre les différences pratiques qui changent votre façon de lire un listing désassemblé. Pour le jeu d'instructions complet, gardez la référence ISA x86/x64 ouverte dans un autre onglet.
Registres : largeur et nombre
Le changement majeur est la largeur. Les registres généraux 32 bits (eax, ebx, ecx, edx, esi, edi, esp, ebp) deviennent des registres 64 bits avec un préfixe r : rax, rbx, et ainsi de suite. Chaque registre reste adressable à ses largeurs plus étroites, de sorte que le même registre physique expose rax (64 bits), eax (32 bits), ax (16 bits) et al (8 bits).
Le second changement est le nombre. x86 possède 8 registres généraux ; x64 en ajoute huit de plus — r8 à r15 — pour un total de 16 GPR. Ces nouveaux registres ont aussi des noms spécifiques à leur largeur : r8, r8d (32 bits), r8w (16 bits) et r8b (8 bits).
Une règle subtile piège presque tout le monde : l'écriture dans un sous-registre 32 bits met à zéro les 32 bits de poids fort du registre complet. L'écriture dans les parties 16 bits ou 8 bits ne le fait pas.
mov rax, 0x1122334455667788
mov eax, 1 ; rax vaut maintenant 0x0000000000000001 (moitié haute mise à zéro)
mov ax, 2 ; seuls les 16 bits de poids faible changentCe comportement d'extension par zéro explique pourquoi les compilateurs émettent xor eax, eax pour effacer rax — c'est plus court qu'un effacement 64 bits et produit le même résultat.
Taille des pointeurs et des adresses
En x86, les pointeurs font 4 octets et l'espace d'adressage virtuel est de 32 bits. En x64, les pointeurs font 8 octets et les adresses sont de 64 bits (bien que le matériel actuel utilise généralement des adresses canoniques de 48 bits). Lorsque vous repérez des emplacements de pile espacés de 8 octets, ou des push/pop déplaçant le pointeur de pile de 8, vous regardez du code 64 bits. Se tromper là-dessus fausse chaque calcul d'offset que vous faites à la main. Le glossaire contient des entrées pour les termes d'adressage utilisés tout au long de cet article.
Conventions d'appel
C'est là que la lecture du flux des arguments tourne mal le plus souvent.
32 bits (cdecl / stdcall) : les arguments sont empilés sur la pile, généralement de droite à gauche. La différence entre les deux est de savoir qui nettoie la pile — l'appelant (cdecl) ou l'appelé via ret N (stdcall). La valeur de retour revient dans eax.
x64 System V AMD64 (Linux, macOS) : les six premiers arguments entiers passent dans rdi, rsi, rdx, rcx, r8, r9. Les arguments à virgule flottante utilisent xmm0–xmm7. La valeur de retour est dans rax.
x64 Microsoft fastcall (Windows) : les quatre premiers arguments entiers passent dans rcx, rdx, r8, r9. L'appelant doit aussi réserver 32 octets de shadow space sur la pile pour ces quatre registres, même lorsqu'ils sont passés dans des registres.
| Aspect | x86 (cdecl/stdcall) | x64 System V | x64 Microsoft |
|---|---|---|---|
| Passage des arguments | Pile (push) | rdi, rsi, rdx, rcx, r8, r9 | rcx, rdx, r8, r9 |
| Arguments en registres | 0 | 6 entiers + 8 SSE | 4 (entier ou flottant) |
| Valeur de retour | eax | rax | rax |
| Nettoyage de la pile | appelant / appelé | appelant | appelant |
| Shadow space | aucun | aucun | 32 octets |
| Alignement de la pile | 4 octets | 16 octets | 16 octets |
À retenir lors du reversing : avant de nommer les arguments d'une fonction, confirmez l'OS. Un wrapper de syscall Linux et un thunk d'API Windows se lisent complètement différemment même si les deux sont en x64. Vous pouvez parcourir les deux conventions en direct dans le simulateur de CPU interactif pour observer les registres changer.
Adressage RIP-relatif
x86 référence les données globales avec des adresses absolues figées dans l'instruction. x64 introduit l'adressage RIP-relatif, où l'opérande est encodé comme un déplacement signé de 32 bits par rapport au pointeur d'instruction (rip).
; x86 — adresse absolue
mov eax, [0x00403010]
; x64 — relatif à l'instruction suivante
mov eax, [rip + 0x2ff0] ; se résout en un global fixe au moment du linkC'est l'épine dorsale du code indépendant de la position, de sorte que les exécutables PIE et la plupart des bibliothèques partagées en regorgent. Dans un désassembleur, la destination est généralement pré-calculée pour vous, mais lorsque vous patchez des octets à la main, vous devez recalculer le déplacement par rapport à la fin de l'instruction, pas à son début.
La red zone
System V AMD64 définit une red zone de 128 octets sous rsp qu'une fonction feuille (qui n'appelle rien) peut utiliser comme espace de travail sans ajuster le pointeur de pile. Vous verrez des fonctions lire et écrire [rsp - 8], [rsp - 16], et ainsi de suite sans sub rsp préalable. C'est légal et attendu sous Linux/macOS. L'ABI Windows x64 n'a pas de red zone, donc son absence est un indice de plus sur la convention que vous lisez.
Appels système
Les transitions du userland vers le kernel ont complètement changé.
; Linux 32 bits — write(1, buf, len)
mov eax, 4 ; __NR_write (table 32 bits)
mov ebx, 1
mov ecx, buf
mov edx, len
int 0x80
; Linux 64 bits — write(1, buf, len)
mov rax, 1 ; __NR_write (table 64 bits — numéro différent !)
mov rdi, 1
mov rsi, buf
mov rdx, len
syscallTrois choses diffèrent : l'instruction (int 0x80/sysenter contre le syscall dédié), les registres d'arguments (ebx, ecx, edx... contre rdi, rsi, rdx...), et les numéros de syscall eux-mêmes — les tables 32 bits et 64 bits ne sont pas les mêmes. syscall écrase aussi rcx et r11, que vous verrez sauvegardés autour de l'appel. Mal lire la table est une façon classique de mal étiqueter le comportement d'un échantillon de malware.
Différences d'encodage des instructions
x64 ajoute le préfixe REX (un octet dans la plage 0x40–0x4F) devant de nombreuses instructions. Il encode la largeur des opérandes (le bit REX.W sélectionne le 64 bits) et le bit de poids fort supplémentaire nécessaire pour adresser r8–r15. Cela signifie que le même octet d'opcode peut se décoder différemment selon le préfixe, de sorte qu'un désassembleur réglé sur le mauvais mode produit un non-sens convaincant. Cela compte aussi pour des astuces comme les instructions chevauchantes, où sauter en plein milieu d'une instruction donne un décodage différent en mode 32 bits contre 64 bits.
Pourquoi cela compte en reversing
La confusion de mode n'est pas académique. Réglez IDA, Ghidra ou objdump sur la mauvaise largeur de bits et vous obtenez des déchets d'apparence plausible. Connaître les conventions vous permet de :
- Lire correctement le flux des arguments par OS au lieu de deviner.
- Reconnaître l'obfuscation qui dissimule des données en ligne, comme les stack strings, qui se présentent différemment selon les deux ABI à cause de la pression sur les registres et de la disposition de la pile.
- Patcher les références RIP-relatives et absolues sans casser les offsets.
Pour le workflow plus large d'identification du mode, de la convention et de l'intention dans un binaire, consultez le catalogue complet des techniques de reverse engineering.
Prochaines étapes
Chargez un binaire dans le simulateur de CPU interactif, exécutez pas à pas un prologue de fonction, et observez quels registres se remplissent avant l'exécution de la première instruction — cette seule habitude vous indique la largeur de bits et la convention d'appel plus vite que n'importe quelle antisèche. Puis plongez dans la référence ISA pour les détails d'encodage derrière tout ce qui précède.