Quel est exactement le pointeur de base et le pointeur de pile? Vers quoi pointent-ils?

225

En utilisant cet exemple provenant de wikipedia, dans lequel DrawSquare () appelle DrawLine (),

texte alternatif

(Notez que ce diagramme a des adresses hautes en bas et des adresses basses en haut.)

Quelqu'un pourrait-il m'expliquer quoi ebpet espsont dans ce contexte?

D'après ce que je vois, je dirais que le pointeur de la pile pointe toujours vers le haut de la pile et le pointeur de base vers le début de la fonction actuelle? Ou quoi?


edit: je veux dire cela dans le contexte des programmes Windows

edit2: Et comment ça eipmarche aussi?

edit3: J'ai le code suivant de MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Tous semblent être des dwords, prenant ainsi 4 octets chacun. Je peux donc voir qu'il y a un écart entre hInstance et var_4 de 4 octets. Que sont-ils? Je suppose que c'est l'adresse de retour, comme on peut le voir sur l'image de wikipedia?


(NDLR: supprimé une longue citation de la réponse de Michael, qui n'appartient pas à la question, mais une question de suivi a été modifiée dans):

En effet, le flux de l'appel de fonction est le suivant:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

Ma question (enfin, j'espère!) Est maintenant: qu'est-ce qui se passe exactement à partir du moment où j'éclate les arguments de la fonction que je veux appeler jusqu'à la fin du prologue? Je veux savoir comment l'ebp, esp évolue pendant ces moments (j'ai déjà compris comment fonctionne le prologue, je veux juste savoir ce qui se passe après avoir poussé les arguments sur la pile et avant le prologue).

Elysium dévoré
la source
23
Une chose importante à noter est que la pile se développe "vers le bas" en mémoire. Cela signifie que pour déplacer le pointeur de pile vers le haut, vous diminuez sa valeur.
BS
4
Une astuce pour différencier ce que font EBP / ESP et EIP: EBP & ESP traitent des données, tandis que EIP traite du code.
mmmmmmmm
2
Dans votre graphique, ebp (généralement) est le "pointeur de trame", en particulier le "pointeur de pile". Cela permet d'accéder aux sections locales via [ebp-x] et aux paramètres de pile via [ebp + x] de manière cohérente, indépendamment du pointeur de pile (qui change fréquemment au sein d'une fonction). L'adressage peut être fait via ESP, libérant EBP pour d'autres opérations - mais de cette façon, les débogueurs ne peuvent pas dire la pile d'appels ou les valeurs des locaux.
peterchen
4
@Ben. Pas nesacerily. Certains compilateurs mettent des cadres de pile dans le tas. Le concept de pile qui grandit n'est que cela, un concept qui le rend facile à comprendre. L'implémentation de la pile peut être n'importe quoi (l'utilisation de morceaux aléatoires du tas rend les hacks qui écrasent des parties de la pile beaucoup plus difficiles car ils ne sont pas aussi déterministes).
Martin York, le
1
en deux mots: le pointeur de pile permet aux opérations push / pop de fonctionner (donc push and pop sait où mettre / récupérer les données). le pointeur de base permet au code de référencer indépendamment les données qui ont été poussées précédemment sur la pile.
tigrou

Réponses:

229

esp est comme vous le dites, le haut de la pile.

ebpest généralement défini sur espau début de la fonction. Les paramètres de fonction et les variables locales sont accessibles en ajoutant et en soustrayant, respectivement, un décalage constant de ebp. Toutes les conventions d'appel x86 se définissent ebpcomme étant préservées sur les appels de fonction. ebplui-même pointe en fait sur le pointeur de base de l'image précédente, ce qui permet à la pile de marcher dans un débogueur et de visualiser les autres variables locales des images pour fonctionner.

La plupart des prologues de fonction ressemblent à ceci:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Plus tard dans la fonction, vous pouvez avoir du code comme (en supposant que les deux variables locales sont de 4 octets)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

L' optimisation des omissions FPO ou du pointeur de trame que vous pouvez activer éliminera cela et l'utilisera ebpcomme un autre registre et accèdera directement aux locaux esp, mais cela rend le débogage un peu plus difficile car le débogueur ne peut plus accéder directement aux trames de pile des appels de fonction précédents.

