web-dev-qa-db-fra.com

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

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. Est-ce que la pile grandit ou descend? (Il me semble que les deux à la fois)
  2. Que se passe-t-il entre les adresses mémoire a[2] et q? Pourquoi il y a une grande différence de mémoire là-bas? (20 octets).

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

76
hdn

Le comportement de la pile (en croissance ou en baisse) dépend de l'interface ABI (Application Binary Interface) et de la manière dont la pile d'appels (ou enregistrement d'activation) est organisée. 

Tout au long de son existence, un programme est obligé de communiquer avec d'autres programmes tels que OS. ABI détermine comment un programme peut communiquer avec un autre programme. 

La pile de différentes architectures peut se développer de l'une ou l'autre manière, mais pour une architecture, elle sera cohérente. Veuillez vérifier this wiki link. 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 suit.

Considérons que la fonction 'fn1' appelle 'fn2'. Maintenant, le cadre de pile 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 deviennent réellement plus basses. Le compilateur peut décider de l'ordre des variables pour l'allocation de mémoire. (Dans votre cas, il peut s'agir de "q" ou de "s" qui est d'abord alloué à la mémoire de pile. Mais, en général, le compilateur empile l'allocation de mémoire selon l'ordre de déclaration des variables).

Mais dans le cas des tableaux, l'allocation n'a qu'un seul pointeur et la mémoire qui doit être allouée sera en réalité pointée par un seul pointeur. La mémoire doit être contiguë pour un tableau. Ainsi, si la pile croît vers le bas, elle grandit pour les tableaux.

C'est en fait deux questions. L'une concerne la manière la pile grandit lorsqu'une fonction en appelle une autre (lorsqu'une nouvelle image est allouée), et l'autre concerne la manière dont les variables sont présentées dans l'image d'une fonction particulière. 

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

  • De quelle manière la pile s'agrandit-elle lorsqu'un nouveau cadre est alloué - si la fonction f() appelle la fonction g (), le pointeur de cadre de f sera-t-il supérieur ou inférieur au pointeur de cadre de g? Ceci peut allez dans un sens ou l’autre - cela dépend du compilateur et de l’architecture particulières (recherchez "convention d’appel"), mais il est toujours cohérent sur une plate-forme donnée (à quelques exceptions bizarres, voir les commentaires). La baisse est plus commune; c'est le cas dans x86, PowerPC, MIPS, SPARC, EE et les SPU Cell.
  • Comment sont placées les variables locales d'une fonction 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.
41
Crashworks

La direction est que les piles se développent est spécifique à l'architecture. Cela dit, si j'ai bien compris, seules quelques rares architectures matérielles ont des piles grandissantes.

La direction dans laquelle une pile grandit est indépendante de la disposition d'un objet individuel. Ainsi, bien que la pile puisse s’agrandir, les tableaux ne le seront pas (par exemple, & array [n] sera toujours <& array [n + 1]);

13

