La pile grandit-elle vers le haut ou vers le bas?

89

J'ai ce morceau de code en c:

int q = 10;
int s = 5;
int a[3];

printf("Address of a: %d\n",    (int)a);
printf("Address of a[1]: %d\n", (int)&a[1]);
printf("Address of a[2]: %d\n", (int)&a[2]);
printf("Address of q: %d\n",    (int)&q);
printf("Address of s: %d\n",    (int)&s);

La sortie est:

Address of a: 2293584
Address of a[1]: 2293588
Address of a[2]: 2293592
Address of q: 2293612
Address of s: 2293608

Donc, je vois que de aà a[2], les adresses mémoire augmentent de 4 octets chacune. Mais de qà s, les adresses mémoire diminuent de 4 octets.

Je me demande 2 choses:

  1. La pile augmente-t-elle ou diminue-t-elle? (Cela me ressemble aux deux dans ce cas)
  2. Que se passe-t-il entre les adresses mémoire a[2]et q? Pourquoi y a-t-il une grande différence de mémoire là-bas? (20 octets).

Remarque: ce n'est pas une question de devoirs. Je suis curieux de savoir comment fonctionne la pile. Merci pour toute aide.

Spikatrix
la source
L'ordre est arbitraire. L'écart est probablement de stocker un résultat intermédiaire tel que & q ou & s - regardez le démontage et voyez par vous-même.
Tom Leys
Je suis d'accord, lisez le code d'assemblage. Si vous posez ce genre de questions, il est temps d'apprendre à le lire.
Per Johansson
Une version d'assemblage plus simple à répondre: stackoverflow.com/questions/664744/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

74