ÉDITER:

Pour votre question mise à jour, les deux entrées manquantes dans la pile sont:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

En effet, le flux de l'appel de fonction est le suivant:

  • Paramètres push ( hInstance, etc.)
  • Fonction d'appel, qui pousse l'adresse de retour
  • Pousser ebp
  • Allouer de l'espace aux locaux
Michael
la source
1
Merci pour l'explication! Mais je suis maintenant un peu confus. Supposons que j'appelle une fonction et que je suis dans la première ligne de son prologue, toujours sans en avoir exécuté une seule ligne. À ce stade, quelle est la valeur de l'ebp? La pile a-t-elle quelque chose à ce stade en plus des arguments poussés? Merci!
dévoré elysium
3
L'EBP n'est pas modifié comme par magie, donc jusqu'à ce que vous ayez établi un nouvel EBP pour votre fonction, vous aurez toujours la valeur des appelants. Et en plus des arguments, la pile contiendra également l'ancien EIP (adresse de retour)
MSalters
3
Bonne réponse. Bien qu'il ne puisse pas être complet sans mentionner ce qu'il y a dans l'épilogue: les instructions "laisser" et "ret".
Calmarius
2
Je pense que cette image aidera à clarifier certaines choses sur le flux. Gardez également à l'esprit que la pile croît vers le bas. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre
Est-ce moi ou tous les signes moins manquent dans l'extrait de code ci-dessus?
BarbaraKwarc
96

ESP est le pointeur de pile actuel, qui changera chaque fois qu'un mot ou une adresse est poussé ou sauté dans / hors de la pile. EBP est un moyen plus pratique pour le compilateur de garder une trace des paramètres d'une fonction et des variables locales que d'utiliser directement l'ESP.

Généralement (et cela peut varier d'un compilateur à l'autre), tous les arguments d'une fonction appelée sont poussés sur la pile par la fonction appelante (généralement dans l'ordre inverse de leur déclaration dans le prototype de la fonction, mais cela varie) . Ensuite, la fonction est appelée, ce qui pousse l'adresse de retour (EIP) sur la pile.

Lors de l'entrée dans la fonction, l'ancienne valeur EBP est poussée sur la pile et EBP est définie sur la valeur ESP. Ensuite, l'ESP est décrémenté (car la pile croît vers le bas en mémoire) pour allouer de l'espace aux variables locales et temporaires de la fonction. À partir de ce moment, pendant l'exécution de la fonction, les arguments de la fonction sont situés sur la pile à des décalages positifs d'EBP (car ils ont été poussés avant l'appel de fonction), et les variables locales sont situées à des décalages négatifs d'EBP (car ils ont été alloués sur la pile après l'entrée de la fonction). C'est pourquoi l'EBP est appelé le pointeur de trame , car il pointe vers le centre de la trame d'appel de fonction .

À la sortie, tout ce que la fonction a à faire est de régler ESP sur la valeur d'EBP (qui désalloue les variables locales de la pile et expose l'entrée EBP en haut de la pile), puis de faire sortir l'ancienne valeur EBP de la pile, puis la fonction retourne (insertion de l'adresse de retour dans EIP).

En revenant à la fonction appelante, il peut ensuite incrémenter ESP afin de supprimer les arguments de fonction qu'il a poussés sur la pile juste avant d'appeler l'autre fonction. À ce stade, la pile est de retour dans le même état qu'elle était avant d'appeler la fonction appelée.

David R Tribble
la source
15

Vous avez raison. Le pointeur de pile pointe vers l'élément supérieur de la pile et le pointeur de base pointe vers le haut "précédent" de la pile avant l'appel de la fonction.

