8.1 Les instructions call et ret
Les instructions call et ret permettent respectivement d'appeler et de sortir d'une procédure. L'opérande qui suit call est l'adresse symbolique de la procédure. Traditionnellement, les paramètres de la procédure sont passés par la pile.
call empile tout d'abord l'adresse de la prochaine instruction (le contenu de %rip) et remplace le contenu de rip par l'adresse donnée en opérande. ret
dépile et place la valeur dépilée dans rip, ce qui provoque (sauf erreur de programmation) le retour à l'endroit où la procédure à été appelée (instruction qui suit le « call adresse »). Comme la pile n'est pas uniquement utilisée pour sauvegarder l'adresse de retour, mais aussi pour stocker les paramètres et les
14 Il existe une version 16 bits de ce registre (sp), mais nous ne pouvons l'utiliser que dans un mode particulier du processeur que nous ne verrons pas ici.
variables locales de la procédure, il est important de s'assurer que le haut de la pile contient bien l'adresse de retour avant l'exécution de ret.
ret peut avoir une opérande n de type « constante entière ». Dans ce cas, ret
dépilera l'adresse de retour, puis n octets avant de retourner à la procédure appelante. Le registre rbp (re-extended base pointer) est souvent utilisé par le programmeur pour accéder aux différentes zones de la pile, il pointe généralement sur l'adresse de retour de la procédure.
Voici un exemple de procédure qui prend 2 entiers non signés comme paramètres (par la pile) et renvoie le maximum des 2 valeurs :
appel:
# Empilage des paramètres de la procédure Maximum push UnsignedQuadWord_a
push UnsignedQuadWord_b
call Maximum # appel procédure Maximum
popq %rax # résultat dans rax
# <<suite de la procédure appelante >>
retq # retour à la procédure appelante Maximum:
movq %rsp, %rbp
# rbp pointe sur l'adresse de retour pushq %rax # sauve ancienne valeur de rax pushq %rbx # sauve ancienne valeur de rbx movq 8(%rbp), %rbx # copie le paramètre
# b dans rbx
movq 16(%rbp), %rax # copie le paramètre # a dans rax
cmpq %rax, %rbx # compare le 1er et le 2e # paramètre
jae bmax # Si b >= a alors saute à bmax
amax: pushq %rax # empile a
jmp FinMax
bmax: pushq %rbx # empile b
FinMax: popq %rax # valeur max -> rax movq %rax, 8(%rbp)
# valeur max -> 1er paramètre
popq %rbx # récupère ancienne valeur de %rbx popq %rax # récupère ancienne valeur de %rax retq $8 # dépile le 2e paramètre avant de # retourner à la procédure appelante
Voici un schéma qui illustre l'exécution de procédures imbriquées. Les chiffres représentent l'ordre d'exécution du code :
Et voici l'évolution de la pile lors de l'exécution schématisée ci-dessus : 1. {} 2. {adresse de 5} 3. {adresse de 5, adresse de 4} 4. {adresse de 5} 5. {} 6. {adresse de 7} 7. {} procA : .... call C .... ret
2
4
procC : ... ret .... call procA ... call procB ... ret procB : ... ret1
5
3
6
7
8.2 Les interruptions et les exceptions
Il nous manque encore un dispositif pour permettre au processeur de réagir en temps réel à divers évènements, liés aux périphériques (appui d'une touche sur le clavier, clic ou mouvement de la souris, etc.) ou à l'état du processeur lui- même (division par zéro, accès à une zone mémoire protégée, etc.). Un simple
balayage permanent de ces très nombreuses données gaspillerait un montant
considérable de ressources.
Heureusement, tous les processeurs actuels possèdent un jeu d'instructions qui permettent une gestion événementielle de cette problématique. Par exemple, un périphérique, lorsqu'il est sollicité par l'utilisateur, envoie un signal (qu'on appelle interruption) au microprocesseur, qui peut alors interrompre l'exécution du programme courant, exécuter une routine du système d'exploitation et revenir au programme courant lorsque l'interruption a été traitée.
En réalité, n'importe quel programme peut générer des interruptions logicielles et des exceptions. Mais nous ne rentrerons pas ici dans des détails qui relèveraient plus d'un cours sur les systèmes d'exploitation que d'un cours sur l'assembleur et le langage machine.
Nous allons limiter cette section à la description de l'instruction int, qui va nous permettre de communiquer avec le système d'exploitation (pour les exemples, nous prendrons GNU/Linux).
L'instruction int a un comportement similaire à l'instruction call. Le registre
rip va être modifié pour permettre l'exécution d'une procédure, mais l'adresse
de l'instruction int est mémorisée, de telle sorte qu'à la sortie de la procédure,
rip prend pour valeur l'adresse de l'instruction qui suit l'instruction int.
La syntaxe de l'instruction int est la suivante : int Constante
Constante est un entier qui désigne le numéro du gestionnaire d'interruption à appeler. La valeur de certains registres sera utilisée par le gestionnaire d'interruptions afin de sélectionner la procédure appropriée et de lui transmettre ses paramètres.
Sous GNU/Linux, par exemple, int $0x80 va appeler le gestionnaire d'interruption du système d'exploitation (le gestionnaire de numéro hexadécimal 80, soit 128 en décimal) . Le numéro de fonction système est donné par %eax et les paramètres sont transmis via les registres %ebx,%ecx, %edx,%esi,%edi (dans cet ordre). Lorsqu'il y a plus de 5 paramètres, %ebx contient tout simplement l'adresse des paramètres.
Voici par exemple, un petit programme en assembleur, destiné à être assemblé, lié et exécuté sous GNU/Linux :
.data # directive de création d'une zone de donnée btlm: # adresse symbolique pointant sur la chaîne: .string "Bonjour tout le monde!\n" .text # directive de création d'une zone # d'instructions .globl main # directive de création d'une étiquette # de portée globale main: # main est l'adresse de début du programme movl $4,%eax # sélection de la fonction # write du système movl $1,%ebx # dernier paramètre de write : # stdout movl $btlm,%ecx # premier paramètre # de write : l'adresse de # la chaîne de caractères à # afficher movl $23,%edx # le nombre de caractères à # afficher : 23 int $0x80 # appel de l'interruption # 128 > GNU/Linux movl $1,%eax # sélection de la fonction # exit du système xorl %ebx,%ebx # mise à zéro du 1er paramètre # en utilisant # xor, c'est à dire ou exclusif int $0x80 # appel de l'interruption # 128 > GNU/Linux ret # fin du programme et retour au système