Le comportement de la pile (grandir ou diminuer) dépend de l'interface binaire de l'application (ABI) et de l'organisation de la pile d'appels (aka enregistrement d'activation).

Tout au long de sa vie, un programme est tenu de communiquer avec d'autres programmes comme OS. ABI détermine comment un programme peut communiquer avec un autre programme.

La pile pour différentes architectures peut croître dans les deux sens, mais pour une architecture, elle sera cohérente. Veuillez vérifier ce lien wiki. Mais la croissance de la pile est décidée par l'ABI de cette architecture.

Par exemple, si vous prenez l'ABI MIPS, la pile d'appels est définie comme ci-dessous.

Considérons que la fonction 'fn1' appelle 'fn2'. Maintenant, le cadre de pile tel que vu par 'fn2' est le suivant:

direction of     |                                 |
  growth of      +---------------------------------+ 
   stack         | Parameters passed by fn1(caller)|
from higher addr.|                                 |
to lower addr.   | Direction of growth is opposite |
      |          |   to direction of stack growth  |
      |          +---------------------------------+ <-- SP on entry to fn2
      |          | Return address from fn2(callee) | 
      V          +---------------------------------+ 
                 | Callee saved registers being    | 
                 |   used in the callee function   | 
                 +---------------------------------+
                 | Local variables of fn2          |
                 |(Direction of growth of frame is |
                 | same as direction of growth of  |
                 |            stack)               |
                 +---------------------------------+ 
                 | Arguments to functions called   |
                 | by fn2                          |
                 +---------------------------------+ <- Current SP after stack 
                                                        frame is allocated

Vous pouvez maintenant voir que la pile se développe vers le bas. Ainsi, si les variables sont allouées à la trame locale de la fonction, les adresses de la variable croissent en fait vers le bas. Le compilateur peut décider de l'ordre des variables pour l'allocation de mémoire. (Dans votre cas, ce peut être «q» ou «s» qui est la première mémoire de pile allouée. Mais, généralement, le compilateur fait l'allocation de mémoire de pile selon l'ordre de la déclaration des variables).

Mais dans le cas des tableaux, l'allocation n'a qu'un seul pointeur et la mémoire à allouer sera en fait pointée par un seul pointeur. La mémoire doit être contiguë pour un tableau. Ainsi, bien que la pile augmente vers le bas, pour les tableaux, la pile grandit.

Ganesh Gopalasubramanian
la source
5
De plus, si vous souhaitez vérifier si la pile se développe vers le haut ou vers le bas. Déclarez une variable locale dans la fonction principale. Imprimez l'adresse de la variable. Appelez une autre fonction depuis main. Déclarez une variable locale dans la fonction. Imprimez son adresse. Sur la base des adresses imprimées, nous pouvons dire que la pile augmente ou diminue.
Ganesh Gopalasubramanian
merci Ganesh, j'ai une petite question: dans la figure que tu as dessinée, dans le troisième bloc, est-ce que tu voulais dire "registre enregistré de calleR utilisé dans CALLER" parce que lorsque f1 appelle f2, nous devons stocker l'adresse f1 (qui est l'adresse de retour pour les registres f2) et f1 (calleR) et non les registres f2 (callee). Droite?
CSawy
44

C'est en fait deux questions. L'un concerne la manière dont la pile se développe lorsqu'une fonction en appelle une autre (lorsqu'une nouvelle image est allouée), et l'autre la disposition des variables dans le cadre d'une fonction particulière.

Ni l'un ni l'autre n'est spécifié par la norme C, mais les réponses sont un peu différentes:

  • De quelle façon la pile augmente-t-elle quand une nouvelle image est allouée - si la fonction f () appelle la fonction g (), le fpointeur de cadre sera-t-il supérieur ou inférieur au gpointeur de cadre de? Cela peut aller dans les deux sens - cela dépend du compilateur et de l'architecture particuliers (recherchez "convention d'appel"), mais c'est toujours cohérent au sein d'une plate-forme donnée (à quelques exceptions près, voir les commentaires). Vers le bas est plus courant; c'est le cas des SPU x86, PowerPC, MIPS, SPARC, EE et Cell.
  • Comment les variables locales d'une fonction sont-elles disposées dans son cadre de pile? Ceci est non spécifié et complètement imprévisible; le compilateur est libre d'organiser ses variables locales mais il souhaite obtenir le résultat le plus efficace.
Crashworks
la source
7
"il est toujours cohérent au sein d'une plate-forme donnée" - non garanti. J'ai vu une plate-forme sans mémoire virtuelle, où la pile était étendue de manière dynamique. Les nouveaux blocs de pile étaient en fait malléables, ce qui signifie que vous descendriez d'un bloc de pile pendant un certain temps, puis soudainement "de côté" vers un bloc différent. «Sideways» pourrait signifier une adresse plus ou moins grande, entièrement due à la chance du tirage au sort.
Steve Jessop
2
Pour plus de détails sur le point 2 - un compilateur peut être en mesure de décider qu'une variable n'a jamais besoin d'être en mémoire (en la gardant dans un registre pour la durée de vie de la variable), et / ou si la durée de vie de deux ou plusieurs variables ne le fait pas ' t se chevauchent, le compilateur peut décider d'utiliser la même mémoire pour plus d'une variable.
Michael Burr
2
Je pense que S / 390 (IBM zSeries) a un ABI où les cadres d'appel sont liés au lieu de croître sur une pile.
éphémère
2
Correct sur S / 390. Un appel est "BALR", registre de branche et de lien. La valeur de retour est placée dans un registre plutôt que poussée sur une pile. La fonction de retour est une branche vers le contenu de ce registre. Au fur et à mesure que la pile s'approfondit, de l'espace est alloué dans le tas et ils sont enchaînés. C'est là que l'équivalent MVS de "/ bin / true" tire son nom: "IEFBR14". La première version avait une seule instruction: "BR 14", qui se ramifiait au contenu du registre 14 qui contenait l'adresse de retour.
janm
1
Et certains compilateurs sur processeurs PIC effectuent une analyse complète du programme et allouent des emplacements fixes pour les variables automatiques de chaque fonction; la pile réelle est minuscule et n'est pas accessible à partir du logiciel; ce n'est que pour les adresses de retour.
janm
13

La direction dans laquelle les piles se développent est spécifique à l'architecture. Cela dit, je crois comprendre que seules quelques architectures matérielles ont des piles qui grandissent.

La direction dans laquelle une pile se développe est indépendante de la disposition d'un objet individuel. Ainsi, même si la pile peut grossir, les tableaux ne le seront pas (c'est-à-dire que & array [n] sera toujours <& array [n + 1]);