Lorsque vous appelez une fonction, toute variable locale sera stockée sur la pile et le pointeur de pile sera incrémenté. Lorsque vous revenez de la fonction, toutes les variables locales de la pile sortent du domaine. Pour ce faire, redéfinissez le pointeur de pile sur le pointeur de base (qui était le haut "précédent" avant l'appel de fonction).

Faire l'allocation de mémoire de cette façon est très , très rapide et efficace.

Robert Cartaino
la source
14
@Robert: Lorsque vous dites "précédent" en haut de la pile avant l'appel de la fonction, vous ignorez à la fois les paramètres, qui sont poussés sur la pile juste avant d'appeler la fonction et l'appelant EIP. Cela pourrait dérouter les lecteurs. Disons simplement que dans un cadre de pile standard, EBP pointe vers le même endroit où ESP pointait juste après avoir entré la fonction.
wigy
7

EDIT: Pour une meilleure description, voir Désassemblage / Fonctions x86 et Cadres de pile dans un WikiBook sur l'assemblage x86. J'essaie d'ajouter des informations qui pourraient vous intéresser à l'aide de Visual Studio.

Le stockage de l'EBP de l'appelant en tant que première variable locale est appelé un cadre de pile standard, et cela peut être utilisé pour presque toutes les conventions d'appel sous Windows. Il existe des différences selon que l'appelant ou l'appelé désalloue les paramètres passés et quels paramètres sont passés dans les registres, mais ceux-ci sont orthogonaux au problème de trame de pile standard.

En parlant de programmes Windows, vous pourriez probablement utiliser Visual Studio pour compiler votre code C ++. Sachez que Microsoft utilise une optimisation appelée Frame Pointer Omission, qui rend presque impossible de parcourir la pile sans utiliser la bibliothèque dbghlp et le fichier PDB pour l'exécutable.

Cette omission de pointeur de trame signifie que le compilateur ne stocke pas l'ancien EBP sur un emplacement standard et utilise le registre EBP pour autre chose, par conséquent, vous avez du mal à trouver l'EIP de l'appelant sans savoir de combien d'espace les variables locales ont besoin pour une fonction donnée. Bien sûr, Microsoft fournit une API qui vous permet de faire des cheminements de pile, même dans ce cas, mais la recherche de la base de données de la table des symboles dans les fichiers PDB prend trop de temps pour certains cas d'utilisation.

Pour éviter FPO dans vos unités de compilation, vous devez éviter d'utiliser / O2 ou ajouter explicitement / Oy- aux indicateurs de compilation C ++ dans vos projets. Vous établissez probablement un lien avec le runtime C ou C ++, qui utilise FPO dans la configuration Release, vous aurez donc du mal à effectuer des parcours de pile sans dbghlp.dll.

perruque
la source
Je ne sais pas comment EIP est stocké sur la pile. Cela ne devrait-il pas être un registre? Comment un registre peut-il être sur la pile? Merci!
dévoré elysium le
L'appelant EIP est poussé sur la pile par l'instruction CALL elle-même. L'instruction RET récupère simplement le haut de la pile et la place dans l'EIP. Si vous avez des dépassements de mémoire tampon, ce fait peut être utilisé pour accéder au code utilisateur à partir d'un thread privilégié.
wigy
@devouredelysium Le contenu (ou la valeur ) du registre EIP est placé (ou copié sur) la pile, pas le registre lui-même.
BarbaraKwarc
@BarbaraKwarc Merci pour l' entrée de valeur . Je ne pouvais pas voir ce que le PO manquait à ma réponse. En effet, les registres restent où ils sont, seule leur valeur est envoyée à la RAM depuis le CPU. En mode amd64, cela devient un peu plus complexe, mais laissez cela à une autre question.
wigy
Et l'amd64? Je suis curieux.
BarbaraKwarc
6

Tout d'abord, le pointeur de pile pointe vers le bas de la pile, car les piles x86 sont construites à partir de valeurs d'adresse élevées vers des valeurs d'adresse inférieures. Le pointeur de pile est le point où le prochain appel à pousser (ou appeler) placera la valeur suivante. Son fonctionnement est équivalent à l'instruction C / C ++:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

Le pointeur de base est en haut de l'image actuelle. ebp indique généralement votre adresse de retour. ebp + 4 pointe vers le premier paramètre de votre fonction (ou la valeur this d'une méthode de classe). ebp-4 pointe vers la première variable locale de votre fonction, généralement l'ancienne valeur d'ebp afin que vous puissiez restaurer le pointeur de trame précédent.

