Cette question se rapproche dangereusement des limites trop larges et des limites fondées sur l'opinion, mais je comprends ce que vous demandez.
Il faut comprendre qu'historiquement, il y a eu d'innombrables conceptions de processeurs et d'implémentations de systèmes différentes. Les langages et les processeurs ont évolué au fil du temps. Par conséquent, toute affirmation absolue est en fait limitée, car il y a sans aucun doute eu un système ou un processeur auquel cette affirmation ne s'applique pas.
En général, la pile n'est que de la mémoire et le pointeur de pile n'est qu'une adresse/un décalage dans cette mémoire. Ce qui différencie un push/pop d'un accès normal à la mémoire, c'est que le programmeur ne se soucie pas/ne devrait pas se soucier de l'adresse spécifique, mais plutôt de l'aspect relatif : j'ai poussé cinq choses, donc la troisième chose est à telle distance du pointeur de pile, pour nettoyer, je dois sortir 5 choses, etc. Mais il s'agit simplement d'une mémoire vive quelque part avec un pointeur d'adresse.
Bien que nous pensions que les adresses inférieures sont plus basses et les adresses supérieures plus hautes, et que nous nous attendions à ce que les dessins/visualisations de la mémoire présentent les adresses inférieures plus bas sur le diagramme et les adresses supérieures plus haut sur le diagramme, il arrive, pour une bonne raison ou parfois non, que cette situation soit inversée. Sur une puce, il n'y a pas vraiment de haut ou de bas et il n'y a pas d'hypothèse selon laquelle la mémoire est disposée de manière physiquement linéaire en 2D, il s'agit simplement de visualisations.
Je ne connais pas d'exception, mais en général les processeurs exécutent dans le sens des adresses croissantes, une instruction à l'adresse 0x1000 qui fait 4 octets de long, l'instruction suivante est supposée être à 0x1004, et non à 0xFFC. Supposons donc que le code se développe vers le haut, c'est-à-dire des adresses inférieures vers les adresses supérieures.
Supposons que notre microprogramme fonctionne dans la mémoire vive et non dans la mémoire flash, et que nous parlions de la consommation de mémoire vive. Et pensons en termes de baremetal et non de système d'exploitation avec de nombreuses applications chargées en même temps.
Un programme comporte généralement du code (souvent appelé .text), des données, des variables (globales), etc. (souvent appelées .data et .bss). Le tas, qui est la mémoire allouée au moment de l'exécution et la pile.
Je n'ai pas fait de recherches à ce sujet, mais d'après ce que l'on m'a enseigné et le nom lui-même, on pourrait considérer une pile comme une pile d'assiettes ou une pile de cartes de notes qui, sous l'effet de la gravité, s'élèvent vers le haut. Indépendamment de l'architecture du processeur, il n'est pas rare de se représenter une pile comme s'élevant vers le haut, les nouveaux éléments étant placés sur les anciens, l'élément supérieur étant enlevé pour accéder aux éléments inférieurs. Mais ce n'est pas si rigide, je ne sais pas si c'est 50/50 mais vous verrez aussi souvent une pile qui croît vers le bas ou vers le haut, ou une fenêtre coulissante avec le pointeur de pile qui ne se déplace pas visuellement dans les diagrammes mais les données qui se déplacent vers le haut ou vers le bas en fonction de la façon dont elles sont représentées.
Notez également que le nom de ce site, Stack Overflow, a une connotation particulière...
Pour aller droit au but, le modèle classique (qui comporte des exceptions mentionnées plus loin) est le suivant : en partant de la mémoire inférieure, ou même de zéro, vous avez votre code, le code machine et tout ce qui entre dans cette catégorie. Vous avez ensuite vos variables globales .data et .bss, puis vous avez votre tas et le sommet est votre pile. Le tas et la pile sont considérés comme dynamiques au moment de l'exécution. Si vous ne libérez jamais, le tas est supposé croître vers le haut. La solution naturelle pour la pile est donc de croître vers le bas. Vous commencez votre tas à l'adresse la plus basse que vous pouvez idéalement sur les autres éléments (.text, .data, .bss) et la pile aussi haut que vous le pouvez, de sorte qu'un débordement de pile (pile et tas entrant en collision, la pile augmentant dans la ram allouée au tas).
Ce modèle traditionnel implique que la pile croît vers le bas, c'est-à-dire des adresses supérieures vers les adresses inférieures. De nombreuses architectures de jeux d'instructions limitent les solutions push/pop à cela, en utilisant les instructions telles qu'elles sont conçues, la pile croît vers le bas. Il y a des exceptions, par exemple les instructions traditionnelles (pré-aarch64) arm (full sized et non thumb) peuvent aller dans les deux sens, donc dans ce cas, c'est le choix de l'auteur du compilateur et non une contrainte de l'architecture. On peut soutenir qu'avec un registre à usage général qui peut accéder à la mémoire, un compilateur peut choisir d'utiliser de simples instructions load/store et non pas push/pop ou des instructions équivalentes et faire ce qu'il veut. Mais à quelques exceptions près, la pile croît vers le bas du point de vue de l'adresse.
Dans certaines architectures, la pile est enfouie dans un espace non visible. Les anciennes puces peuvent avoir, par rapport à aujourd'hui, une très petite pile de 16 ou 32 pouces de profondeur et notre seul accès est le push et le pop, et c'est tout.
Certaines architectures avec push/pop ou équivalent, sur un push par exemple, écriront puis ajusteront le pointeur de pile ou ajusteront le pointeur de pile puis écriront. Ainsi, pour un système 16 bits, pour obtenir tous les emplacements possibles, vous commencerez par 0x10000 que vous ne pouvez pas représenter, donc 0x0000, d'autres 0xffff ou 0xfffc en fonction de l'architecture et de son fonctionnement, etc.
Ainsi, si vous voulez visualiser une pile comme étant littéralement une pile de choses, une pile de cartes de notes, une pile d'assiettes, etc. Alors, en raison de la gravité, vous la visualiserez en train de croître vers le haut. J'écris un nombre sur une carte, je la place sur la pile, j'écris un autre nombre sur une carte et je le place (push) sur la pile, je retire la carte (pop) et ainsi de suite. Comme il s'agit d'une question de 50/50, vous verrez parfois la pile visualisée de cette manière, avec les adresses les plus élevées dans la partie inférieure du diagramme et les adresses les plus basses dans la partie supérieure du diagramme.
C'est pourquoi ils ont dessiné le diagramme de cette façon. En fin de compte, il faut se préparer mentalement à faire face à n'importe quelle façon dont les gens visualisent une pile.
- Pourquoi le pointeur de pile commence-t-il à la dernière adresse de la pile ?
Il s'agit d'un cas typique au sens classique du terme. Dans le monde réel, il existe des cas d'utilisation où la pile est placée dans un espace mémoire différent de celui des autres éléments, éventuellement protégés contre la sortie de leur espace par les dispositifs de sécurité (mmu, etc.). Mais c'est souvent une limitation de l'architecture qui fait que l'utilisation normale du pointeur de pile et/ou des instructions est de faire croître la pile vers le bas par rapport à l'adresse mémoire utilisée. La dernière adresse est une façon classique de procéder, mais on voit souvent des gens allouer de l'espace à la pile dans l'éditeur de liens script et elle atterrit là où elle atterrit (parfois même en dessous du tas ou des données).
- Est-ce vraiment ainsi que les piles sont implémentées dans tous les langages ?
Trop large, le langage lui-même se compile en code qui utilise des instructions, son enchaînement et le bootstrap (ou le système d'exploitation) qui détermine la valeur initiale de la pile pour un programme. Et il n'est pas rare que les instructions basées sur le pointeur de pile soient limitées à une pile qui croît vers le bas. S'il y a un choix, basé sur l'opinion, je m'attendrais, en raison de l'histoire, à ce que l'implémentation soit à croissance descendante (adresse).
- Cette façon d'implémenter la pile permet-elle d'éviter les problèmes liés au débordement de la pile ?
Oui, si nous supposons que le tas croît vers le haut et que la pile croît vers le bas, il faut que le tas commence au bas de l'espace disponible et la pile au sommet pour laisser le plus de place possible avant qu'un débordement de la pile ne se produise.
- Cela a-t-il un rapport avec la façon dont la pile et le tas sont stockés dans la mémoire ?
Oui, sur la base d'une opinion. Comme indiqué ci-dessus.
- Qu'est-ce qui aurait changé si nous avions commencé par l'adresse $ffe6 ?
Il n'y a rien de vraiment important puisque chaque "fonction" est appelée et que le pointeur de pile est là où il se trouve. C'est là tout l'intérêt : vous ne vous souciez pas de l'adresse, vous vous souciez simplement de faire correspondre le "push" et le "popping" ou, dans la mesure du possible, l'adressage relatif, et non absolu. Ainsi, si $ffe6, l'adresse devient plus petite/plus grande au fur et à mesure que l'on pousse et que l'on retire. Si $8000, même chose $5432, même chose. Si vous commencez à une adresse différente de celle montrée dans le tutoriel, tout fonctionne de la même manière, seules les adresses physiques montrées devront refléter le nouveau point de départ.
Donc, oui, la vision traditionnelle d'une pile est celle du dernier entré, premier sorti. Elle croît vers le bas dans l'espace d'adressage, mais l'auteur d'un texte ne sait pas si l'adresse la plus élevée se trouve en bas ou en haut du diagramme. En réalité, les jeux d'instructions les plus performants ne se limitent pas strictement à pousser et à sauter, mais aussi à l'adressage relatif, de sorte que si vous commencez par apprendre seulement à pousser et à sauter, vous passez ensuite directement à l'adressage relatif. Si j'ai poussé 5 éléments sur la pile, je peux accéder à chacun d'entre eux avec l'adressage sp+offset, parfois avec des instructions spéciales basées sur le sp.
Ne vous préoccupez pas de la manière dont un didacticiel ou un manuel a représenté la pile, en plaçant les adresses les plus élevées en haut ou en bas.