Pourquoi avons-nous toujours pousser la pile à l'envers?

46

Lorsque vous compilez du code C et que vous regardez l’assemblage, la pile croît à l’arrière comme ceci:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- Cela signifie-t-il que le pointeur de base ou le pointeur de pile déplacent les adresses de la mémoire au lieu de remonter? Pourquoi donc?

J'ai changé $5, -4(%rbp)pour $5, +4(%rbp), compilé et exécuté le code et il n'y avait aucune erreur. Alors pourquoi devons-nous toujours revenir en arrière sur la pile de mémoire?

alex
la source
2
Notez que -4(%rbp)cela ne déplace pas le pointeur de base et que +4(%rbp)cela ne pourrait pas fonctionner.
Margaret Bloom
14
" pourquoi devons-nous encore revenir en arrière " - selon vous, quel serait l'avantage de poursuivre? En fin de compte, peu importe, vous devez simplement en choisir un.
Bergi
31
"Pourquoi cultivons-nous la pile à l'envers?" - Parce que si nous ne le faisions pas, quelqu'un d'autre demanderait pourquoi on fait mallocgrandir le tas en arrière
slebetman
2
@MargaretBloom: Apparemment, sur la plate-forme de l'OP, le code de démarrage du tube cathodique n'a pas d'importance si mainsa RBP est annulée. C'est certainement possible. (Et oui, l'écriture 4(%rbp)irait sur la valeur RBP enregistrée). Euh, en fait, cela n’est jamais le cas mov %rsp, %rbp, donc l’accès à la mémoire est relatif à la RBP de l’appelant , si c’est ce que le PO a réellement testé !!! Si cela a été réellement copié à partir de la sortie du compilateur, certaines instructions ont été omises!
Peter Cordes
1
Il me semble que "en arrière" ou "en avant" (ou "en bas" et "en haut") dépend de votre point de vue. Si vous représentiez la mémoire sous forme de colonne avec des adresses basses en haut, augmenter la pile en décrémentant un pointeur de pile serait analogue à une pile physique.
jamesdlin le

Réponses:

86

Cela signifie-t-il que le pointeur de base ou le pointeur de pile se déplacent réellement vers le bas des adresses de mémoire au lieu de remonter? Pourquoi donc?

Oui, les pushinstructions décrémentent le pointeur de pile et écrivent dans la pile, tandis popque l’inverse effectue une lecture à partir de la pile et incrémente le pointeur de pile.

Ceci est quelque peu historique dans le sens où pour les machines avec une mémoire limitée, la pile était placée haut et grossie vers le bas, alors que le tas était bas et développé vers le haut. Il n'y a qu'un seul espace de "mémoire libre" - entre le tas et la pile, et cet espace est partagé, l'un ou l'autre peut croître dans l'espace selon les besoins individuels. Ainsi, le programme ne manque de mémoire que lorsque la pile et le tas se rencontrent sans laisser de mémoire libre. 

Si la pile et le tas grandissent tous deux dans la même direction, il y a deux lacunes et la pile ne peut pas vraiment se transformer dans l'espace du tas (l'inverse est également problématique).

À l'origine, les processeurs n'avaient pas d'instructions de traitement de pile dédiées. Cependant, avec l’ajout de la pile au matériel, le modèle évoluait à la baisse et les processeurs suivent encore ce modèle.

On pourrait faire valoir que sur un ordinateur 64 bits, il y a suffisamment d'espace d'adressage pour permettre plusieurs espaces vides - et, à preuve, de multiples espaces vides sont nécessairement le cas lorsqu'un processus comporte plusieurs threads. Bien que cela ne soit pas une motivation suffisante pour changer les choses, étant donné que, avec les systèmes à fractures multiples, la direction de la croissance est sans doute arbitraire. C'est pourquoi tradition / compatibilité fait pencher la balance en avant.


Il faudrait modifier les instructions de manipulation de la pile CPU afin de changer la direction de la pile, ou bien renoncer à l' utilisation des instructions pousser et popping dédiés (par exemple push, pop, call, ret, autres).

Notez que l’architecture du jeu d’instructions MIPS n’est pas dédiée push& pop, il est donc pratique de faire croître la pile dans un sens ou dans l’autre - vous pouvez toujours souhaiter une disposition mémoire un espace pour un processus à un seul thread, mais vous pouvez augmenter la pile et le tas vers le bas. Cependant, si vous agissiez de la sorte , certains codes C varargs pourraient nécessiter un ajustement de la source ou du passage des paramètres sous le capot.

