Quelle est la direction de la croissance de la pile dans la plupart des systèmes modernes?

102

Je prépare du matériel de formation en C et je souhaite que mes exemples correspondent au modèle de pile typique.

Dans quelle direction une pile C évolue-t-elle sous Linux, Windows, Mac OSX (PPC et x86), Solaris et les Unix les plus récents?

Uri
la source
3
Une version pourquoi à la baisse: stackoverflow.com/questions/2035568/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

147

La croissance de la pile ne dépend généralement pas du système d'exploitation lui-même, mais du processeur sur lequel il fonctionne. Solaris, par exemple, fonctionne sur x86 et SPARC. Mac OSX (comme vous l'avez mentionné) fonctionne sur PPC et x86. Linux fonctionne sur tout, de mon grand honkin 'System z au travail à une petite montre-bracelet chétive .

Si le CPU offre un choix quelconque, la convention ABI / appel utilisée par le système d'exploitation spécifie le choix que vous devez faire si vous voulez que votre code appelle le code de tout le monde.

Les processeurs et leur direction sont:

  • x86: vers le bas.
  • SPARC: sélectionnable. L'ABI standard utilise du duvet.
  • PPC: bas, je pense.
  • System z: dans une liste chaînée, je ne plaisante pas (mais toujours en panne, au moins pour zLinux).
  • ARM: sélectionnable, mais Thumb2 a des encodages compacts uniquement pour down (LDMIA = incrémentation après, STMDB = décrémentation avant).
  • 6502: vers le bas (mais seulement 256 octets).
  • RCA 1802A: comme vous le souhaitez, sous réserve de la mise en œuvre SCRT.
  • PDP11: vers le bas.
  • 8051: en haut.

Montrant mon âge sur ces derniers, la 1802 était la puce utilisée pour contrôler les premières navettes (détecter si les portes étaient ouvertes, je suppose, en fonction de la puissance de traitement qu'elle avait :-) et de mon deuxième ordinateur, le COMX-35 ( suivant mon ZX80 ).

Détails PDP11 glanés d' ici , 8051 détails d' ici .

L'architecture SPARC utilise un modèle de registre à fenêtre glissante. Les détails architecturaux visibles incluent également un tampon circulaire de fenêtres de registre qui sont valides et mises en cache en interne, avec des interruptions lorsque celles-ci dépassent / dépassent. Voir ici pour plus de détails. Comme l'explique le manuel SPARCv8 , les instructions SAVE et RESTORE sont comme les instructions ADD plus la rotation des registres-fenêtres. Utiliser une constante positive au lieu du négatif habituel donnerait une pile croissante.

La technique SCRT mentionnée ci-dessus en est une autre - le 1802 utilisait une partie ou ses seize registres 16 bits pour SCRT (technique d'appel et de retour standard). L'un était le compteur de programme, vous pouviez utiliser n'importe quel registre comme PC avec l' SEP Rninstruction. L'un était le pointeur de pile et deux étaient définis pour pointer toujours vers l'adresse de code SCRT, un pour l'appel, un pour le retour. Aucun registre n'a été traité de manière particulière. Gardez à l'esprit que ces détails sont de mémoire, ils peuvent ne pas être totalement corrects.

Par exemple, si R3 était le PC, R4 était l'adresse d'appel SCRT, R5 était l'adresse de retour SCRT et R2 était la «pile» (citations telles qu'elles sont implémentées dans le logiciel), SEP R4définirait R4 comme le PC et commencerait à exécuter le SCRT code d'appel.

Il stockerait alors R3 sur la "pile" R2 (je pense que R6 était utilisé pour le stockage temporaire), l'ajustant vers le haut ou vers le bas, saisissait les deux octets suivant R3, les chargeait dans R3, puis SEP R3exécutait et fonctionnait à la nouvelle adresse.

Pour revenir, cela SEP R5retirerait l'ancienne adresse de la pile R2, y ajouterait deux (pour sauter les octets d'adresse de l'appel), la chargerait dans R3 et SEP R3commencerait à exécuter le code précédent.

Très difficile à comprendre au départ après tout le code basé sur la pile 6502/6809 / z80, mais toujours élégant d'une manière qui se heurte au mur. L'une des principales caractéristiques de vente de la puce était également une suite complète de 16 registres 16 bits, malgré le fait que vous en ayez immédiatement perdu 7 (5 pour SCRT, deux pour DMA et interruptions de mémoire). Ahh, le triomphe du marketing sur la réalité :-)