jmucchiello
la source
2
Non, ESP ne pointe pas vers le bas de la pile. Le schéma d'adressage de la mémoire n'a rien à voir avec cela. Peu importe que la pile se développe vers des adresses inférieures ou supérieures. Le "haut" de la pile est toujours l'endroit où la prochaine valeur sera poussée (placée sur le haut de la pile), ou, sur d'autres architectures, où la dernière valeur poussée a été placée et où elle se trouve actuellement. Par conséquent, ESP pointe toujours vers le haut de la pile.
BarbaraKwarc
1
Le bas ou la base de la pile, d'autre part, est l'endroit où la première (ou la plus ancienne ) valeur a été placée, puis couverte par des valeurs plus récentes. C'est de là que vient le nom "pointeur de base" pour EBP: il était censé pointer vers la base (ou le bas) de la pile locale actuelle d'un sous-programme.
BarbaraKwarc
Barbara, dans Intel x86, la pile est à l'envers. Le haut de la pile contient le premier élément poussé sur la pile et chaque élément suivant est poussé EN DESSOUS de l'élément supérieur. Le bas de la pile est l'endroit où les nouveaux objets sont placés. Les programmes sont placés en mémoire à partir de 1k et atteignent l'infini. La pile commence à l'infini, de façon réaliste au maximum mem mem moins ROM, et croît vers 0. ESP pointe vers une adresse dont la valeur est inférieure à la première adresse poussée.
jmucchiello
1

Cela fait longtemps que je n'ai pas fait de programmation d'assemblage, mais ce lien pourrait être utile ...

Le processeur possède une collection de registres qui sont utilisés pour stocker des données. Certains d'entre eux sont des valeurs directes tandis que d'autres pointent vers une zone dans la RAM. Les registres ont tendance à être utilisés pour certaines actions spécifiques et chaque opérande de l'assemblage nécessitera une certaine quantité de données dans des registres spécifiques.

Le pointeur de pile est principalement utilisé lorsque vous appelez d'autres procédures. Avec les compilateurs modernes, un tas de données sera d'abord vidé sur la pile, suivi de l'adresse de retour pour que le système sache où retourner une fois qu'on lui a dit de retourner. Le pointeur de pile pointera à l'emplacement suivant où les nouvelles données peuvent être poussées vers la pile, où elles resteront jusqu'à ce qu'elles soient à nouveau renvoyées.

Les registres de base ou les registres de segments pointent simplement vers l'espace d'adressage d'une grande quantité de données. Combiné avec un deuxième enregistreur, le pointeur de base divisera la mémoire en blocs énormes tandis que le deuxième registre pointera sur un élément de ce bloc. Des pointeurs de base pointent donc vers la base de blocs de données.

Gardez à l'esprit que l'assemblage est très spécifique au processeur. La page à laquelle j'ai lié fournit des informations sur les différents types de CPU.

Wim ten Brink
la source
Les registres de segments sont séparés sur x86 - ils sont gs, cs, ss, et à moins que vous n'écriviez un logiciel de gestion de mémoire, vous ne les touchez jamais.
Michael
ds est également un registre de segment et à l'époque du MS-DOS et du code 16 bits, vous deviez certainement changer ces registres de segment de temps en temps, car ils ne pouvaient jamais pointer vers plus de 64 Ko de RAM. Pourtant, DOS pouvait accéder à la mémoire jusqu'à 1 Mo car il utilisait des pointeurs d'adresse de 20 bits. Plus tard, nous avons eu des systèmes 32 bits, certains avec des registres d'adresses 36 bits et maintenant des registres 64 bits. De nos jours, vous n'aurez plus vraiment besoin de modifier ces registres de segments.
Wim ten Brink le
Aucun système d'exploitation moderne n'utilise 386 segments
Ana Betts
@Paul: FAUX! FAUX! FAUX! Les segments de 16 bits sont remplacés par des segments de 32 bits. En mode protégé, cela permet la virtualisation de la mémoire, permettant essentiellement au processeur de mapper des adresses physiques à des adresses logiques. Cependant, dans votre application, les choses semblent toujours plates, car le système d'exploitation a virtualisé la mémoire pour vous. Le noyau fonctionne en mode protégé, permettant aux applications de s'exécuter dans un modèle de mémoire plate. Voir aussi en.wikipedia.org/wiki/Protected_mode
Wim ten Brink
@Workshop ALex: C'est une technicité. Tous les systèmes d'exploitation modernes définissent tous les segments sur [0, FFFFFFFF]. Ça ne compte pas vraiment. Et si vous lisez la page liée, vous verrez que toutes les choses fantaisistes sont faites avec des pages, qui sont beaucoup plus fines que les segments.
MSalters
-4