R Samuel Klatchko
la source
4

Il n'y a rien dans la norme qui impose la façon dont les choses sont organisées sur la pile. En fait, vous pouvez créer un compilateur conforme qui ne stocke pas du tout les éléments de tableau dans les éléments contigus de la pile, à condition qu'il ait l'intelligence pour toujours faire correctement l'arithmétique des éléments de tableau (de sorte qu'il sache, par exemple, qu'un 1 était 1K de [0] et pourrait s'ajuster pour cela).

La raison pour laquelle vous pouvez obtenir des résultats différents est que, bien que la pile puisse s'agrandir pour y ajouter des "objets", le tableau est un seul "objet" et il peut avoir des éléments de tableau ascendants dans l'ordre opposé. Mais il n'est pas sûr de se fier à ce comportement car la direction peut changer et les variables peuvent être permutées pour diverses raisons, notamment, mais sans s'y limiter:

  • optimisation.
  • alignement.
  • les caprices de la personne la partie gestion de pile du compilateur.

Voir ici mon excellent traité sur la direction de la pile :-)

En réponse à vos questions spécifiques:

  1. La pile augmente-t-elle ou diminue-t-elle?
    Cela n'a pas d'importance du tout (en termes de standard) mais, comme vous l'avez demandé, cela peut augmenter ou diminuer en mémoire, selon l'implémentation.
  2. Que se passe-t-il entre une adresse mémoire [2] et q? Pourquoi y a-t-il une grande différence de mémoire là-bas? (20 octets)?
    Cela n'a pas d'importance du tout (en termes de norme). Voir ci-dessus pour les raisons possibles.
paxdiablo
la source
Je vous ai vu dire que la plupart des architectures de CPU adoptent la méthode de «croissance vers le bas», savez-vous s'il y a un avantage à le faire?
Baiyan Huang
Aucune idée, vraiment. Il est possible que quelqu'un pense que le code monte à partir de 0, donc la pile devrait descendre à partir de highmem, afin de minimiser la possibilité d'intersection. Mais certains processeurs commencent spécifiquement à exécuter du code à des emplacements différents de zéro, ce qui peut ne pas être le cas. Comme pour la plupart des choses, peut-être que cela a été fait de cette façon simplement parce que c'était la première façon dont quelqu'un pensait le faire :-)
paxdiablo
@lzprgmr: Il y a quelques légers avantages à avoir certains types d'allocation de tas exécutés dans l'ordre croissant, et il a toujours été courant que la pile et le tas se trouvent aux extrémités opposées d'un espace d'adressage commun. À condition que l'utilisation combinée statique + tas + pile ne dépasse pas la mémoire disponible, on n'a pas à se soucier de la quantité exacte de mémoire de pile utilisée par un programme.
supercat
3

