Un cadre de stack est-il réellement poussé sur la stack lorsqu’une fonction est appelée?

La façon dont on m’enseigne depuis un certain temps est que, lorsque j’exécute un programme, la première chose qui va immédiatement sur la stack est un frame de stack pour la méthode principale. Et si j’appelle une fonction appelée foo () depuis main, un cadre de stack représentant la taille des variables locales (objects automatiques) et les parameters sont également placés dans la stack.

Cependant, j’ai rencontré quelques choses qui contredisent cela. Et j’espère que quelqu’un pourra dissiper ma confusion ou expliquer pourquoi il n’y a pas vraiment de contradictions.

Première contradiction:

Dans le livre “Le langage de programmation C ++”, 3e édition, de Bjarne Stroustrup, il est écrit à la page 244, “Un object nommé automatiquement est créé chaque fois que sa déclaration est rencontrée lors de l’exécution du programme.” Si cela n’est pas assez clair, il est écrit à la page suivante: “Le constructeur d’une variable locale est exécuté à chaque fois que le thread de contrôle passe par la déclaration de la variable locale.”

Cela signifie-t-il que la mémoire totale d’une trame de stack n’est pas allouée en une fois, mais plutôt bloc par bloc lorsque les déclarations de variables sont rencontrées? En outre, cela signifie-t-il qu’un cadre de stack peut ne pas avoir la même taille à chaque fois si une déclaration de variable n’a pas été rencontrée en raison d’une instruction if?

Deuxième contradiction:

J’ai fait un peu de codage en assembleur (ARM pour être précis), et mon cours a été enseigné de telle sorte que lorsqu’une fonction était appelée, nous utilisions immédiatement les registres et ne poussions jamais aucune des variables locales de la fonction actuelle sur le emstackr à moins que l’algorithme ne soit pas réalisable avec le nombre limité de registres. Et même alors, nous n’avions poussé que les variables restantes.

Est-ce que cela signifie que lorsqu’une fonction est appelée, un cadre de stack peut ne pas être créé du tout? Cela implique-t-il également que la taille d’un cadre de stack peut varier en raison de l’utilisation de registres?

Concernant votre première question:

La création de l’object n’a rien à voir avec l’allocation des données elles-mêmes. Pour être plus précis: le fait que l’object ait son espace réservé sur la stack n’implique en rien le moment d’appeler son constructeur.

Cela signifie-t-il que la mémoire totale d’une trame de stack n’est pas allouée en une fois, mais plutôt bloc par bloc lorsque les déclarations de variable sont rencontrées?

Cette question est vraiment spécifique au compilateur. Un pointeur de stack est juste un pointeur, la manière dont il est utilisé par le binary revient au compilateur. En fait, certains compilateurs peuvent réserver l’intégralité de l’enregistrement d’activation, d’autres petit à petit, d’autres de manière dynamic, en fonction de l’invocation spécifique, etc. Ceci est même étroitement associé à une optimisation afin que le compilateur puisse arranger les choses de la manière qu’il pense être mieux.

Est-ce que cela signifie que lorsqu’une fonction est appelée, un cadre de stack peut ne pas être créé du tout? Cela implique-t-il également que la taille d’un cadre de stack peut varier en raison de l’utilisation de registres?

Encore une fois, il n’y a pas de réponse ssortingcte ici. Généralement, les compilateurs utilisent des algorithmes d’ allocation de registres capables d’allouer des registres de manière à minimiser les variables «renversées» (sur la stack). Bien sûr, si vous écrivez manuellement en assembleur, vous pouvez décider d’atsortingbuer des registres spécifiques à des variables spécifiques dans votre programme, simplement parce que vous savez, par leur contenu, comment vous voulez le faire fonctionner.