Edit Ouais, c'est surtout faux. Il décrit quelque chose de complètement différent au cas où quelqu'un serait intéressé :)

Oui, le pointeur de pile pointe vers le haut de la pile (que ce soit le premier emplacement de pile vide ou le dernier plein dont je ne suis pas sûr). Le pointeur de base pointe vers l'emplacement de mémoire de l'instruction en cours d'exécution. C'est au niveau des opcodes - l'instruction la plus élémentaire que vous pouvez obtenir sur un ordinateur. Chaque opcode et ses paramètres sont stockés dans un emplacement mémoire. Une ligne C ou C ++ ou C # peut être traduite en un opcode, ou une séquence de deux ou plus selon sa complexité. Ceux-ci sont écrits dans la mémoire du programme de manière séquentielle et exécutés. Dans des circonstances normales, le pointeur de base est incrémenté d'une instruction. Pour le contrôle de programme (GOTO, IF, etc.), il peut être incrémenté plusieurs fois ou simplement remplacé par l'adresse de mémoire suivante.

Dans ce contexte, les fonctions sont stockées dans la mémoire programme à une certaine adresse. Lorsque la fonction est appelée, certaines informations sont poussées sur la pile qui permet au programme de retrouver son origine d'où la fonction a été appelée ainsi que les paramètres de la fonction, puis l'adresse de la fonction dans la mémoire du programme est poussée dans le pointeur de base. Au cycle d'horloge suivant, l'ordinateur commence à exécuter des instructions à partir de cette adresse mémoire. Puis, à un moment donné, il RETOURNE à l'emplacement de mémoire APRÈS l'instruction qui a appelé la fonction et continue à partir de là.

Stephen Friederichs
la source
J'ai un peu de mal à comprendre ce qu'est l'ebp. Si nous avons 10 lignes de code MASM, cela signifie qu'au fur et à mesure de l'exécution de ces lignes, ebp augmentera toujours?
dévoré elysium le
1
@Devoured - Non. Ce n'est pas vrai. eip va augmenter.
Michael
Vous voulez dire que ce que j'ai dit est vrai, mais pas pour EBP, mais pour IEP, c'est ça?
dévoré elysium le
2
Oui. EIP est le pointeur d'instruction et est implicitement modifié après l'exécution de chaque instruction.
Michael
2
Oooh ma mauvaise. Je pense à un pointeur différent. Je pense que je vais me laver le cerveau.
Stephen Friederichs, le
-8

esp signifie "Extended Stack Pointer" ..... ebp pour "Something Base Pointer" .... et eip pour "Something Instruction Pointer" ...... Le pointeur de pile pointe vers l'adresse de décalage du segment de pile . Le pointeur de base pointe vers l'adresse de décalage du segment supplémentaire. Le pointeur d'instruction pointe vers l'adresse de décalage du segment de code. Maintenant, à propos des segments ... ce sont de petites divisions de 64 Ko de la zone de mémoire du processeur ..... Ce processus est connu sous le nom de segmentation de la mémoire. J'espère que ce message a été utile.

Adarsha Kharel
la source
3
C'est une vieille question, cependant, sp signifie pointeur de pile, bp signifie pointeur de base et ip pour pointeur d'instruction. Le e au début de tout le monde dit simplement qu'il s'agit d'un pointeur 32 bits.
Hyden
1
La segmentation n'est pas pertinente ici.
BarbaraKwarc