Le système z est en fait assez similaire, utilisant ses registres R14 et R15 pour l'appel / le retour.

paxdiablo
la source
3
Pour ajouter à la liste, ARM peut croître dans les deux sens, mais peut être réglé sur l'un ou l'autre par une implémentation de silicium particulière (ou peut être laissé sélectionnable par le logiciel). Les rares personnes avec lesquelles j'ai eu affaire ont toujours été en mode de croissance.
Michael Burr
1
Dans le petit bout du monde ARM que j'ai vu jusqu'à présent (ARM7TDMI), la pile est entièrement gérée dans le logiciel. Les adresses de retour sont stockées dans un registre qui est sauvegardé par le logiciel si nécessaire, et les instructions de pré- / post-incrémentation / décrémentation permettent de le mettre avec d'autres éléments sur la pile dans les deux sens.
starblue
1
Un HPPA, la pile a grandi! Assez rare parmi les architectures raisonnablement modernes.
tml
2
Pour les curieux, voici une bonne ressource sur le fonctionnement de la pile sous z / OS: www-03.ibm.com/systems/resources/Stack+and+Heap.pdf
Dillon Cower
1
Merci @paxdiablo pour votre compréhension. Parfois, les gens prennent cela comme un affront personnel lorsque vous faites un tel commentaire, surtout lorsqu'il s'agit d'un commentaire plus ancien. Je sais seulement qu'il y a une différence parce que j'ai moi-même commis la même erreur dans le passé. Prends soin de toi.
CasaDeRobison
23

En C ++ (adaptable à C) stack.cc :

static int
find_stack_direction ()
{
    static char *addr = 0;
    auto char dummy;
    if (addr == 0)
    {
        addr = &dummy;
        return find_stack_direction ();
    }
    else
    {
        return ((&dummy > addr) ? 1 : -1);
    }
}
jfs
la source
14
Wow, ça fait longtemps que je n'ai pas vu le mot-clé "auto".
paxdiablo
9
(& dummy> addr) n'est pas défini. Le résultat de l'alimentation de deux pointeurs vers un opérateur relationnel est défini uniquement si les deux pointeurs pointent dans le même tableau ou structure.
sigjuice
2
Essayer d'étudier la disposition de votre propre pile - quelque chose que C / C ++ ne spécifie pas du tout - est «non portable» pour commencer, donc je ne m'en soucierais pas vraiment. Cependant, il semble que cette fonction ne fonctionnera correctement qu'une seule fois.
éphémère
9
Il n'est pas nécessaire d'utiliser un staticpour cela. Au lieu de cela, vous pouvez passer l'adresse en tant qu'argument à un appel récursif.
R .. GitHub STOP AIDER ICE
5
de plus, en utilisant a static, si vous appelez cela plus d'une fois, les appels suivants peuvent échouer ...
Chris Dodd
7

L'avantage de la réduction est que dans les systèmes plus anciens, la pile était généralement au sommet de la mémoire. Les programmes remplissaient généralement la mémoire en commençant par le bas, ce type de gestion de la mémoire minimisait donc le besoin de mesurer et de placer le bas de la pile à un endroit raisonnable.

mP.
la source
3
Pas un «avantage», une tautologie vraiment.
Marquis of Lorne
1
Pas une tautologie. Le but est d'avoir deux régions mémoire croissantes qui n'interfèrent pas (à moins que la mémoire soit pleine de toute façon), comme l'a souligné @valenok.
YvesgereY
6