Sur un x86, l '"allocation" mémoire d'un frame de pile consiste simplement à soustraire le nombre d'octets nécessaire au pointeur de pile (je crois que d'autres architectures sont similaires). En ce sens, je suppose que la pile grossit "vers le bas", en ce que les adresses deviennent progressivement plus petites au fur et à mesure que vous appelez plus profondément dans la pile (mais j'imagine toujours que la mémoire commence par 0 en haut à gauche et augmente les adresses lorsque vous vous déplacez à droite et envelopper, donc à mon image mentale la pile grandit ...). L'ordre des variables déclarées peut ne pas avoir d'incidence sur leurs adresses - je crois que la norme permet au compilateur de les réorganiser, tant que cela ne provoque pas d'effets secondaires (quelqu'un, veuillez me corriger si je me trompe) . Ils'

L'espace autour du tableau peut être une sorte de rembourrage, mais c'est mystérieux pour moi.

rmeador
la source
1
En fait, je sais que le compilateur peut les réorganiser, car il est également libre de ne pas les attribuer du tout. Il peut simplement les mettre dans des registres et n'utiliser aucun espace de pile.
rmeador
Il ne peut pas les mettre dans les registres si vous référencez leurs adresses.
florin
bon point, n'avait pas envisagé cela. mais cela suffit quand même comme preuve que le compilateur peut les réorganiser, car nous savons qu'il peut le faire au moins de temps en temps :)
rmeador
1

Tout d'abord, ses 8 octets d'espace inutilisé en mémoire (ce n'est pas 12, rappelez-vous que la pile augmente vers le bas, donc l'espace qui n'est pas alloué est de 604 à 597). et pourquoi?. Parce que chaque type de données prend de l'espace en mémoire à partir de l'adresse divisible par sa taille. Dans notre cas, un tableau de 3 entiers prend 12 octets d'espace mémoire et 604 n'est pas divisible par 12. Il laisse donc des espaces vides jusqu'à ce qu'il rencontre une adresse mémoire divisible par 12, c'est 596.

Ainsi, l'espace mémoire alloué au tableau est de 596 à 584. Mais comme l'allocation du tableau se poursuit, le premier élément du tableau commence à partir de l'adresse 584 et non à partir de 596.

Prateek Khurana
la source
1

Le compilateur est libre d'allouer des variables locales (auto) à n'importe quel endroit du cadre de la pile locale, vous ne pouvez pas déduire de manière fiable la direction de croissance de la pile uniquement à partir de cela. Vous pouvez déduire le sens de croissance de la pile en comparant les adresses des cadres de pile imbriqués, c'est-à-dire en comparant l'adresse d'une variable locale à l'intérieur du cadre de pile d'une fonction par rapport à son appel:

#include <stdio.h>
int f(int *x)
{
  int a;
  return x == NULL ? f(&a) : &a - x;
}

int main(void)
{
  printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up");
  return 0;
}
matja
la source
5
Je suis presque sûr que c'est un comportement indéfini pour soustraire des pointeurs vers différents objets de pile - les pointeurs qui ne font pas partie du même objet ne sont pas comparables. Évidemment, il ne plantera pas sur une architecture "normale".
Steve Jessop
@SteveJessop Y a-t-il un moyen de résoudre ce problème pour obtenir la direction de la pile par programme?
xxks-kkk
@ xxks-kkk: en principe non, car une implémentation C n'est pas obligée d'avoir une "direction de pile". Par exemple, cela ne violerait pas la norme d'avoir une convention d'appel dans laquelle un bloc de pile est alloué à l'avance, puis une routine d'allocation de mémoire interne pseudo-aléatoire est utilisée pour sauter à l'intérieur. En pratique, cela fonctionne comme le décrit Matja.
Steve Jessop
0

Je ne pense pas que ce soit déterministe comme ça. Le tableau a semble «croître» car cette mémoire doit être allouée de manière contiguë. Cependant, étant donné que q et s ne sont pas du tout liés l'un à l'autre, le compilateur colle simplement chacun d'eux dans un emplacement de mémoire libre arbitraire dans la pile, probablement ceux qui correspondent le mieux à une taille entière.

Ce qui s'est passé entre a [2] et q est que l'espace autour de l'emplacement de q n'était pas assez grand (c'est-à-dire qu'il n'était pas plus grand que 12 octets) pour allouer un tableau de 3 entiers.

Javanix
la source
si oui, pourquoi q, s, a n'ont pas de mémoire contingente? (Ex: Adresse du q: 2293612 Adresse du s: 2293608 Adresse d'un: 2293604)
Je vois un "écart" entre s et a
Parce que s et a n'ont pas été alloués ensemble, les seuls pointeurs qui doivent être contigus sont ceux du tableau. L'autre mémoire peut être allouée n'importe où.
javanix
0

Ma pile semble s'étendre vers les adresses numérotées plus faibles.

Cela peut être différent sur un autre ordinateur, ou même sur mon propre ordinateur si j'utilise un autre appel de compilateur. ... ou le compilateur muigt choisit de ne pas utiliser du tout une pile (tout en ligne (fonctions et variables si je n'en ai pas pris l'adresse)).

$ cat stack.c
#include <stdio.h>

int stack(int x) {
  printf("level %d: x is at %p\n", x, (void*)&x);
  if (x == 0) return 0;
  return stack(x - 1);
}

int main(void) {
  stack(4);
  return 0;
}
$ / usr / bin / gcc -Wall -Wextra -std = c89 -pedantic stack.c
$ ./a.out
niveau 4: x est à 0x7fff7781190c
niveau 3: x est à 0x7fff778118ec
niveau 2: x est à 0x7fff778118cc
niveau 1: x est à 0x7fff778118ac
niveau 0: x est à 0x7fff7781188c
pmg
la source
0

La pile s'agrandit (sur x86). Cependant, la pile est allouée en un seul bloc lorsque la fonction se charge, et vous n'avez aucune garantie de l'ordre des éléments dans la pile.

Dans ce cas, il a alloué de l'espace pour deux entiers et un tableau trois int sur la pile. Il a également alloué 12 octets supplémentaires après le tableau, donc cela ressemble à ceci:

a [12 octets]
remplissage (?) [12 octets]
s [4 octets]
q [4 octets]

Pour une raison quelconque, votre compilateur a décidé qu'il devait allouer 32 octets pour cette fonction, et peut-être plus. C'est opaque pour vous en tant que programmeur C, vous ne savez pas pourquoi.

Si vous voulez savoir pourquoi, compilez le code en langage assembleur, je crois que c'est -S sur gcc et / S sur le compilateur C de MS. Si vous regardez les instructions d'ouverture de cette fonction, vous verrez l'ancien pointeur de pile en cours d'enregistrement, puis 32 (ou autre chose!) En être soustrait. À partir de là, vous pouvez voir comment le code accède à ce bloc de mémoire de 32 octets et comprendre ce que fait votre compilateur. À la fin de la fonction, vous pouvez voir le pointeur de pile en cours de restauration.

Aric TenEyck
la source
0

Cela dépend de votre système d'exploitation et de votre compilateur.

David R Tribble
la source
Je ne sais pas pourquoi ma réponse a été rejetée. Cela dépend vraiment de votre système d'exploitation et de votre compilateur. Sur certains systèmes, la pile se développe vers le bas, mais sur d'autres, elle se développe vers le haut. Et sur certains systèmes, il n'y a pas de véritable pile de trames push-down, mais plutôt simulée avec une zone réservée de mémoire ou un jeu de registres.
David R Tribble
3
Probablement parce que les affirmations à une phrase ne sont pas de bonnes réponses.
Courses de légèreté en orbite le
0

La pile grossit. Donc f (g (h ())), la pile allouée pour h commencera à une adresse inférieure, puis g et g seront inférieurs à f. Mais les variables au sein de la pile doivent suivre la spécification C,

http://c0x.coding-guidelines.com/6.5.8.html

1206 Si les objets pointés sont des membres du même objet agrégé, les pointeurs vers les membres de la structure déclarés plus tard comparent plus que les pointeurs aux membres déclarés plus tôt dans la structure, et les pointeurs vers les éléments du tableau avec des valeurs d'indice plus grandes comparent plus que les pointeurs aux éléments du même tableau avec des valeurs d'indice inférieures.

& a [0] <& a [1], doit toujours être vrai, quelle que soit la façon dont 'a' est alloué

user1187902
la source
Sur la plupart des machines, la pile se développe vers le bas - sauf pour celles où elle pousse vers le haut.
Jonathan Leffler
0

croît vers le bas et cela est dû au petit standard d'ordre des octets endian en ce qui concerne l'ensemble de données en mémoire.

Une façon de voir les choses est que la pile grandit vers le haut si vous regardez la mémoire de 0 à partir du haut et max à partir du bas.

La raison de la croissance de la pile vers le bas est de pouvoir déréférencer du point de vue de la pile ou du pointeur de base.

N'oubliez pas que le déréférencement de tout type augmente de l'adresse la plus basse à la plus élevée. Étant donné que la pile croît vers le bas (adresse la plus élevée à la plus basse), cela vous permet de traiter la pile comme une mémoire dynamique.

C'est l'une des raisons pour lesquelles tant de langages de programmation et de script utilisent une machine virtuelle basée sur la pile plutôt que sur un registre.

Nergal
la source
The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.Très beau raisonnement
user3405291
0

Cela dépend de l'architecture. Pour vérifier votre propre système, utilisez ce code de GeeksForGeeks :

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
kurdtpage
la source