JMP vs CALL en assembleur x86 : La pile fait toute la différence
En assembleur x86, JMP et CALL transfèrent tous deux le contrôle vers une autre adresse, mais leur comportement sur la pile crée des sémantiques de flux de contrôle fondamentalement différentes. Comprendre cette distinction est essentiel pour écrire sous-routines, implémenter récursivité, et déboguer flux contrôle.
Différence fondamentale : Interaction pile
| Aspect | JMP destination | CALL destination |
|---|---|---|
| Effet pile | Aucun — pile inchangée | Empile adresse retour (EIP/RIP) |
| Mécanisme retour | Aucun — transfert aller simple | RET dépile adresse, reprend après CALL |
| Usage typique | Boucles, conditionnels, branches inconditionnelles | Sous-routines, fonctions, procédures |
| Support imbrication | Non — ne peut imbriquer proprement | Oui — pile permet imbrication arbitraire |
| Récursivité | Impossible sans gestion pile manuelle | Support natif via frames pile |
Encodage instructions (x86-64)
; Variantes JMP EB cb JMP rel8 ; Saut court (-128 à +127 octets) E9 cw JMP rel16/32 ; Saut relatif proche FF /4 JMP r/m16/32/64 ; Saut indirect (registre/mémoire) EA cd JMP ptr16:16/32 ; Saut loin (segment:offset) - legacy ; Variantes CALL E8 cw CALL rel16/32 ; Appel relatif proche (empile RIP) FF /2 CALL r/m16/32/64 ; Appel indirect 9A cd CALL ptr16:16/32 ; Appel loin (legacy)
Mécanique frame pile
Flux exécution CALL
; Avant CALL RIP = adresse instruction suivante RSP = pointeur pile actuel ; CALL cible exécute : 1. RSP ← RSP - 8 ; Décrémente pointeur pile (64-bit) 2. [RSP] ← RIP ; Empile adresse retour (instruction suivante) 3. RIP ← cible ; Saute vers sous-routine ; Dans sous-routine (prologue typique) : PUSH RBP ; Sauvegarde base pointer appelant MOV RBP, RSP ; Établit nouveau frame pointer SUB RSP, N ; Alloue espace pile local ; Exécution RET : 1. RIP ← [RSP] ; Dépile adresse retour 2. RSP ← RSP + 8 ; Restaure pointeur pile 3. Exécution reprend après CALL
Flux exécution JMP
; JMP cible exécute : 1. RIP ← cible ; Saut inconditionnel ; Pointeur pile (RSP) inchangé ; Aucune adresse retour stockée ; Pas de retour automatique possible
Implications pratiques
Pattern sous-routine (CALL/RET)
; Appelant CALL calculer_somme ; Empile adresse retour, saute ; ... reprend ici après RET ; Sous-routine calculer_somme: PUSH RBP MOV RBP, RSP ; ... calcul ... MOV RAX, résultat POP RBP RET ; Dépile adresse retour, saute retour
Pattern boucle/machine états (JMP)
boucle_etat: CMP AL, 'A' JE traiter_A CMP AL, 'B' JE traiter_B JMP gestionnaire_défaut traiter_A: ; ... traite A ... JMP caractère_suivant traiter_B: ; ... traite B ... JMP caractère_suivant gestionnaire_défaut: ; ... erreur ... JMP caractère_suivant caractère_suivant: ; ... récupère suivant ... JMP boucle_etat ; Reboucle
Avancé : Optimisation appel terminal
Compilateurs remplacent parfois séquence CALL ... RET par JMP quand dernière action fonction est appel autre fonction :
; Sans optimisation foo: CALL bar RET ; Avec optimisation appel terminal (TCO) foo: JMP bar ; Réutilise frame pile appelant
Avantages : Économie espace pile, retour plus rapide. Requiert : pas nettoyage après appel, même convention appel.
Considérations sécurité
- Return-Oriented Programming (ROP) : Exploite chaînes
RET—CALLcrée adresses retour prévisibles - Control Flow Integrity (CFI) : Matériel (Intel CET, ARM PAC) valide cibles
CALL/RET - Shadow Stacks : Pile séparée protégée pour adresses retour (CET)
Résumé
- JMP = "Va là" (pas retour, pas changement pile)
- CALL = "Va là, reviens ici" (empile adresse retour, permet
RET) - CALL/RET = Fondations programmation procédurale, récursivité, code modulaire
- JMP = Fondations boucles, machines états, flux conditionnel
Chaque appel fonction haut niveau (func() en C, def en Python, method() en Java) compile en machinerie CALL/RET. Chaque while, for, if, goto compile en variantes JMP. La pile est ce qui rend CALL puissant — et ce qui rend débordements tampon dangereux.
