Les fonctions et la pile de mémoire

Les fonctions et la pile de mémoire

Une fonction est un bloc nommé d'instructions que vous pouvez utiliser plusieurs fois. Vous y sautez, elle exécute sa tâche, puis rend le contrôle à l'endroit d'où vous l'avez appelée. Cela vous permet d'écrire une logique une seule fois et de l'utiliser depuis de nombreux endroits au lieu de répéter les mêmes lignes dans tout votre programme.

Utilisez pour appeler une fonction : cela saute à l'étiquette et stocke l'adresse de retour dans ra. La fonction se termine par , qui saute en retour à l'adresse contenue dans ra. et sont des pseudo-instructions : des noms pratiques que l'assembleur développe en instructions réelles, et .

Les arguments sont placés dans a0, a1, et les registres qui suivent. La valeur de retour est placée dans a0. Cette organisation est la convention d'appel, l'accord partagé qui permet à des morceaux de code distincts de fonctionner ensemble.

Une fonction square() pour calculer le carré
li a0, 6 # argument
call square # appelle, et ra se souvient où revenir
li a7, 1 # square laisse son résultat dans a0
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # retourne à ra

Suivez l'appel. place l'argument 6 dans a0. saute à l'étiquette square et stocke l'adresse de retour dans ra. À l'intérieur de la fonction, calcule 6 * 6 = 36 et laisse le résultat dans a0, le registre utilisé pour les valeurs de retour. saute en retour à l'instruction juste après l'appel, où et affichent 36, puis le programme se termine.

Appel de fonction et retour
caller
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

L'appelant est sur le point d'appeler A.

Regardez la ligne surlignée effectuer l'aller-retour complet. caller exécute , ce qui enregistre automatiquement le point de retour vers caller dans le registre ra. Avant que A puisse exécuter , il rencontre un problème : l'instruction est sur le point d'écraser ra avec une nouvelle adresse de retour, détruisant celle dont A a besoin pour revenir à son point de départ. Pour corriger cela, A libère de la place sur la pile () et y sauvegarde le ra d'origine (). Il est maintenant sûr d'exécuter . B s'exécute et retourne avec un ordinaire, atterrissant de nouveau à l'intérieur de A. Enfin, A restaure le ra d'origine depuis la pile (), nettoie le pointeur de pile (), et son propre utilise cette valeur restaurée pour sauter avec succès vers caller. Si A avait sauté cette étape, l'instruction aurait écrasé définitivement ra, et le final de A sauterait au mauvais endroit au lieu de revenir à son point de départ.

La pile

Comme il n'existe qu'une poignée de registres, les fonctions qui en appellent d'autres ont besoin d'espace de travail. La pile est de la mémoire que vous empruntez via le pointeur de pile sp : soustrayez pour faire de la place, stockez ce que vous devez conserver, puis restaurez-le avant .

La pile est une structure Last-In, First-Out (LIFO) : la valeur stockée le plus récemment est la première que vous récupérez. En RISC-V, la pile croît vers le bas dans la mémoire. Soustraire de sp réserve un nouvel espace utilisable en toute sécurité. Lorsque vous récupérez les valeurs, vous devez le faire dans l'ordre inverse de leur stockage, sinon le flux d'exécution du programme sera corrompu.

Enregistrer et restaurer ra lors d'un appel
addi sp, sp, -4 # réserve la place pour un mot
sw ra, 0(sp) # sauvegarde l'adresse de retour
# ... appeler d'autres fonctions ici ...
lw ra, 0(sp) # la restaure
addi sp, sp, 4 # libère la place
ret

Chaque / écrase ra, donc si votre fonction appelle une autre fonction, l'appel interne détruit votre adresse de retour et votre propre saute au mauvais endroit. Pour éviter cela, réserve 4 octets sur la pile, et y sauvegarde ra. Une fois les appels internes terminés, restaure l'adresse de retour, et rend l'espace avant .

⚠
Si vous avez appelé quoi que ce soit, restaurez ra avant , et ramenez toujours sp à la valeur qu'il avait au début. Une pile qui ne correspond pas à son état initial est la cause habituelle d'un plantage dans les programmes en assembleur.