web-dev-qa-db-fra.com

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

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

alt text

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

Quelqu'un pourrait-il m'expliquer ce que ebp et esp sont 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 la base au début de la fonction en cours? Ou quoi?


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

edit2: Et comment fonctionne eip également?

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 dwords, prenant ainsi 4 octets chacun. Donc, je peux voir qu'il y a un espace 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 la photo de wikipedia?


(note de l'éditeur: suppression d'une longue citation de la réponse de Michael, qui n'appartient pas à la question, mais une question complémentaire a été modifiée):

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 (la dernière, j'espère!) Est la suivante: qu'est-ce qui se passe exactement à partir du moment où je passe les arguments de la fonction que je souhaite appeler jusqu'à la fin du prologue? Je veux savoir comment évoluent les ebp, en particulier pendant ces moments (j'ai déjà compris comment fonctionne le prologue, je veux juste savoir ce qui se passe après que j'ai poussé les arguments sur la pile et avant le prologue).

207
devoured elysium

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

ebp est généralement réglé sur esp au début de la fonction. On accède aux paramètres de fonction et aux variables locales en ajoutant et en soustrayant, respectivement, un décalage constant par rapport à ebp. Toutes les conventions d'appel x86 définissent ebp comme étant préservée lors d'appels de fonction. ebp lui-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 d'afficher les variables locales des autres images pour fonctionner.

La plupart des prologies fonctionnelles ressemblent à quelque chose comme:

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 aurez peut-être un code comme (en supposant que les deux variables locales ont 4 octets)

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

FPO ou omission du pointeur de la trame l’optimisation que vous pouvez activer l’éliminera en fait et utilisera ebp comme autre registre et permettra d’accéder aux locales directement à partir de esp, mais cela rendra un peu plus le débogage difficile car le débogueur ne peut plus accéder directement aux cadres de pile des appels de fonction antérieurs.

MODIFIER:

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

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 envoie l'adresse de retour
  • Appuyez sur ebp
  • Allouer de l'espace aux locaux
216
Michael

ESP est le pointeur actuel de la pile, qui changera chaque fois qu'un mot ou une adresse sont insérés ou affichés dans 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 plutôt que d'utiliser le ESP directement.

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). . La fonction est ensuite 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 dans la pile et EBP est défini sur la valeur ESP. Ensuite, le ESP est décrémenté (car la pile s'agrandit vers le bas en mémoire) pour allouer de l'espace aux variables locales et aux temporaires de la fonction. À partir de ce moment-là, lors de l'exécution de la fonction, les arguments de la fonction sont situés sur la pile aux décalages positifs de EBP (car ils ont été poussés avant l'appel de la fonction) et les variables locales sont situés aux décalages négatifs de 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 cadre , car il pointe vers le centre de la fonction cadre d'appel .

À la sortie, il suffit que ESP soit défini sur EBP (ce qui libère les variables locales de la pile et expose l'entrée EBP en haut de la pile), puis supprime l'ancien EBP de la pile, puis la fonction retourne (en saisissant l'adresse de retour dans EIP).

Lors du retour à la fonction appelante, il peut alors incrémenter ESP afin de supprimer les arguments de la fonction qu'il a poussés sur la pile juste avant d'appeler l'autre fonction. À ce stade, la pile est de nouveau dans l'état où elle se trouvait avant d'appeler la fonction appelée.

80
David R Tribble

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

Lorsque vous appelez une fonction, toute variable locale sera stockée dans la pile et le pointeur de la pile sera incrémenté. Lorsque vous revenez de la fonction, toutes les variables locales de la pile sont hors de portée. Vous faites cela en redéfinissant le pointeur de pile sur le pointeur de base (qui était le sommet "précédent" avant l'appel de la fonction).

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

15
Robert Cartaino

EDIT: Pour une meilleure description, voir x86 Disassembly/Functions and Stack Frames dans un WikiBook à propos de x86 Assembly. J'essaie d'ajouter des informations qui pourraient vous intéresser si vous utilisez Visual Studio.

Stocker l'appelant EBP en tant que première variable locale s'appelle un cadre de pile standard. Il peut être utilisé pour presque toutes les conventions d'appel sous Windows. Il existe des différences selon que l'appelant ou l'appelant libère les paramètres passés et quels paramètres sont passés dans les registres, mais ils sont orthogonaux au problème standard du cadre de pile.

En parlant de programmes Windows, vous pouvez probablement utiliser Visual Studio pour compiler votre code C++. Sachez que Microsoft utilise une optimisation appelée Frame Pointer Omission, qui rend pratiquement impossible de parcourir la pile sans utiliser la bibliothèque dbghlp et le fichier PDB de 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'appelant EIP sans connaître l'espace requis par les variables locales pour une fonction donnée. Bien entendu, Microsoft fournit une API qui vous permet de procéder à des analyses d'empilement 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 vous associez probablement au runtime C ou C++, qui utilise FPO dans la configuration Release, vous aurez donc du mal à faire des marches de pile sans dbghlp.dll.

7
wigy

Tout d'abord, le pointeur de pile pointe vers le bas de la pile, car les piles x86 construisent à partir de valeurs d'adresse élevées en valeurs d'adresses inférieures. Le pointeur de pile est le point où le prochain appel à Push (ou call) 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 en cours. ebp indique généralement votre adresse de retour. ebp + 4 pointe sur le premier paramètre de votre fonction (ou sur la valeur this d'une méthode de classe). ebp-4 pointe sur la première variable locale de votre fonction, généralement l'ancienne valeur de ebp, afin que vous puissiez restaurer le pointeur d'image précédent.

6
jmucchiello

Cela fait longtemps que je fais de la programmation en assembleur, mais ce lien pourrait être utile ...

Le processeur a une collection de registres qui sont utilisés pour stocker des données. Certaines d'entre elles sont des valeurs directes, tandis que d'autres pointent vers une zone de la RAM. Les registres ont tendance à être utilisés pour certaines actions spécifiques et chaque opérande dans Assembly nécessite 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, une série de données sera d'abord sauvegardée dans la pile, suivie de l'adresse de retour afin que le système sache où retourner une fois que le retour est demandé. Le pointeur de la pile indiquera l'emplacement suivant où les nouvelles données peuvent être placées dans la pile, où elles resteront jusqu'à ce qu'elles soient à nouveau affichées.

Les registres de base ou les registres de segments pointent simplement sur l'espace d'adressage d'une grande quantité de données. Combiné à 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. Les pointeurs de base correspondants indiquent la base de blocs de données.

N'oubliez pas que Assembly est très spécifique au processeur. La page à laquelle je suis lié fournit des informations sur différents types de CPU.

1
Wim ten Brink