Il n'y a rien dans la norme qui commande la manière dont les choses sont organisées sur la pile. En fait, vous pouvez créer un compilateur conforme qui ne stocke pas les éléments de tableau au niveau des éléments contigus de la pile, à condition qu'il ait la capacité de toujours effectuer correctement le calcul arithmétique des éléments de tableau (pour qu'il sache, par exemple, que a - 1 était à 1K de un [0] et pouvait 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 "objet" unique et peut comporter des éléments de tableau ascendants dans l’ordre inverse. Mais il n’est pas prudent de s’appuyer sur ce comportement car la direction peut changer et les variables peuvent être permutées pour diverses raisons, notamment les suivantes:

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

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

En réponse à vos questions spécifiques:

  1. Est-ce que la pile grandit ou descend?
    Cela n’a aucune importance (du point de vue de la norme), mais, comme vous l’avez demandé, cela peut grandir ou dans mémoire, en fonction de la mise en œuvre.
  2. Que se passe-t-il entre une [2] et q adresses de mémoire? Pourquoi il y a une grande différence de mémoire là-bas? (20 octets)?
    Cela n’a aucune importance (en termes de norme). Voir ci-dessus pour des raisons possibles.
4
paxdiablo

Sur un x86, "l'allocation" en mémoire d'un cadre de pile consiste simplement à soustraire le nombre d'octets nécessaire du pointeur de pile (je pense que d'autres architectures sont similaires). En ce sens, je suppose que la pile grandit "vers le bas", en ce sens que les adresses deviennent de plus en plus petites au fur et à mesure que vous appelez plus profondément dans la pile (mais j'imagine toujours que la mémoire commence avec 0 en haut à gauche et obtient des adresses plus grandes au fur et à mesure à droite et envelopper, donc dans mon image mentale de 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 (veuillez corriger quelqu'un si je me trompe) . Ils sont juste coincés quelque part dans cette lacune dans les adresses utilisées créées quand on soustrait le nombre d'octets au pointeur de la pile.

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

2
rmeador

Tout d’abord, ses 8 octets d’espace inutilisé en mémoire (ce n’est pas 12, rappelez-vous que la pile croît, donc l’espace qui n’est pas alloué va de 604 à 597). et pourquoi? . Parce que chaque type de données prend de la place 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, il est donc 596. 

Donc, l’espace mémoire alloué au tableau va de 596 à 584. Mais comme l’allocation de tableau est en cours, le premier élément du tableau commence donc à partir de 584 adresses et non à partir de 596.

1
Prateek Khurana

pousse vers le bas et cela est dû au standard d’ordre d’octets peu important en ce qui concerne l’ensemble des données en mémoire.

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

La raison pour laquelle la pile croît est de pouvoir déréférencer la perspective 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 haute. Étant donné que la pile grandit (adresse la plus élevée à la plus basse), vous pouvez traiter la pile comme une mémoire dynamique.

C'est l'une des raisons pour lesquelles un si grand nombre de langages de programmation et de script utilise une machine virtuelle basée sur une pile plutôt que sur une base de registres.

1
Nergal

Le compilateur est libre d'allouer des variables locales (auto) à n'importe quel endroit du cadre de pile local. Vous ne pouvez pas en déduire de manière fiable le sens 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.-à-d. En comparant l'adresse d'une variable locale dans le cadre de pile d'une fonction par rapport à sa valeur:

#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;
}
1
matja

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; 
} 
0
kurdtpage

Je ne pense pas que ce soit déterministe comme ça. Le tableau a semble "grandir" car cette mémoire devrait être allouée de manière contiguë. Cependant, comme q et s ne sont pas du tout liés l'un à l'autre, le compilateur place chacun d'eux dans un emplacement de mémoire libre arbitraire dans la pile, probablement ceux qui correspondent le mieux à la taille d'un entier.

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

0
javanix

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

0
David R Tribble

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

Cela peut être différent sur un autre ordinateur, ou même sur le mien, si j'utilise un autre appel du compilateur. ... ou le compilateur doit choisir de ne pas utiliser de pile (tout en ligne (fonctions et variables si je n'ai pas pris leur 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
level 4: x est à 0x7fff7781190c 
 niveau 3: x est à 0x7fff778118ec 
 niveau 2: x est à 0x7fff778118cc 
 niveau 1: x est à 0x7fff778118ac 
 niveau 0: x est à 0x7fff7781188c 
0
pmg

La pile grandit (sur x86). Cependant, la pile est allouée dans un bloc lorsque la fonction est chargée et vous ne pouvez garantir l'ordre dans lequel les éléments seront placés dans la pile.

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

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

Pour une raison quelconque, votre compilateur a décidé qu'il lui fallait affecter 32 octets à cette fonction, voire davantage. 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 Assembly, je pense 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 de sauvegarde, puis 32 (ou quelque chose d'autre!) 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.

0
Aric TenEyck

La pile croît. Donc, f (g (h ())), la pile allouée pour h commencera à une adresse inférieure à g et g sera inférieure à f. Mais les variables dans 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 sur les membres de la structure déclarés ultérieurement comparent davantage les pointeurs aux membres déclarés précédemment dans la structure, et les pointeurs sur des éléments de tableau avec des valeurs en indice plus grandes comparent les plus grands pointeurs sur des éléments du même élément. tableau avec des valeurs en indice inférieur.

& a [0] <& a [1], doit toujours être vrai, quel que soit le mode d'affectation de "a" 

0
user1187902