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?
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?
Réponses:
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:
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 Rn
instruction. 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 R4
dé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 R3
exécutait et fonctionnait à la nouvelle adresse.
Pour revenir, cela SEP R5
retirerait 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 R3
commencerait à 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.
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);
}
}
static
pour cela. Au lieu de cela, vous pouvez passer l'adresse en tant qu'argument à un appel récursif.
static
, si vous appelez cela plus d'une fois, les appels suivants peuvent échouer ...
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.
Dans MIPS et beaucoup de modernes architectures RISC (comme PowerPC, RISC-V, SPARC ...) il n'y a pas push
et pop
instructions. 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
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.
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.
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.
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;
}