(En fait, étant donné qu’il n’existe pas de traitement de pile dédié sur MIPS, nous pourrions utiliser pré ou post incrémentation ou pré ou post décrémentation pour la mise en place de la pile tant que nous utilisons l’inverse exact pour le vidage de la pile et en supposant que Le système d’exploitation respecte le modèle d’utilisation de pile choisi. En effet, dans certains systèmes embarqués et certains systèmes éducatifs, la pile MIPS est agrandie.

Erik Eidt
la source
32
Il est non seulement pushet popsur la plupart des architectures, mais aussi l'interruption de manutention beaucoup plus important, call, retet tout ce qui a cuit au four en interaction avec la pile.
Déduplicateur
3
ARM peut avoir les quatre types de pile.
Margaret Bloom
14
Pour ce que cela vaut, je ne pense pas que "la direction de la croissance soit arbitraire" dans le sens où l’un ou l’autre choix est tout aussi bon. En grandissant, la propriété déborde sur la fin d'une mémoire tampon, plus tôt dans la pile, y compris les adresses de retour enregistrées. Grandir a la propriété qui déborde à la fin d’un tampon d’obturateur uniquement dans le même ou plus tard (si le tampon n’est pas dans la dernière, il peut y avoir des ultérieurs) trame d’appel, et éventuellement même seulement dans l’espace inutilisé page après la pile). Donc, du point de vue de la sécurité, grandir semble hautement préférable
R ..
6
@R ..: grandir n'élimine pas les exploitations de saturation de la mémoire tampon, car les fonctions vulnérables ne sont généralement pas des fonctions feuilles: elles appellent d'autres fonctions, plaçant une adresse de retour au-dessus de la mémoire tampon. Les fonctions feuille qui obtiennent un pointeur de leur appelant pourraient devenir vulnérables au remplacement de leur propre adresse de retour. Par exemple, si une fonction alloue un tampon sur la pile et le passe à gets(), ou fait un objet strcpy()qui ne soit pas en ligne, le retour dans ces fonctions de bibliothèque utilisera l'adresse de retour écrasée. Actuellement, avec des piles à la baisse, c'est quand leur appelant revient.
Peter Cordes le
5
@PeterCordes: En effet, mon commentaire a noté que les cadres de pile de même niveau ou plus récents que le tampon débordé sont toujours potentiellement clobberables, mais c'est beaucoup moins. Dans le cas où la fonction clobber est une fonction feuille appelée directement par la fonction dont elle est le tampon (par exemple strcpy), sur une arche où l'adresse de retour est conservée dans un registre sauf si elle doit être renversée, l'accès n'est pas possible pour obstruer le retour adresse.
R ..
8

Dans votre système spécifique, la pile commence à partir d'une adresse de mémoire haute et "grandit" vers le bas pour devenir une adresse de mémoire faible. (le cas symétrique de bas en haut existe aussi)

Et puisque vous avez changé de -4 à +4 et que cela a fonctionné, cela ne signifie pas que c'est correct. La structure de la mémoire d’un programme en cours d’exécution est plus complexe et dépend de nombreux autres facteurs qui peuvent avoir contribué au fait que vous n’avez pas subi une panne instantanée sur ce programme extrêmement simple.

nadir
la source
1

Le pointeur de pile pointe vers la limite entre la mémoire de pile allouée et non allouée. Sa croissance vers le bas signifie qu'il pointe au début de la première structure dans l'espace de pile alloué, les autres éléments alloués étant suivis par des adresses plus grandes. Avoir des pointeurs indiquant le début des structures allouées est beaucoup plus courant que l’inverse.

De nos jours, sur de nombreux systèmes, il existe un registre séparé pour les trames de pile qui peuvent être déroulées de manière relativement fiable afin de comprendre la chaîne d'appels, avec un stockage de variables local intercalé. La manière dont ce registre de trame de pile est configuré sur certaines architectures signifie qu'il pointe finalement derrière le stockage de variables local, par opposition au pointeur de pile précédent . Donc, utiliser ce registre de trame de pile nécessite alors une indexation négative.

Notez que les cadres de pile et leur indexation sont un aspect secondaire des langages informatiques compilés. C'est donc le générateur de code du compilateur qui doit gérer le "manque de naturel" plutôt qu'un programmeur de langage d'assemblage médiocre.

Ainsi, bien qu'il y ait eu de bonnes raisons historiques de choisir des piles de croître vers le bas (et certaines d'entre elles sont conservées si vous programmez en langage assembleur et que vous ne vous occupez pas de la configuration d'un cadre de pile approprié), elles sont devenues moins visibles.


la source
2
"Aujourd'hui, sur de nombreux systèmes, il existe un registre séparé pour les cadres de pile", vous êtes en retard. Les formats d'informations de débogage plus riches ont largement éliminé le besoin de pointeurs de trames de nos jours.
Peter Green