Un compilateur ne peut pas le deviner, mais il peut voir quand une variable commence à être utilisée ou n’est plus nécessaire et organise les choses de manière à minimiser les access à la mémoire (donc la taille de la stack). Par exemple, il pourrait mettre en œuvre une politique selon laquelle certains registres devraient être sauvegardés par l’appelé, d’autres par l’appelé et assignés, etc.

  1. Construire un object C ++ a très peu à voir avec l’acquisition de mémoire pour cet object. En fait, il serait plus juste de dire “réserver de la mémoire”, car en général, les ordinateurs ne disposent pas de petites équipes de constructeurs de mémoire vive qui entrent en action chaque fois que vous demandez un nouvel object. La mémoire est plus ou moins permanente (bien que nous puissions discuter de la VM). Bien entendu, le compilateur doit faire en sorte que son programme n’utilise une plage de mémoire particulière que pour une chose à la fois. Cela peut (et est probablement le cas) l’obliger à réserver une plage de mémoire avant l’existence de l’object et à éviter de l’utiliser pour d’autres objects jusqu’à un certain temps après la disparition de l’object. Par souci d’efficacité, le compilateur peut (même dans le cas d’objects à durée de stockage dynamic) optimiser les réservations en réservant plusieurs blocs de mémoire à la fois, s’il en sait qu’il en aura besoin. En tout état de cause, lorsque C ++ parle de “construction d’un object”, cela signifie simplement: prendre une plage de mémoire avec un contenu indéfini et faire le nécessaire pour créer la représentation de l’object (et de tout autre élément de l’état du monde). est impliqué par la création de l’object, qui peut ne pas être limité à un bloc particulier de mémoire.)

  2. Il n’y a aucune exigence pour que les frameworks de stack existent. Il n’y a aucune exigence pour qu’une stack existe. Tout dépend du compilateur. La plupart des compilateurs génèrent du code qui utilise une stack, bien sûr, et les bons compilateurs détermineront s’il est possible d’abréger, voire d’omettre, un cadre de stack. Donc, oui, la taille des frameworks peut varier.

Vous avez absolument raison, un cadre de stack n’est pas nécessaire. Les frameworks de stack sont une solution rapide et complexe au problème de la gestion de l’espace local, plus facile à déboguer qu’à gérer les modifications du pointeur de stack au cours de l’exécution de la fonction. Si vous avez besoin de la stack dans la fonction, il est plus facile d’ajuster le pointeur de la stack à l’entrée et de le restaurer au retour.

Ce n’est pas non plus en noir et blanc, les compilateurs sont des programmes comme n’importe quel autre programme, et si vous ne le savez pas déjà, vous vous rendrez compte que, compte tenu du nombre de programmeurs, vous obtiendrez plusieurs solutions au même problème. Même si le nombre de programmeurs est suffisant, une personne peut choisir de résoudre le problème encore et encore jusqu’à ce qu’elle soit satisfaite et / ou pour quelque raison que ce soit peut choisir de publier les différentes versions. L’utilisation de la stack est très courante pour les variables locales. C’est vraiment ce que vous faites, mais cela ne signifie pas que vous devez utiliser un cadre de stack créé à l’entrée et restauré au retour.

Et comme vous l’avez appris dans vos classes, il est très facile de voir à travers des expériences (comstackr des fonctions simples, avec différents niveaux d’optimisation, d’optimisation nulle à d’autres) que gcc n’utilisera la stack que s’il le faut. Nous parlons de arm là où la convention d’appel normale est basée sur un registre (rien n’indique que les auteurs du compilateur doivent suivre cette convention, il est possible d’utiliser une stack basée sur arm si un compilateur choisit de le faire). Les processeurs où la convention normale est basée sur la stack puisque le code traite déjà avec la stack, il peut choisir d’utiliser un cadre de stack. Il est probable que, dans ces cas, la convention basée sur la stack soit utilisée car le processeur manque de registres à usage général et repose davantage sur la stack que d’autres processeurs disposant de plus de registres, ce qui signifie que le processeur a probablement besoin de la stack souvent non seulement pour la convention d’appel, la plus grande partie du stockage local.