Comment fonctionne exactement de la pile des appels de travail?
Je vais essayer d'obtenir une compréhension plus profonde de la façon dont les opérations de bas niveau de la programmation des langues de travail, et en particulier la façon dont ils interagissent avec le système d'exploitation et PROCESSEUR. J'ai probablement lu toute les réponses dans chaque pile/tas de sujet ici sur Pile Dépassement de capacité, et ils sont tous brillants. Mais il y a encore une chose que je n'ai pas bien compris encore.
Considérer cette fonction dans le pseudo-code qui tend à être valide Rouille code 😉
fn foo() {
let a = 1;
let b = 2;
let c = 3;
let d = 4;
//line X
doSomething(a, b);
doAnotherThing(c, d);
}
C'est la façon dont je me charge de la pile, de manière à ressembler à la ligne X:
Stack
a +-------------+
| 1 |
b +-------------+
| 2 |
c +-------------+
| 3 |
d +-------------+
| 4 |
+-------------+
Maintenant, tout ce que j'ai lu sur la façon dont la pile fonctionne, c'est qu'il obéit strictement LIFO règles (last in, first out). Tout comme une pile de type de données dans .NET, Java ou tout autre langage de programmation.
Mais si c'est le cas, alors ce qui se passe après la ligne X? Parce que, évidemment, la prochaine chose que nous avons besoin est de travailler avec a
et b
, mais cela signifierait que le système d'exploitation et PROCESSEUR (?) a sortir des d
et c
premier à revenir à a
et b
. Mais alors qu'il allait tirer dans le pied, car il a besoin c
et d
dans la ligne suivante.
Alors, je me demande ce que exactement qui se passe en coulisses?
Une autre question connexe. Envisager de nous passer une référence à l'une des autres fonctions comme ceci:
fn foo() {
let a = 1;
let b = 2;
let c = 3;
let d = 4;
//line X
doSomething(&a, &b);
doAnotherThing(c, d);
}
De la façon dont je comprends les choses, cela signifie que les paramètres de doSomething
sont essentiellement pointant vers la même adresse mémoire comme a
et b
dans foo
. Mais là encore, cela signifie qu'il n'y a pas de pop haut de la pile jusqu'à ce que nous arrivons à a
et b
passe.
Ces deux cas, me font penser que je n'ai pas bien saisi comment exactement la pile fonctionne et comment il se conforme strictement à l' LIFO règles.
- Dernier entré, premier sorti uniquement les questions pour la réservation de l'espace sur la pile. Vous pouvez toujours accéder à toutes les variables qui est au moins sur votre cadre de pile (déclarée à l'intérieur de la fonction), même si c'est sous beaucoup d'autres variables
- En d'autres termes,
LIFO
signifie que vous pouvez ajouter ou supprimer des éléments qu'à la fin de la pile, et vous pouvez toujours lire/modifier n'importe quel élément. - Pourquoi ne pas vous démonter une fonction simple, après compilation avec -O0 et de regarder les instructions générées? C'est joli, bien, instructif ;-). Vous verrez que le code fait bon usage de la R de la partie de la mémoire vive; elle accède à une adresse directement à volonté. Vous pouvez penser à un nom de variable comme un décalage d'un registre d'adresse (le pointeur de pile). Comme les autres ont dit, la pile est juste de ce PRINCIPE à l'égard de l'empilement (bon pour la récursivité, etc.). Ce n'est pas ce PRINCIPE à l'égard de l'accès. L'accès est totalement aléatoire.
- Vous pouvez faire votre propre pile de la structure de données à l'aide d'un tableau, et juste le stockage de l'index de l'élément supérieur, en augmentant lorsque vous appuyez sur, le décrémenter lorsque vous pop. Si vous avez fait cela, si vous souhaitez toujours être en mesure d'accéder à tout élément individuel dans le tableau, à tout moment, sans pousser ou à éclater, comme vous pouvez toujours avec des tableaux. Environ la même chose se passe ici.
- Bonne explication.
- Merci à tous pour les commentaires. Vous m'avez aidé à obtenir une bien meilleure compréhension de ce bas niveau des choses. Vous rock!
- Autre remarque: en C au moins, le compilateur n'est pas obligé de mettre des variables sur la pile à tous. L'optimiseur peut les garder dans les registres. Il peut également les placer sur la pile dans un ordre différent. Aussi, jetez un oeil à la "Suite" de la langue et de sa pile de la programmation orientée.
- Un seul bloc de pile est un la plupart du temps non structuré zone de mémoire sous le seul contrôle du compilateur. Il n'y a pas de règles, que le compilateur doit obéir à l'intérieur d'une seule image. Il peut violer "dernier entré, premier sorti" et légalement dessin à main levée sur des octets comme il lui plaît. La structure de la pile a provient surtout de l'exigence que de nombreuses langues différentes doivent interagir et de s'appeler les uns les autres (à l'aide d'une convention d'appel).
- Afin de fournir un point de vue historique: lorsque la pile a été d'abord inventé en effet, il était utilisé uniquement dans un dernier entré, premier sorti de la manière que vous avez poussé des données de la pile à enregistrer tout ce que vous avez quelque chose d'autre, a déclenché lorsque vous ont été faites. Alors, dans ces jours, comment avez-vous utiliser la pile pour stocker les variables locales? Généralement, vous n'avez pas! Les variables locales vécu dans la mémoire statique, et oui, cela signifiait que la plupart des procédures n'étaient pas ré-entrant. Et l'empilement des images ont été inventés, et il n'y a allégresse générale. 🙂
- La norme ne mentionne même pas le mot "pile". Une zone contiguë de la pile comme celui en cours de discussion est certainement la façon la plus commune pour mettre en œuvre C du modèle de mémoire, mais ce n'est pas le seul. Il y a eu des compilateurs C que allouer de la mémoire pour les appels de fonction dans le tas, ou quelque chose comme ça, de sorte que la mémoire de commande de cadres pour les appels successifs est entièrement arbitraire.
- Vous n'avez pas de pile variables, mais les images 🙂
- Notez également que, pour votre exemple, certaines conventions d'appel n'a même pas de l'utilisation de la pile d'appel de stocker ou de transmettre ces valeurs, mais qui va simplement utiliser les registres.
- Généralement, un pointeur de pile commence à la haute adresse de la pile et pousse à la baisse des adresses comme les choses sont poussés sur la pile. Les choses sont poussés sur la pile dans l'ordre de leur occurrence, de sorte que le haut de l'entrée sur le stack de " 1 "à l'entrée suivante de la pile(à une adresse inférieure) sera" 2 " et ainsi de suite.
- Fondamentalement, le nom de pile/tas est regrettable. Ils ont peu de ressemblance avec la pile et le tas dans des structures de données de la terminologie, afin de l'appelant de la même est très déroutant.
- eh bien, il a été rédigé de manière à dessiner un scénario spécifique. Comme je l'ai dit, j'avais lu à peu près tout ce que j'ai trouvé sur le sujet, mais ne pouvais pas trouver exactement ces bits manquants. Les gens a donné beaucoup de réponses ici, donc je pense que tout le monde devrait être heureux 🙂
- IMO le meilleur moyen de comprendre correctement la pile et les conventions d'appel est d'écrire un peu de montage.
- Mai , je sais qu'avez-vous voulu dire par "Mais là encore, cela signifie qu'il n'y a pas de pop-up de la pile jusqu'à ce que nous arrivons à a et b qui se passe." Je ne suis pas tout à fait le faire.
- À partir d'un lien seule réponse: ce blog (cryptroix.com/2016/10/16/journey-to-the-stack) est assez bonne. Il utilise Python sous forme de pseudo-code pour la simple asm fonctions (comme vous pourriez trouver dans le compilateur C de sortie), et l'hypothèse d'un pile-args convention d'appel (moderne conventions d'appel de passer des arguments dans les registres)
Vous devez vous connecter pour publier un commentaire.
La pile d'appel pourrait également être appelé à cadre de pile.
Les choses qui sont empilés après ce principe ne sont pas les variables locales, mais l'ensemble de la pile des cadres ("calls") de ces fonctions étant appelé. Les variables locales sont poussés et sauté avec ces images dans la soi-disant prologue de fonction et épilogue, respectivement.
À l'intérieur de l'armature de l'ordre des variables est complètement non spécifié; les Compilateurs "réorganiser" les positions des variables locales à l'intérieur d'un cadre de façon appropriée afin d'optimiser leur alignement, afin que le processeur peut les récupérer le plus rapidement possible. Le point crucial est que le décalage des variables par rapport à une adresse fixe est constante tout au long de la durée de vie du châssis - de sorte qu'il suffit de prendre un point d'ancrage adresse, disons, l'adresse de l'image elle-même, et de travailler avec des décalages de cette adresse pour les variables. Un tel ancrage adresse est en fait contenue dans la soi-disant base ou pointeur de cadre qui est stocké dans le registre EBP. Les décalages, d'autre part, sont clairement connus au moment de la compilation et ne sont donc codés en dur dans le code machine.
Ce graphique du Wikipédia montre ce que l'typiques de la pile d'appel est structuré comme1:
Ajouter le décalage d'une variable nous voulons accéder à l'adresse contenue dans le pointeur de cadre et de nous obtenir l'adresse d'une variable. Si peu de temps, a déclaré, le code vient d'y accéder directement via la constante de compilation des décalages à partir de la base de pointeur; C'est simple arithmétique des pointeurs.
Exemple
gcc.godbolt.org nous donne
.. pour
main
. J'ai divisé le code en trois sous-sections.Le prologue de fonction se compose des trois premières opérations:
Puis
cin
est déplacé dans le registre EDI2 etget
est appelé; La valeur de retour est dans EAX.So far So good. Maintenant, la chose la plus intéressante qui se passe:
La faible octet de poids fort de l'EAX, désigné par le 8-bit registre AL, est pris et stockés dans l'octet de droite après le pointeur de la base: C'est
-1(%rbp)
, le décalage de la base de pointeur est-1
. Cet octet est notre variablec
. L'offset est négatif parce que la pile grandit vers le bas sur x86. La prochaine opération magasinsc
dans EAX: EAX est déplacé à l'ESI,cout
est déplacé à l'EDI et puis l'insertion de l'opérateur est appelé aveccout
etc
être les arguments.Enfin,
main
est stocké dans EAX: 0. C'est parce que de l'implicitereturn
déclaration.Vous pouvez également voir
xorl rax rax
au lieu demovl
.leave
est l'abréviation de cet épilogue et, implicitement,Après cette opération et
ret
ont été effectuées, l'image a bien été sauté, bien que l'appelant a encore pour nettoyer les arguments que nous sommes à l'aide de la cdecl convention d'appel. D'autres conventions, par exemple stdcall, exiger que le destinataire de l'appel à ranger, par exemple, par l'adoption de la quantité d'octets deret
.Pointeur De Cadre Omission
Il est également possible de ne pas utiliser les crédits issus de la base/pointeur de l'image, mais à partir du pointeur de pile (ESB) à la place. Cela rend le EBP-registre contiennent le pointeur de cadre valeur disponible pour l'usage arbitraire - mais il peut faire débogage impossible sur certaines machines, et sera implicitement désactivée pour certaines fonctions. Il est particulièrement utile lors de la compilation pour processeurs avec seulement quelques registres, y compris les x86.
Cette optimisation est connu comme FPO (pointeur de cadre omission) et fixé par
-fomit-frame-pointer
dans GCC et-Oy
dans Clang; à noter qu'il est implicitement déclenchée par chaque niveau d'optimisation > 0 si et seulement si le débogage est encore possible, car elle n'a pas tous les frais en dehors de cela.Pour de plus amples informations, voir ici et ici.
1 Comme l'a souligné dans les commentaires, le pointeur de l'image est probablement censé pointer vers l'adresse après l'adresse de retour.
2 Notez que les registres qui commencent par R sont la 64 bits homologues de ceux qui commencent par E. EAX désigne les quatre bas-octets de RAX. J'ai utilisé les noms des registres 32 bits pour plus de clarté.
rbp
de faire d'autres travaux.-fomit-frame-pointer
être, par défaut, est que la gestion des exceptions peut encore être fait. c'est à dire unwindind la pile comme un débogueur de trace. La quantité d'espace de pile chaque réserve de fonction sur la pile est stockée dans le.eh_frame_hdr
section, ou quelque chose comme ça (voir l'ABI doc pour plus de détails).-fomit-frame-pointer
a été la valeur par défaut pour les 32 bits pour quelques versions de gcc maintenant, parce que les choses sont faites de la même manière.ret
instruction ne fonctionne que lorsque le pointeur de pile est en pointant directement à l'adresse de retour. Retour valeurs ne sont pas stockées sur la pile à toutes les fonctions valeurs de retour dansrax
, ourdx:rax
). Le sale pile c'est un bon point, cependant: il est utilisé pour obtenir la fonction args, avant l'adresse de retour. Oh, je crois que j'ai tout compris: la pile est en croissance haut, qui est à l'opposé de la façon dont Intel manuels de présenter les choses. en outre, montrant le "haut de la pile" en haut est faux dans ce cas.rdx:rax
. (Tant que les membres sont tous des entiers). Il existe des alternatives, comme le laissant dans une temporaire sur la pile. Caché pointeur pouvez enregistrer une copie dans le cas où l'appelant peut passer un pointeur vers la finale de la dest.malloc
ed, ou quoi que ce soit), non pas parce que la convention d'appel en ont besoin.if (g==4)
puisint d = 3
etg
- je prendre d'entrée à l'aide descanf
après que j'définir une autre variableint h = 5
. Maintenant , comment le compilateur donnerd = 3
de l'espace dans la pile. Comment fonctionne le décalage fait parce que, sig
n'est pas4
, alors il n'y aurait pas de mémoire pourd
dans la pile et tout simplement décalage devrait être donnée àh
et sig == 4
puis décalage est d'abord pourg
et puis pourh
. Comment compilateur de le faire au moment de la compilation , il ne connaît pas notre entrée pourg
.En bref:
Il n'y a pas besoin de la pop les arguments. Les arguments transmis par l'appelant
foo
à la fonctiondoSomething
et les variables locales dansdoSomething
peuvent tous être référencé comme un décalage à partir du base de pointeur.Donc,
En détail:
La règle est que chaque appel de fonction des résultats lors d'une création d'un cadre de pile (avec le minimum étant de l'adresse de retour). Donc, si
funcA
appelsfuncB
etfuncB
appelsfuncC
, trois pile cadres sont mis en place l'une sur l'autre. Lorsqu'une fonction retourne une valeur, son image devient invalide. Un bon comportement de la fonction agit uniquement sur son propre cadre de la pile et n'empiète pas sur l'autre. En d'autres termes, le POPing est effectuée à la trame de pile sur le dessus (quand, de retour de la fonction).La pile dans votre question, c'est l'installation par l'appelant
foo
. LorsquedoSomething
etdoAnotherThing
sont appelés, puis ils en place leur propre pile. La figure peut vous aider à comprendre ceci:Noter que, pour accéder à la arguments, le corps de la fonction aura pour traverser vers le bas (plus d'adresses) à partir de l'emplacement où l'adresse de retour est stockée, et pour accéder aux variables locales, le corps de la fonction aura pour parcourir la pile (adresse inférieure) par rapport à l'emplacement de l'adresse de retour est stockée. En fait, typique généré par le compilateur de code de la fonction qui va faire exactement cela. Le compilateur consacre un registre appelé EBP pour ce (Base pointer). Un autre nom pour le même pointeur de l'image. Le compilateur généralement, que la première chose pour le corps de la fonction, pousse l'actuelle EBP valeur sur la pile et des jeux de l'EBP pour le courant de l'ESP. Cela signifie que, une fois cela fait, dans n'importe quelle partie du code de la fonction, l'argument 1 est EBP+8 distance (4 octets pour chaque appelant EBP et l'adresse de retour), l'argument 2 est EBP+12(décimal) à l'abri, les variables locales sont EBP-4n loin.
Prendre un coup d'oeil à la suite de code C pour la formation de cadre de pile de la fonction:
Lors de l'appel de l'appelant, il
le code suivant sera généré
et le code assembleur de la fonction sera (mis en place par l'appelant avant de revenir)
Références:
EBP
etESP
?Comme d'autres l'ont noté, il n'y a pas besoin de la pop paramètres, jusqu'à ce qu'ils passent hors de portée.
Je vais coller un exemple de "Pointeurs et de la Mémoire" par Nick Parlante.
Je pense que la situation est un peu plus simple que vous ne l'avait imaginé.
Voici le code:
Les points dans le temps
T1, T2, etc
. sont marqués dansle code et l'état de la mémoire à l'époque, est montré dans le dessin:
Différents processeurs et d'utilisation des langues différentes pile de dessins. Deux motifs traditionnels sur le 8x86 et 68000 sont appelés la convention d'appel Pascal et la convention d'appel C; chaque convention est traité de la même façon dans les deux processeurs, sauf pour les noms de registres. Chacun utilise deux registres pour gérer la pile et les variables associées, appelé le pointeur de pile (SP ou A7) et le pointeur de l'image (BP ou A6).
Lors de l'appel de sous-programme à l'aide soit de la convention, tous les paramètres sont poussés sur la pile avant l'appel de la routine. La routine du code de pousse alors la valeur courante du pointeur de l'image sur la pile, des copies de la valeur courante du pointeur de pile pour le pointeur de cadre, et en soustrait le pointeur de pile, le nombre d'octets utilisés par les variables locales [le cas échéant]. Une fois que c'est fait, même si des données supplémentaires sont poussés sur la pile, toutes les variables locales seront stockées dans des variables avec une constante négative de déplacement du pointeur de pile, et tous les paramètres qui ont été poussés sur la pile par l'utilisateur peuvent être consultées à une constante positive déplacement du pointeur de l'image.
La différence entre les deux conventions réside dans la façon de gérer une sortie de la sous-routine. Dans le C de la convention, le retour à la fonction copie le cadre pointeur vers le pointeur de pile [restaurer la valeur qu'ils avaient juste après l'ancien pointeur de cadre a été poussé], pop l'ancien cadre la valeur du pointeur, et effectue un retour. Tous les paramètres que l'appelant avait poussé sur la pile avant l'appel d'y rester. Dans la convention Pascal, après avoir mis l'ancien pointeur de l'image, le processeur saute à la fonction de l'adresse de retour, ajoute le pointeur de pile, le nombre d'octets de paramètres poussé par l'appelant, et puis s'en va à l'sauté sur l'adresse de retour. Sur l'original 68000 il était nécessaire de recourir à un 3-séquence d'instruction pour retirer de l'appelant paramètres; le 8x86 et tous les processeurs 680x0 après l'original inclus un "ret N" [ou 680x0 équivalent] instruction qui leur permettrait d'ajouter à la N du pointeur de pile lors de l'exécution d'un retour.
La convention Pascal a l'avantage de sauvegarder un peu de code sur l'appelant côté, puisque l'appelant n'a pas à mettre à jour le pointeur de pile, après un appel de fonction. Il nécessite, toutefois, que la fonction appelée savoir exactement combien d'octets valeur des paramètres de l'appelant va mettre sur la pile. À défaut de pousser la bon nombre de paramètres sur la pile avant l'appel d'une fonction qui utilise la convention Pascal est presque garanti pour provoquer un plantage. Cela est compensé toutefois par le fait qu'un peu plus de code à l'intérieur de chaque méthode appelée sera enregistrer le code dans les lieux où la méthode est appelée. Pour cette raison, la plupart des Macintosh d'origine de boîte à outils des routines utilisées la convention d'appel Pascal.
La convention d'appel C a l'avantage de permettre des routines d'accepter un nombre variable de paramètres, et d'être robuste, même si une routine n'utilise pas tous les paramètres qui sont passés (l'appelant ne sais combien d'octets valeur des paramètres qu'il a poussé, et sera donc en mesure de les nettoyer). De plus, il n'est pas nécessaire d'effectuer de la pile de nettoyage après chaque appel de fonction. Si un des appels de routine quatre fonctions dans l'ordre, chacun de qui a utilisé les quatre octets de la valeur de paramètres, il peut, au lieu de l'utilisation d'un
ADD SP,4
après chaque appel, utilisez unADD SP,16
après le dernier appel de nettoyage les paramètres de tous les quatre appels.Aujourd'hui l'décrit les conventions d'appel sont considérés comme quelque peu désuet. Puisque les compilateurs suis de plus en plus efficace au registre de l'utilisation, il est courant d'avoir des méthodes d'accepter quelques paramètres dans les registres plutôt que d'exiger que tous les paramètres soient poussés sur la pile; si une méthode peut utiliser les registres à tenir tous les paramètres et les variables locales, il n'y a pas besoin d'utiliser un pointeur de l'image, et donc pas besoin d'enregistrer et de restaurer l'ancienne. Pourtant, il est parfois nécessaire d'utiliser les anciennes conventions d'appel lors de l'appel de bibliothèques qui était lié à leur utilisation.
(g==4)
puisint d = 3
etg
- je prendre d'entrée à l'aide descanf
après que j'définir une autre variableint h = 5
. Maintenant , comment le compilateur donnerd = 3
de l'espace dans la pile. Comment fonctionne le décalage fait parce que, sig
n'est pas4
, alors il n'y aurait pas de mémoire pour d dans la pile et tout simplement décalage devrait être donnée àh
et sig == 4
puis décalage est d'abord pour g et puis pourh
. Comment compilateur de le faire au moment de la compilation , il ne connaît pas notre entrée pourg
Il y a déjà quelques de bonnes réponses ici. Toutefois, si vous êtes toujours intéressés sur le PRINCIPE comportement de la pile, pensez-y comme une pile d'images, plutôt que d'une pile de variables. Ce que je veux suggérer, c'est que, si une fonction peut accéder à des variables qui ne sont pas sur le haut de la pile, c'est toujours d'exploitation sur le élément en haut de la pile: un seul bloc de pile.
Bien sûr, il y a des exceptions à cela. Les variables locales à l'ensemble de la chaîne d'appels sont toujours attribuées et disponible. Mais ils ne sont pas accessibles directement. Au lieu de cela, ils sont passés par référence (ou par pointeur, ce qui est vraiment différent du point de vue sémantique). Dans ce cas, une variable locale d'un cadre de pile beaucoup plus bas peut être consulté. Mais même dans ce cas, la fonction en cours d'exécution est toujours d'exploitation uniquement sur ses propres données locales. C'est l'accès à une référence stockée dans son propre cadre de pile, qui peut être une référence à quelque chose sur le tas, dans la mémoire statique, ou plus bas dans la pile.
C'est la partie de la pile de l'abstraction qui rend les fonctions appelables dans n'importe quel ordre, et permet la récursivité. Le sommet de la pile est le seul objet qui est directement accessible par le code. Tout le reste est accessible indirectement (par l'intermédiaire d'un pointeur qui vit dans le top frame de pile).
Il peut être instructif d'examiner l'ensemble de votre petit programme, en particulier si vous compilez sans optimisation. Je pense que vous verrez que tous les accès à la mémoire dans votre fonction passe par un décalage du cadre de pile pointeur, ce qui est la façon dont le code de la fonction qui sera écrit par le compilateur. Dans le cas d'un passage par référence, vous verrez indirecte d'accès à la mémoire d'instructions par l'intermédiaire d'un pointeur est stocké à un certain décalage à partir de la pile pointeur de l'image.
La pile d'appel n'est pas en fait une pile de structure de données. En coulisses, les ordinateurs que nous utilisons sont des implémentations de l'accès aléatoire architecture de la machine. Donc, a et b peuvent être directement accessibles.
Derrière les coulisses, la machine n':
http://en.wikipedia.org/wiki/Random-access_machine