Article de reference

Pile d'appels

En informatique , une pile d'appels est une structure de données qui stocke les informations relatives aux sous-programmes actifs et aux blocs de code en ligne d'un programme . ...

informatique , une pile d'appels est une structure de données qui stocke les informations relatives aux sous-programmes actifs et aux blocs de code en ligne d'un programme . Ce type de pile est également appelé pile d'exécution , pile de programme , pile de contrôle , pile d'exécution ou pile machine , et est souvent abrégé en « pile ». Bien que la gestion de la pile d'appels soit essentielle au bon fonctionnement de la plupart des logiciels , ses détails sont généralement masqués et automatisés dans les langages de programmation de haut niveau . De nombreux jeux d'instructions proposent des instructions spécifiques pour manipuler les piles.

La pile d'appels sert à plusieurs fins connexes, mais sa principale raison est de garder trace du point auquel chaque sous-programme actif doit renvoyer le contrôle une fois son exécution terminée. Un sous-programme est un sous-programme qui a été appelé, mais dont l'exécution n'est pas encore terminée ; après quoi, le contrôle doit être rendu au point d'appel. Ces appels de sous-programmes peuvent être imbriqués à n'importe quel niveau (récursifs dans un cas particulier), d'où la structure de pile. Par exemple, si un sous-programme DrawSquareappelle un autre sous-programme DrawLinedepuis quatre emplacements différents, DrawLineil doit savoir où retourner une fois son exécution terminée. Pour ce faire, l' adresse suivant l' instruction qui effectue le saut vers DrawLinel' adresse de retour est empilée au sommet de la pile d'appels lors de chaque appel.

pile , l'appelant d'une procédure, ou le prologue de cette procédure, empile l'adresse de retour sur la pile. La sous-routine appelée, une fois son exécution terminée, dépile l'adresse de retour et transfère le contrôle à cette adresse. De même, le prologue d'un bloc en ligne empile un cadre de pile et son épilogue le dépile. Si une sous-routine appelée appelle une autre sous-routine, elle empile une nouvelle adresse de retour sur la pile, et ainsi de suite, les informations s'empilant et se dépilant selon les besoins du programme. Si l'empilement consomme tout l'espace alloué à la pile d'appels, une erreur appelée dépassement de pile se produit, provoquant généralement le plantage du programme . L'ajout d'une entrée de bloc ou de sous-routine à la pile d'appels est parfois appelé « enroulement », et la suppression d'entrées, « déroulement ».

