Pour fournir un exemple concret de la manière dont un compilateur gère la pile et comment accéder aux valeurs de la pile, nous pouvons examiner les représentations visuelles, ainsi que le code généré par GCC
dans un environnement Linux avec i386 comme architecture cible.
1. empiler des cadres
Comme vous le savez, la pile est un emplacement de l’espace adresse d’un processus en cours utilisé par des fonctions ou procédures , en ce sens que l’espace est alloué sur la pile pour les variables déclarées localement, ainsi que les arguments passés à la fonction ( l'espace pour les variables déclarées en dehors de toute fonction (c'est-à-dire les variables globales) est alloué dans une région différente de la mémoire virtuelle). L'espace alloué pour toutes les données d'une fonction est appelé un cadre de pile . Voici une représentation visuelle de plusieurs cadres de pile (de Computer Systems: Perspective du programmeur ):
2. Gestion des cadres de pile et emplacement variable
Pour que les valeurs écrites dans la pile dans un cadre de pile particulier soient gérées par le compilateur et lues par le programme, il doit exister une méthode permettant de calculer les positions de ces valeurs et d'extraire leur adresse de mémoire. Les registres de la CPU dénommés le pointeur de pile et le pointeur de base facilitent cette tâche.
Le pointeur de base, ebp
par convention, contient l'adresse de la mémoire du bas, ou base, de la pile. Les positions de toutes les valeurs dans le cadre de pile peuvent être calculées en utilisant l'adresse du pointeur de base comme référence. Ceci est illustré dans l'image ci-dessus: %ebp + 4
est l'adresse de la mémoire stockée dans le pointeur de base plus 4, par exemple.
3. Code généré par le compilateur
Mais ce que je ne comprends pas, c'est comment une application lit ensuite les variables sur la pile. Si je déclare et assigne x sous forme d’entier, disons x = 3, le stockage est réservé sur la pile et sa valeur 3 est stockée. là, puis dans la même fonction, je déclare et assigne y comme, disons 4, et ensuite que j'utilise ensuite x dans une autre expression, (disons z = 5 + x) comment le programme peut-il lire x afin d'évaluer z quand il est en dessous de y sur la pile?
Utilisons un exemple de programme écrit en C pour voir comment cela fonctionne:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Examinons le texte d'assemblage produit par GCC pour ce texte source C (je l'ai un peu nettoyé pour des raisons de clarté):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
Nous observons que les variables x, y et z sont situées aux adresses %ebp - 12
, %ebp -8
et %ebp - 4
respectivement. En d'autres termes, les emplacements des variables dans le cadre de pile main()
sont calculés en utilisant l'adresse de mémoire enregistrée dans le registre de la CPU %ebp
.
4. Les données en mémoire au-delà du pointeur de pile sont hors de portée
Il me manque clairement quelque chose. Est-ce que l'emplacement sur la pile ne concerne que la durée de vie / la portée de la variable, et que toute la pile est effectivement accessible au programme tout le temps? Si tel est le cas, cela signifie-t-il qu'un autre index contient uniquement les adresses des variables de la pile afin de permettre l'extraction des valeurs? Mais ensuite, j'ai pensé que le but de la pile était que les valeurs étaient stockées au même endroit que l'adresse de la variable.
La pile est une région de la mémoire virtuelle, dont l'utilisation est gérée par le compilateur. Le compilateur génère du code de sorte que les valeurs situées au-delà du pointeur de la pile (les valeurs situées au-dessus du sommet de la pile) ne soient jamais référencées. Lorsqu'une fonction est appelée, la position du pointeur de la pile change pour créer un espace sur la pile réputé ne pas être "hors limites", pour ainsi dire.
Lorsque les fonctions sont appelées et retournées, le pointeur de pile est décrémenté et incrémenté. Les données écrites dans la pile ne disparaissent pas une fois hors de portée, mais le compilateur ne génère pas d'instructions référençant ces données car il ne dispose d'aucun moyen pour calculer les adresses de ces données à l'aide de %ebp
ou %esp
.
5. Résumé
Le code pouvant être exécuté directement par la CPU est généré par le compilateur. Le compilateur gère la pile, les cadres de pile pour les fonctions et les registres de la CPU. Une stratégie utilisée par GCC pour suivre les emplacements des variables dans des cadres de pile dans du code destiné à être exécuté sur une architecture i386 consiste à utiliser l'adresse mémoire dans le pointeur de base du cadre de pile %ebp
, comme référence et à écrire les valeurs des variables dans les emplacements des cadres de pile. à des décalages à l'adresse en %ebp
.