Je prépare du matériel de formation en langage C et je veux que mes exemples correspondent au modèle de pile typique.
Dans quelle direction évolue une pile C sous Linux, Windows, Mac OSX (PPC et x86), Solaris et les systèmes Unix les plus récents?
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 est exécuté. 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 la CPU offre un type de choix, la convention ABI/d'appel utilisée par le système d'exploitation spécifie le choix à effectuer si vous souhaitez que votre code appelle le code de tous les autres.
Les processeurs et leur direction sont:
En montrant mon âge sur ces derniers mois, le 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 dont il disposait :-) et de mon second ordinateur, le COMX -35 (suivant mon ZX80 ).
Les détails de PDP11 glanés de ici , 8051 détails de ici .
L'architecture SPARC utilise un modèle de registre à fenêtre glissante. Les détails visibles sur le plan architectural incluent également un tampon circulaire de fenêtres de registre 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 explique le manuel de SPARCv8 , les instructions SAVE et RESTORE ressemblent aux instructions ADD plus la rotation de la fenêtre de registre. L'utilisation d'une constante positive au lieu de la valeur négative habituelle donnerait une pile à la hausse.
La technique SCRT mentionnée ci-dessus en est une autre - le 1802 en utilise ou seize registres 16 bits pour SCRT (technique d'appel et de retour standard). L'un était le compteur de programme, vous pouvez utiliser n'importe quel registre en tant que PC avec l'instruction SEP Rn
. L'un était le pointeur de pile et deux étaient toujours configurés pour pointer vers l'adresse du code SCRT, un pour l'appel, un pour le retour. Non registre a été traité de manière spéciale. 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 la "pile" (guillemets lorsqu’il est implémenté dans le logiciel), SEP R4
définirait R4 comme étant le PC et commencerait à exécuter le Code d'appel SCRT.
Il serait ensuite stocker R3 sur la "pile" R2 (je pense que R6 a été utilisé pour le stockage temporaire), l'ajustant de haut en bas, saisir les deux octets suivant R3, les charger dans R3, puis faire SEP R3
et être en cours à la nouvelle adresse.
Pour renvoyer, il faudrait SEP R5
qui extrairait l’ancienne adresse de la pile R2, en ajouterait deux (pour ignorer les octets d’adresse de l’appel), le charger dans R3 et SEP R3
pour commencer à exécuter le code précédent.
Très difficile à comprendre, après tout le code basé sur une pile 6502/6809/z80, mais toujours élégant dans un style bang-tête-contre-le-mur. Une des caractéristiques les plus vendues de la puce était une suite complète de 16 registres 16 bits, malgré le fait que vous en avez immédiatement perdu 7 (5 pour SCRT, deux pour DMA et des interruptions de mémoire). Ahh, le triomphe du marketing sur la réalité :-)
System z est en fait assez similaire, utilisant ses registres R14 et R15 pour les appels/retours.
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);
}
}
L'avantage de la croissance 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 partant du bas; cette gestion de la mémoire minimisait donc la nécessité de mesurer et de placer le bas de la pile à un endroit judicieux.
La pile grandit sur x86 (définie par l'architecture, pop incrémente le pointeur de pile, Push décroît.)
Dans MIPS, il n'y a pas d'instruction Push
/pop
. Tous les push/pops sont explicitement effectués par load/store par rapport au pointeur de la pile, puis ajustent manuellement le pointeur $sp
. Cependant, comme tous les registres (à l'exception de $0
) sont d'usage général, en théorie n'importe quel registre peut être un pointeur de pile et la pile peut croître dans n'importe quelle direction souhaitée par le programmeur. Les ABI MIPS poussent généralement vers le bas.
Dans Intel 8051, la pile grandit, probablement parce que la mémoire est si petite (128 octets dans la version originale) qu'il n'y a pas de tas et qu'il n'est pas nécessaire de placer la pile au-dessus pour qu'elle soit séparée du tas. du bas.
Sur la plupart des systèmes, la pile diminue et mon article à https://Gist.github.com/cpq/8598782 explique POURQUOI sa croissance. La raison en est que c'est la disposition optimale de deux régions de mémoire en croissance (tas et pile).
Il s’agrandit car la mémoire allouée au programme contient les "données permanentes", c’est-à-dire le code du programme lui-même situé en bas, puis le segment de mémoire 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 grossit jusqu'à devenir potentiellement adjacente aux objets du tas.
Cette macro devrait le détecter au moment 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;
}
Juste un petit ajout aux autres réponses qui, autant que je sache, n’ont pas touché à ce point:
Faire croître la pile vers le bas donne à toutes les adresses de la pile un décalage positif par rapport au pointeur de la pile. Il n'est pas nécessaire de recourir à des compensations négatives, car elles ne font que pointer 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 point de pile.
De nombreux processeurs ont des instructions qui permettent des accès avec un décalage positif uniquement par rapport à certains registres. Celles-ci incluent de nombreuses architectures modernes, ainsi que des anciennes. Par exemple, le ARM Thumb ABI 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 grandissait, tous les décalages utiles relatifs 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 au registre, par exemple pour accéder aux champs d'une structure.