Il existe généralement une seule pile d'appels associée à un programme en cours d'exécution (ou plus précisément, à chaque tâche ou thread d'un processus ), bien que des piles supplémentaires puissent être créées pour la gestion des signaux ou le multitâche coopératif (comme avec ` setcontext` ). Puisqu'il n'y en a qu'une dans ce contexte important, on peut la désigner comme la pile (implicitement « de la tâche »). Cependant, en Forth, la pile de données ou la pile de paramètres est utilisée plus explicitement que la pile d'appels et est généralement appelée « pile » (voir ci-dessous).

Dans les langages de programmation de haut niveau , le fonctionnement précis de la pile d'appels est généralement masqué au programmeur. Ce dernier n'a accès qu'à un ensemble de fonctions, et non à la mémoire de la pile elle-même. C'est un exemple d' abstraction . En revanche, la plupart des langages assembleur exigent que le programmeur intervienne dans la manipulation de la pile. Les détails concrets de la pile dans un langage de programmation dépendent du compilateur , du système d'exploitation et du jeu d'instructions disponible .

Fonctions de la pile d'appels

conventions d'appel , telles que le stockage de l'adresse de retour avant le début de la sous-routine appelée ou à un autre emplacement fixe. L'un des avantages est que chaque tâche peut avoir sa propre pile, ce qui permet à la sous-routine d'être thread-safe , c'est-à-dire de pouvoir être active simultanément pour différentes tâches effectuant des opérations différentes. Un autre avantage est que, grâce à la réentrance , la récursivité est automatiquement prise en charge. Lorsqu'une fonction s'appelle elle-même de manière récursive, une adresse de retour doit être stockée pour chaque appel afin de pouvoir être utilisée ultérieurement pour revenir de l'appel. Les structures de pile offrent cette fonctionnalité automatiquement.

Selon le langage, le système d'exploitation et l'environnement machine, une pile d'appels peut servir à des fins supplémentaires, notamment :

stockage de données local
Une sous-routine a souvent besoin d'espace mémoire pour stocker les valeurs des variables locales , c'est-à-dire les variables connues uniquement au sein de la sous-routine active et qui perdent leur valeur après son exécution. Il est souvent pratique d'allouer cet espace en décalant simplement le sommet de la pile d'une position suffisante. Cette méthode est très rapide comparée à l'allocation dynamique de mémoire , qui utilise l' espace du tas . Notez que chaque activation d'une sous-routine dispose de son propre espace dans la pile pour les variables locales.
De même, un bloc en ligne peut allouer un cadre de pile pour les variables locales. Comme pour un cadre d'appel, les valeurs sont perdues à la sortie du bloc.
Passage de paramètres
Les sous-programmes nécessitent souvent que le code appelant leur fournisse des valeurs pour leurs paramètres , et il est fréquent que l'espace réservé à ces paramètres soit alloué dans la pile d'appels. Généralement, si le nombre de paramètres est faible, les registres du processeur sont utilisés pour transmettre leurs valeurs. En revanche, si leur nombre dépasse la capacité de cette méthode, de l'espace mémoire est nécessaire. La pile d'appels constitue un emplacement idéal pour ces paramètres, d'autant plus que chaque appel à un sous-programme, qui requiert des valeurs de paramètres différentes, bénéficie d'un espace distinct dans la pile d'appels. Dans les langages orientés objet tels que C++ , la liste des paramètres peut également inclure le thispointeur .
Pile d'évaluation
Les opérandes des opérations arithmétiques ou logiques sont généralement placés dans des registres et traités à ce niveau. Cependant, dans certains cas, les opérandes peuvent être empilés sur une profondeur arbitraire, ce qui implique l'utilisation d'un mécanisme autre que les registres (on parle alors de débordement de registres ). La pile de ces opérandes, semblable à celle d'une calculatrice RPN , est appelée pile d'évaluation et peut occuper de l'espace dans la pile d'appels.
Contexte de sous-routine englobant
Certains langages de programmation (par exemple, Pascal et Ada ) permettent la déclaration de sous-routines imbriquées , qui peuvent accéder au contexte de leurs routines englobantes, c'est-à-dire aux paramètres et variables locales de ces dernières. Cette imbrication statique peut se répéter (une fonction déclarée dans une autre fonction, elle-même déclarée dans une troisième fonction…). L'implémentation doit fournir un moyen pour qu'une fonction appelée à un niveau d'imbrication statique donné puisse référencer le cadre d'exécution englobant à chaque niveau d'imbrication. Cette référence est généralement implémentée par un pointeur vers le cadre d'exécution de la dernière instance activée de la fonction englobante, appelé « lien statique » ou « lien descendant », afin de le distinguer du « lien dynamique » qui référence l'appelant direct (qui n'est pas nécessairement la fonction parente statique).
Burroughs B6500 implémentait un tel affichage au niveau matériel, prenant en charge jusqu'à 32 niveaux d'imbrication statique.
Les entrées d'affichage indiquant les portées englobantes sont obtenues à partir du préfixe approprié de l'affichage de l'appelant. Une routine interne récursive crée des cadres d'appel distincts pour chaque invocation. Dans ce cas, tous les liens statiques de la routine interne pointent vers le même contexte de routine externe.
Contexte de bloc inclus
Dans certains langages, comme ALGOL 60 et PL/I , un bloc au sein d'une procédure peut posséder ses propres variables locales, allouées à l'entrée du bloc et libérées à sa sortie. De même, le bloc peut disposer de ses propres gestionnaires d'exceptions, désactivés à sa sortie.
Autre état de retour
Outre l'adresse de retour, dans certains environnements, d'autres états de la machine ou du logiciel peuvent devoir être restaurés lors du retour d'une sous-routine. Il peut s'agir, par exemple, du niveau de privilège, des informations de gestion des exceptions, des modes arithmétiques, etc. Le cas échéant, ces informations peuvent être stockées dans la pile d'appels, au même titre que l'adresse de retour.

Forth , par exemple, seules l'adresse de retour, les paramètres et index des boucles (comptés), et éventuellement les variables locales, sont généralement stockés sur la pile d'appels (appelée pile de retour dans cet environnement ). Toutefois, toute donnée peut y être temporairement placée grâce à un code de gestion spécifique de la pile de retour, à condition que les besoins des appels et des retours soient respectés. Les paramètres sont généralement stockés sur une pile de données ou pile de paramètres distincte , souvent appelée « pile » en Forth, même s'il existe une pile d'appels, car elle est généralement utilisée de manière plus explicite. Certaines distributions Forth possèdent également une troisième pile pour les paramètres à virgule flottante .

Disposition de la pile d'appels pour les piles à croissance ascendante après la DrawSquaresous-routine (représentée en bleu ) appelée DrawLine(représentée en vert ), qui est la routine en cours d'exécutiondépendantes de la machine et de l'ABI , contenant des informations sur l'état des sous-programmes. Chaque cadre de pile correspond à un appel de sous-programme qui ne s'est pas encore terminé par un retour. Le cadre de pile situé au sommet de la pile correspond au programme en cours d'exécution. Par exemple, si un sous-programme nommé `a` DrawLineest en cours d'exécution, ayant été appelé par un autre sous-programme DrawSquare, la partie supérieure de la pile d'appels peut être agencée comme illustré ci-contre.

Un tel diagramme peut être dessiné dans les deux sens, pourvu que l'emplacement de l'élément supérieur, et donc le sens de croissance de la pile, soit compris. Les architectures diffèrent quant à la direction de croissance des piles d'appels (vers les adresses supérieures ou inférieures) ; par convention, la logique du diagramme n'est donc pas tributaire de ce choix d'adressage.

Pendant son exécution, avec son cadre en haut de la pile, une routine peut accéder aux informations contenues dans son cadre selon ses besoins. Le cadre de la pile comprend généralement au moins les éléments suivants (dans l'ordre d'empilement) :

  • les arguments (valeurs des paramètres) transmis à la routine sur la pile d'appels (le cas échéant) ;
  • l'adresse de retour à l'appelant de la routine (par exemple, dans le DrawLinecadre de pile, une adresse dans DrawSquarele code de ), si celle-ci est enregistrée sur la pile d'appels plutôt que dans un registre ; et
  • espace pour les variables locales de la routine (le cas échéant).

les sous-routines imbriquées possèdent également un champ dans le cadre d'appel pointant vers le cadre de pile de la dernière activation de la procédure encapsulant au plus près l'appelé, c'est-à-dire la portée immédiate de ce dernier. Ce champ est appelé lien d'accès ou lien statique (car il conserve la trace de l'imbrication statique lors des appels dynamiques et récursifs) et permet à la routine (ainsi qu'à toute autre routine qu'elle pourrait invoquer) d'accéder aux données locales de ses routines encapsulantes à chaque niveau d'imbrication. Certaines architectures, compilateurs ou optimisations stockent un lien pour chaque niveau d'imbrication (et non seulement le niveau immédiatement englobant), afin que les routines profondément imbriquées accédant à des données superficielles n'aient pas à parcourir plusieurs liens ; cette stratégie est souvent appelée « affichage ».

Les liens d'accès peuvent être optimisés lorsqu'une fonction interne n'accède à aucune donnée locale (non constante) dans l'encapsulation, comme c'est le cas pour les fonctions pures communiquant uniquement par le biais d'arguments et de valeurs de retour, par exemple. Certains ordinateurs anciens, tels que l' Electrologica X8 et, un peu plus tard, les grands systèmes Burroughs , disposaient de « registres d'affichage » spéciaux pour prendre en charge les fonctions imbriquées, tandis que les compilateurs de la plupart des machines modernes (comme l'omniprésent x86) réservent simplement quelques mots sur la pile pour les pointeurs, selon les besoins.

endroit précis , car elles sont spécifiques à l'appel, puis empilées ou placées dans des registres, selon la convention d'appel utilisée . L'instruction d'appel proprement dite, telle que « branchement et liaison », est ensuite exécutée pour transférer le contrôle au code de la sous-routine cible.

Traitement des entrées de sous-routine

Dans la sous-routine appelée, le premier code exécuté est généralement appelé prologue de la sous-routine , car il effectue les opérations de nettoyage nécessaires avant le début du code des instructions de la routine.

Dans les architectures de jeu d'instructions où l'instruction appelant une sous-routine place l'adresse de retour dans un registre plutôt que de l'empiler, le prologue sauvegarde généralement cette adresse en empilant la valeur sur la pile d'appels. Toutefois, si la sous-routine appelée n'appelle aucune autre routine, la valeur peut rester dans le registre. De même, les valeurs du pointeur de pile et/ou du pointeur de cadre peuvent être empilées.

Si des pointeurs de cadre sont utilisés, le prologue initialise généralement le registre du pointeur de cadre à partir du pointeur de pile. L'espace sur la pile pour les variables locales peut ensuite être alloué en modifiant progressivement le pointeur de pile.

Le langage de programmation Forth permet l'enroulement explicite de la pile d'appels (appelée dans ce langage « pile de retour »).

Traitement des retours

Lorsqu'une sous-routine est prête à se terminer, elle exécute un épilogue qui annule les étapes du prologue. Cet épilogue restaure généralement les valeurs des registres sauvegardées (comme la valeur du pointeur de cadre) depuis la pile, dépile l'intégralité du cadre en modifiant la valeur du pointeur de pile, et enfin, effectue un branchement vers l'instruction à l'adresse de retour. Selon de nombreuses conventions d'appel, les éléments dépilés par l'épilogue incluent les valeurs des arguments d'origine ; dans ce cas, l'appelant n'a généralement aucune autre manipulation de la pile à effectuer. Cependant, selon certaines conventions d'appel, il incombe à l'appelant de retirer les arguments de la pile après le retour.

la gestion des exceptions . Dans ce cas, le cadre de pile d'une fonction contient une ou plusieurs entrées spécifiant les gestionnaires d'exceptions. Lorsqu'une exception est levée, la pile est déroulée jusqu'à ce qu'un gestionnaire soit trouvé, prêt à traiter (intercepter) le type de l'exception levée.

Certains langages possèdent d'autres structures de contrôle nécessitant un déroulement de pile. Pascal autorise l'instruction globale `goto` pour transférer le contrôle d'une fonction imbriquée vers une fonction externe précédemment appelée. Cette opération requiert le déroulement de la pile, en supprimant autant de cadres de pile que nécessaire pour restaurer le contexte approprié au transfert de contrôle vers l'instruction cible au sein de la fonction externe englobante. De même, C dispose des fonctions `goto` setjmpetlongjmp `goto` qui agissent comme des `goto` non locaux. Common Lisp permet de contrôler le comportement de la pile lors de son déroulement grâce à l' unwind-protectopérateur spécial `goto`.

Lors de l'exécution d'une continuation , la pile est (logiquement) déroulée puis rembobinée avec la pile de la continuation. Ce n'est pas la seule façon d'implémenter des continuations ; par exemple, en utilisant plusieurs piles explicites, l'exécution d'une continuation peut simplement activer sa pile et rembobiner une valeur à passer. Le langage de programmation Scheme permet d'exécuter des thunks arbitraires à des points spécifiques lors du « déroulement » ou du « rembobinage » de la pile de contrôle lorsqu'une continuation est invoquée.

Ruby et Smalltalk , pour implémenter des continuations de première classe. À titre d'exemple, le débogueur GNU (GDB) permet l'inspection interactive de la pile d'appels d'un programme C en cours d'exécution, mais mis en pause

L'échantillonnage régulier de la pile d'appels peut s'avérer utile pour profiler les performances des programmes car, si l'adresse d'une sous-routine apparaît plusieurs fois dans les données d'échantillonnage de la pile d'appels, elle est susceptible de constituer un goulot d'étranglement dans le code et doit être examinée afin de détecter d'éventuels problèmes de performance.

Sécurité

exploité par le biais de débordements de tampon de pile , qui sont le type de débordement de tampon le plus courant .

Une attaque de ce type consiste à remplir un tampon avec du code exécutable arbitraire, puis à provoquer un débordement de ce tampon ou d'un autre pour écraser une adresse de retour avec une valeur pointant directement vers le code exécutable. Ainsi, au retour de la fonction, l'ordinateur exécute ce code. Ce type d'attaque peut être bloqué par W^X , mais des attaques similaires peuvent réussir même avec la protection W^X activée, notamment l' attaque par retour à la libc ou les attaques issues de la programmation orientée retour . Diverses mesures d'atténuation ont été proposées, comme le stockage des tableaux dans un emplacement complètement distinct de la pile de retour, comme c'est le cas dans le langage de programmation Forth.