La pile s'agrandit sur x86 (définie par l'architecture, pop incrémente le pointeur de pile, push décrémente.)

Michael
la source
5

Dans MIPS et beaucoup de modernes architectures RISC (comme PowerPC, RISC-V, SPARC ...) il n'y a pas pushet popinstructions. Ces opérations sont explicitement effectuées en ajustant manuellement le pointeur de pile, puis en chargeant / en stockant la valeur par rapport au pointeur ajusté. Tous les registres (sauf le registre zéro) sont à usage général, donc en théorie, tout registre peut être un pointeur de pile, et la pile peut croître dans n'importe quelle direction le programmeur veut

Cela dit, la pile se développe généralement vers le bas sur la plupart des architectures, probablement pour éviter le cas où la pile et les données de programme ou les données de tas se multiplient et se heurtent les unes aux autres. Il y a aussi les bonnes raisons d'adressage mentionnées dans la réponse de sh- . Quelques exemples: MIPS ABI croît vers le bas et utilise $29(AKA $sp) comme pointeur de pile, RISC-V ABI se développe également vers le bas et utilise x2 comme pointeur de pile

Dans Intel 8051, la pile grandit, probablement parce que l'espace mémoire est si petit (128 octets dans la version originale) qu'il n'y a pas de tas et vous n'avez pas besoin de mettre la pile sur le dessus pour qu'elle soit séparée du tas croissant du bas

Vous pouvez trouver plus d'informations sur l'utilisation de la pile dans diverses architectures sur https://en.wikipedia.org/wiki/Calling_convention

Voir également

phuclv
la source
2

Juste un petit ajout aux autres réponses, qui, pour autant que je sache, n'ont pas touché ce point:

La croissance de la pile vers le bas donne à toutes les adresses de la pile un décalage positif par rapport au pointeur de pile. Il n'y a pas besoin de décalages négatifs, car ils ne pointeraient que vers l'espace de pile inutilisé. Cela simplifie l'accès aux emplacements de pile lorsque le processeur prend en charge l'adressage relatif au pointeur de pile.

De nombreux processeurs ont des instructions qui autorisent les accès avec un décalage uniquement positif par rapport à certains registres. Ceux-ci incluent de nombreuses architectures modernes, ainsi que des anciennes. Par exemple, l'ABI ARM Thumb fournit des accès relatifs au pointeur de pile avec un décalage positif codé dans un seul mot d'instruction de 16 bits.

Si la pile augmentait vers le haut, tous les décalages utiles par rapport au pointeur de pile seraient négatifs, ce qui est moins intuitif et moins pratique. Il est également en contradiction avec d'autres applications d'adressage relatif aux registres, par exemple pour accéder aux champs d'une structure.

sh-
la source
2

Sur la plupart des systèmes, la pile diminue et mon article à l' adresse https://gist.github.com/cpq/8598782 explique POURQUOI il se réduit. C'est simple: comment disposer deux blocs de mémoire croissants (tas et pile) dans un morceau fixe de mémoire? La meilleure solution est de les mettre aux extrémités opposées et de les laisser pousser l'une vers l'autre.

Valenok
la source
cet essentiel semble être mort maintenant :(
Ven
@Ven - Je peux y arriver
Brett Holman
1

Il grossit car la mémoire allouée au programme a les "données permanentes" c'est-à-dire le code du programme lui-même en bas, puis le tas au milieu. Vous avez besoin d'un autre point fixe à partir duquel référencer la pile, ce qui vous laisse en haut. Cela signifie que la pile s'agrandit jusqu'à ce qu'elle soit potentiellement adjacente aux objets sur le tas.

Kai
la source
0

Cette macro doit le détecter lors de l'exécution sans UB:

#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);

__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) { 
    return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}
PSkocik
la source