La pile
Ce que ce chapitre apporte
- Expliquer pourquoi la pile part du haut de la mémoire et descend, alors que les données montent.
- Empiler puis dépiler un mot avec les deux gestes qui conviennent, et relever la valeur de sp après chaque instruction.
- Lire la figure de la pile case par case, et dire à quelle case le pointeur sp renvoie.
- Réserver un cadre de plusieurs mots d'un seul mouvement, et y ranger par décalages.
- Repérer un rééquilibrage oublié, et dire ce que le code qui suit lira à la place de ce qu'il attendait.
Les registres sont trente-deux, et ce nombre ne changera pas au cours du programme. Un calcul qui a besoin de retenir davantage de valeurs doit donc les poser quelque part, puis les reprendre. Les chapitres précédents ont montré où poser une donnée dont l'adresse est connue d'avance : une déclaration, un nom, une case. Reste tout le reste, c'est-à-dire l'essentiel : les valeurs dont un fragment de programme a besoin le temps de son exécution, et dont plus personne ne veut ensuite. Pour celles-là, la machine ne réserve pas de case nommée. Elle réserve une discipline, une seule, qui tient en deux instructions et qui porte un nom : la pile. Ce chapitre l'installe et la fait voir grandir, parce que le chapitre suivant en dépend entièrement.
Trente-deux cases, et tout le reste
Un processeur RISC-V a trente-deux registres, dont un qui vaut toujours zéro et deux ou trois qui ont déjà un emploi réservé. Il en reste donc moins de trente pour calculer. C'est beaucoup pour une addition, et très peu dès qu'un programme fait deux choses l'une après l'autre : le fragment qui vient ensuite a lui aussi besoin de registres, et il ne connaît pas ceux que le fragment précédent comptait garder.
Une première réponse consiste à déclarer des données nommées, comme au chapitre 2, et à y ranger ce qui déborde. Elle échoue sur trois points. Une donnée déclarée occupe une adresse fixe, choisie à l'assemblage, alors qu'une valeur temporaire n'a besoin d'exister que pendant quelques instructions. Une donnée déclarée porte un nom, alors qu'il faudrait en inventer un par valeur intermédiaire et par fragment de code. Surtout, une donnée déclarée est unique : si le même fragment de code s'exécute deux fois sans que la première soit terminée, ce qui arrive dès qu'une fonction s'appelle elle-même, les deux exécutions se disputent la même case et l'une écrase l'autre.
Ce qu'il faut est donc une zone de mémoire où l'on prend de la place pour un moment, où l'on repose ce qu'on avait pris, et où la place reprise redevient disponible. Une seule règle suffit à organiser cela sans tenir aucune comptabilité : ce qui est rangé en dernier est repris en premier. Cette règle porte le nom de la chose, une pile, comme une pile d'assiettes dont on ne retire que celle du dessus.
Une pile est une zone de mémoire gérée par une seule règle : on ne prend et on ne rend de la place qu'à une extrémité, appelée le sommet. L'adresse de ce sommet tient dans un registre, sp, le pointeur de pile. Il n'y a pas d'instruction « empiler » ni « dépiler » dans le matériel : il y a un registre, des rangements, des chargements, et une convention que tout le monde respecte.
Une mémoire, deux occupants
La mémoire d'un programme est un seul grand tableau d'octets, partagé entre les données déclarées et la pile. Les données, elles, montent : la première déclaration se pose à l'adresse 0x1000, la suivante juste après, et ainsi de suite. Plus un programme déclare de données, plus loin vers le haut il arrive.
Si la pile montait elle aussi, il faudrait décider au départ où les données s'arrêtent et où la pile commence. Ce partage serait faux presque toujours : un programme qui déclare un gros tableau et n'empile presque rien perdrait de la place à un bout, et un programme qui déclare trois mots mais appelle des fonctions profondément imbriquées buterait sur une frontière posée trop bas, avec de la mémoire libre juste derrière.
Faire partir les deux occupants des deux bouts supprime la question. Les données montent depuis le bas, la pile descend depuis le haut, et la seule limite est leur rencontre. Aucune frontière n'est fixée à l'avance ; c'est l'usage réel qui décide du partage, programme par programme et instant par instant.
programme
- 0x0mv t0, spaddi t0, sp, 0
- 0x4addi t1, sp, -4
registres
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le sommet de départ vaut ici 0x2000, soit 8 192 en décimal. Ce n'est pas un nombre magique : c'est le réglage par défaut des figures de ce module, modifiable par une ligne pile:. Sur une machine réelle, l'adresse est bien plus haute et le système d'exploitation la choisit au lancement, mais le principe est identique.
Le programme suivant met les deux occupants dans la même figure : trois mesures déclarées en bas, un mot empilé en haut, et la soustraction qui mesure ce qui reste libre entre les deux.
programme
- 0x0la t0, mesureslui t0, 1
- 0x4addi zero, zero, 0
- 0x8addi sp, sp, -4
- 0xcsw t0, 0(sp)
- 0x10sub t1, sp, t0
registres
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 |
mémoire
| 0x1000 | mesures | 10 |
| 0x1004 | 20 | |
| 0x1008 | 30 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Quatre mille quatre-vingt-douze octets libres entre le dernier octet utilisé par les données et le sommet courant de la pile. Aucune ligne du programme n'a fixé cette valeur : elle est la conséquence de ce que le programme a réellement déclaré et réellement empilé.
Empiler : réserver, puis écrire
Empiler un mot demande deux instructions, toujours les mêmes, toujours dans cet ordre.
programme
- 0x0addi sp, sp, -4
- 0x4sw t0, 0(sp)
- 0x8li t0, 0addi t0, zero, 0
registres
| sp | 0x2000 | |
| t0 | 42 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
La première instruction réserve : elle retire quatre à sp, qui passe de 0x2000 à 0x1ffc. Rien n'est écrit, mais quatre octets viennent d'être déclarés occupés. La seconde écrit dans la case ainsi réservée, à l'adresse que sp désigne maintenant.
L'ordre des deux n'est pas indifférent. Écrire d'abord à -4(sp) puis déplacer sp donnerait le même contenu, mais il existerait un instant, entre les deux instructions, où un mot serait écrit en dehors de la zone réservée. Sur une machine de bureau ce n'est qu'un détail de style ; sur une machine qui interrompt un programme pour en traiter un autre, l'interruption qui survient à cet instant écrase le mot. La règle est donc : réserver d'abord, écrire ensuite.
La troisième instruction du programme est là pour la démonstration. Une fois le mot empilé, t0 peut servir à autre chose : c'est exactement la raison d'être de la manœuvre.
Dépiler : lire, puis rendre
Reprendre le mot demande les deux mêmes instructions, à l'envers.
programme
- 0x0addi sp, sp, -4
- 0x4sw t0, 0(sp)
- 0x8li t0, 7addi t0, zero, 7
- 0xclw t0, 0(sp)
- 0x10addi sp, sp, 4
registres
| sp | 0x2000 | |
| t0 | 42 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le chargement reprend la valeur, l'addition rend la place. Là encore l'ordre compte, et pour la même raison : rendre la place avant d'avoir lu laisserait un instant pendant lequel la valeur à lire se trouve en dehors de la zone réservée.
À la fin de ce programme, sp vaut de nouveau 0x2000 et la figure n'affiche plus aucune case de pile. Le mot 42 est pourtant toujours écrit à l'adresse 0x1ffc : dépiler n'efface rien, cela déclare seulement la place disponible. Le prochain empilement écrira par-dessus.
Empiler un mot, c'est addi sp, sp, -4 puis sw registre, 0(sp). Le dépiler, c'est lw registre, 0(sp) puis addi sp, sp, 4. Un fragment de code qui empile trois mots doit dépiler trois mots. La machine ne le vérifie pas : c'est celui qui écrit le code qui répond de l'équilibre.
Voir la pile grandir
Trois empilements de suite, et la figure montre trois cases là où il n'y en avait aucune.
programme
- 0x0addi sp, sp, -4
- 0x4sw t0, 0(sp)
- 0x8addi sp, sp, -4
- 0xcsw t1, 0(sp)
- 0x10addi sp, sp, -4
- 0x14sw t2, 0(sp)
- 0x18lw t3, 0(sp)
- 0x1clw t4, 4(sp)
- 0x20lw t5, 8(sp)
registres
| sp | 0x2000 | |
| t0 | 11 | |
| t1 | 22 | |
| t2 | 33 | |
| t3 | 0 | |
| t4 | 0 | |
| t5 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
La figure affiche les cases du sommet courant jusqu'au sommet de départ, et signale d'une marque celle que sp désigne. Le mot empilé en dernier, 33, se trouve à l'adresse la plus basse, et c'est celui que lw t3, 0(sp) ramène. Le premier empilé, 11, se trouve tout en haut, à 8(sp), et il faudrait dépiler les deux autres pour l'atteindre par le sommet. Voilà la règle du dernier arrivé, premier sorti, non plus énoncée mais lue sur des adresses.
Les trois derniers chargements montrent au passage qu'une pile n'interdit pas de regarder plus bas que le sommet. Un décalage constant à partir de sp donne accès à n'importe quelle case déjà réservée, sans rien déplacer. C'est ce qui rend l'écriture suivante possible.
Réserver un cadre d'un seul mouvement
Quand un fragment de code sait combien de mots il lui faut, il n'a aucune raison de déplacer sp une fois par mot. Une seule soustraction réserve le tout, et les rangements se font par décalages.
programme
- 0x0addi sp, sp, -12
- 0x4sw t0, 8(sp)
- 0x8sw t1, 4(sp)
- 0xcsw t2, 0(sp)
- 0x10addi sp, sp, 12
registres
| sp | 0x2000 | |
| t0 | 11 | |
| t1 | 22 | |
| t2 | 33 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Cette zone réservée en une fois porte un nom, le cadre. Elle contient tout ce dont un fragment de code a besoin le temps de son exécution, et elle disparaît d'un seul addi sp, sp, 12 à la fin. Le chapitre suivant montrera que c'est exactement la forme que prend une fonction : un cadre à l'entrée, un cadre rendu à la sortie, et entre les deux un code qui travaille par décalages à partir de sp.
Un détail de lecture mérite l'attention, parce qu'il surprend au premier abord. Le décalage 0 désigne l'adresse la plus basse du cadre, et le décalage le plus grand l'adresse la plus haute. Les cases se remplissent donc vers le bas quand les décalages diminuent, ce qui est la conséquence directe d'une pile qui descend.
Les déplacements de sp se font par multiples de quatre, parce qu'un sw et un lw exigent une adresse multiple de quatre. Un cadre de trois mots occupe douze octets, pas dix. Un cadre d'un seul octet utile occupe quand même quatre octets. Ce n'est pas du gaspillage : c'est le prix d'un accès mémoire qui fonctionne.
Ce que coûte un rééquilibrage oublié
Le programme suivant contient une faute qui ne déclenche aucune erreur, et c'est justement ce qui la rend intéressante. Un premier fragment empile une valeur qu'il compte retrouver plus tard. Un second fragment empile deux mots et n'en dépile qu'un. Le premier reprend ensuite la main.
programme
- 0x0addi sp, sp, -4
- 0x4sw t3, 0(sp)
- 0x8addi sp, sp, -4
- 0xcsw t0, 0(sp)
- 0x10addi sp, sp, -4
- 0x14sw t1, 0(sp)
- 0x18lw t2, 0(sp)
- 0x1caddi sp, sp, 4
- 0x20lw t4, 0(sp)
- 0x24addi sp, sp, 4
registres
| sp | 0x2000 | |
| t0 | 100 | |
| t1 | 200 | |
| t2 | 0 | |
| t3 | 7 | |
| t4 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le mot 7 est toujours en mémoire, intact, à l'adresse 0x1ffc. Personne ne l'a effacé. Simplement, sp ne le désigne plus : il pointe une case plus bas, celle du 100 abandonné par le fragment fautif, et le chargement final ramène donc 100. Aucune instruction n'a été refusée, aucun message n'a été affiché, et le programme se termine normalement avec une valeur fausse.
La seconde conséquence est plus insidieuse encore. À la fin, sp vaut 0x1ffc et non 0x2000 : quatre octets de pile sont perdus pour toujours. Un fragment qui dérive ainsi de quatre octets à chaque exécution et qui tourne dans une boucle fait descendre la pile sans fin, jusqu'à ce qu'elle rejoigne les données.
Deux façons de casser la pile
La première est signalée. Déplacer sp d'une quantité qui n'est pas multiple de quatre suffit à rendre le rangement suivant impossible.
programme
- 0x0addi sp, sp, -2
- 0x4sw t0, 0(sp)
registres
| sp | 0x2000 | |
| t0 | 5 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
L'instruction fautive n'est pas celle qui échoue. Le sw est correct ; c'est le addi d'avant qui a mis sp dans un état où plus aucun rangement de mot ne peut réussir. C'est une situation générale en assembleur, et la raison d'être du bouton qui recule : l'endroit où un programme s'arrête est rarement l'endroit où il s'est trompé.
La seconde façon n'est signalée par rien du tout. Il suffit que la pile descende assez pour atteindre les données.
programme
- 0x0addi sp, sp, -4
- 0x4sw t0, 0(sp)
- 0x8addi sp, sp, -4
- 0xcsw t1, 0(sp)
registres
| sp | 0x1010 | |
| t0 | 111 | |
| t1 | 222 |
mémoire
| 0x1000 | mesures | 10 |
| 0x1004 | 20 | |
| 0x1008 | 30 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
La ligne pile: 0x1010 rapproche artificiellement les deux occupants, parce qu'il faudrait un millier d'empilements pour provoquer la même chose avec le réglage habituel. Le mécanisme, lui, est exactement celui d'un programme réel : le second empilement écrit à l'adresse 0x1008, qui est le troisième mot du tableau mesures. La mesure 30 devient 222. Le tableau n'a pas été déclaré trop court, le programme n'a pas dépassé un indice, et pourtant sa troisième valeur est fausse.
La mémoire n'est qu'un tableau d'octets, et aucune instruction ne demande la permission d'écrire. Sur une machine réelle, le système d'exploitation pose au bas de la pile une zone interdite d'accès, et le programme qui la touche est arrêté net : c'est l'erreur de segmentation. Cette protection attrape la pile qui descend d'un coup, pas les cas où les deux zones se recouvrent progressivement. Un débordement de pile détecté est une chance ; un débordement silencieux est la règle.
Un travail complet : renverser une suite
La pile ne sert pas qu'à ranger des valeurs en attente. Sa règle du dernier arrivé, premier sorti est un outil de calcul à elle seule. Le programme suivant lit un tableau de quatre valeurs, empile chacune, puis les ressort une à une dans un second tableau : l'ordre se trouve inversé sans qu'aucune instruction n'ait eu à s'en occuper.
programme
- 0x0la t0, suitelui t0, 1
- 0x4addi zero, zero, 0
- 0x8li t1, 0addi t1, zero, 0
- 0xcli t2, 4addi t2, zero, 4
- 0x10beq t1, t2, preparer
- 0x14slli t3, t1, 2
- 0x18add t4, t0, t3
- 0x1clw t5, 0(t4)
- 0x20addi sp, sp, -4
- 0x24sw t5, 0(sp)
- 0x28addi t1, t1, 1
- 0x2cj empilerjal zero, empiler
- 0x30la t0, renverselui t0, 1
- 0x34addi t0, t0, 16
- 0x38li t1, 0addi t1, zero, 0
- 0x3cli t2, 4addi t2, zero, 4
- 0x40beq t1, t2, fin
- 0x44lw t5, 0(sp)
- 0x48addi sp, sp, 4
- 0x4cslli t3, t1, 2
- 0x50add t4, t0, t3
- 0x54sw t5, 0(t4)
- 0x58addi t1, t1, 1
- 0x5cj viderjal zero, vider
registres
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 | |
| t4 | 0 | |
| t5 | 0 |
mémoire
| 0x1000 | suite | 3 |
| 0x1004 | 1 | |
| 0x1008 | 4 | |
| 0x100c | 1 | |
| 0x1010 | renverse | 0 |
| 0x1014 | 0 | |
| 0x1018 | 0 | |
| 0x101c | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le parcours du tableau reprend le calcul d'adresse du chapitre 7 : slli t3, t1, 2 multiplie l'indice par quatre, l'addition à l'adresse de base donne l'adresse de l'élément. La nouveauté tient en quatre instructions, deux pour empiler et deux pour dépiler, et c'est tout ce qu'il faut pour renverser une suite de longueur quelconque.
Au dernier pas, sp est revenu à 0x2000. Les deux boucles empilent et dépilent le même nombre de mots, la pile est donc rendue dans l'état où elle a été trouvée. C'est la propriété que le chapitre suivant va exiger de chaque fonction.
Un dernier exercice de lecture
Le programme suivant échange le contenu de deux registres en passant par la pile, sans faire appel à un troisième registre. Prévoir, avant de lancer la figure, l'adresse touchée par chacun des deux rangements, puis les valeurs de t0 et de t1 à la fin.
programme
- 0x0addi sp, sp, -8
- 0x4sw t0, 4(sp)
- 0x8sw t1, 0(sp)
- 0xclw t0, 0(sp)
- 0x10lw t1, 4(sp)
- 0x14addi sp, sp, 8
registres
| sp | 0x2000 | |
| t0 | 1 | |
| t1 | 2 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
La réponse
Le cadre de huit octets s'ouvre à 0x1ff8, donc 4(sp) désigne 0x1ffc et 0(sp) désigne 0x1ff8. La valeur 1 part en 0x1ffc, la valeur 2 en 0x1ff8. Les deux chargements croisent les décalages : t0 reprend ce qui se trouve en 0(sp), soit 2, et t1 ce qui se trouve en 4(sp), soit 1. L'échange n'a demandé aucun registre supplémentaire, seulement quatre accès mémoire, et sp retrouve 0x2000 à la dernière instruction. Une pile ne sert donc pas seulement à mettre de côté : les décalages en font une zone de travail à part entière.
1.Pourquoi la pile part-elle du haut de la mémoire et descend-elle ?
2.Quel couple d'instructions empile le contenu de t0 ?
3.Après trois empilements d'un mot chacun depuis 0x2000, où se trouve le mot empilé en premier ?
4.Un fragment de code empile deux mots et n'en dépile qu'un. Que se passe-t-il ?
5.Que provoque addi sp, sp, -2 suivi d'un sw ?
6.Un programme écrit sur la pile jusqu'à atteindre la zone des données. Que signale la machine ?
La méthode
- Compter les mots nécessaires avant d'écrire la première instruction, puis réserver le cadre d'un seul
addi sp, sp, -4n, plutôt que de déplacerspune fois par valeur. - Écrire le rééquilibrage en même temps que la réservation : taper la ligne
addi sp, sp, +4nimmédiatement après avoir tapé la ligne négative, et remplir le corps entre les deux. - Ranger par décalages constants à partir de
sp, le décalage 0 pour la case la plus basse, et noter en commentaire ce que contient chaque décalage du cadre. - Vérifier l'alignement : tout déplacement de
spest un multiple de quatre, sans exception. - Relever
spau début et à la fin d'un fragment dans la figure : les deux valeurs doivent être identiques. Un écart signale un rééquilibrage manquant, même quand le résultat affiché semble correct. - Chercher la faute en amont de l'arrêt : reculer d'un pas depuis l'instruction qui échoue, car c'est presque toujours celle qui a placé
spou une adresse dans un état impossible.
Synthèse
- La pile existe parce que les registres sont trente-deux et qu'un programme a besoin de retenir plus de valeurs que cela, pour une durée courte, sans leur donner de nom ni d'adresse fixe.
- Les données montent depuis 0x1000, la pile descend depuis 0x2000. Faire partir les deux occupants des deux bouts de la mémoire évite de fixer une frontière à l'assemblage, et laisse l'usage réel décider du partage.
- Empiler, c'est réserver puis écrire :
addi sp, sp, -4puissw. Dépiler, c'est lire puis rendre :lwpuisaddi sp, sp, 4. Les deux ordres sont imposés, pour qu'aucun mot ne soit jamais écrit ou lu en dehors de la zone réservée. - La figure affiche la pile case par case, du sommet courant au sommet de départ, avec une marque sur la case que
spdésigne. Le dernier mot empilé occupe l'adresse la plus basse et se lit à0(sp). - Un cadre se réserve d'un seul mouvement,
addi sp, sp, -12pour trois mots, et se remplit par décalages, le décalage 0 désignant la case la plus basse. - Un rééquilibrage oublié ne déclenche aucune erreur :
spreste décalé, le code suivant lit la case du voisin, et la pile perd quelques octets à chaque passage. - Rien ne surveille la frontière entre la pile et les données. Une pile qui descend trop loin écrase les données en silence, et un déplacement de
spqui n'est pas multiple de quatre rend impossible le rangement suivant.
La pile est maintenant un outil. Appeler en fait le cœur du langage machine : une fonction appelée doit savoir où revenir, et cette adresse de retour se range, comme le reste, sur la pile.