¿Cuál es exactamente el puntero base y el puntero de pila? ¿A qué apuntan?

225

Usando este ejemplo proveniente de wikipedia, en el que DrawSquare () llama a DrawLine (),

texto alternativo

(Tenga en cuenta que este diagrama tiene direcciones altas en la parte inferior y direcciones bajas en la parte superior).

¿Alguien podría explicarme qué ebpy espen este contexto?

Por lo que veo, diría que el puntero de la pila apunta siempre a la parte superior de la pila, y el puntero de base al comienzo de la función actual. ¿O que?


editar: me refiero a esto en el contexto de los programas de Windows

edit2: ¿Y cómo eipfunciona también?

edit3: Tengo el siguiente código 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

Todos ellos parecen ser dwords, por lo tanto toman 4 bytes cada uno. Entonces puedo ver que hay una brecha entre hInstance y var_4 de 4 bytes. ¿Qué son? Supongo que es la dirección de retorno, como se puede ver en la imagen de Wikipedia.


(Nota del editor: eliminó una larga cita de la respuesta de Michael, que no pertenece a la pregunta, pero se editó una pregunta de seguimiento):

Esto se debe a que el flujo de la llamada a la función es:

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

Mi pregunta (por último, ¡espero!) Ahora es, ¿qué es exactamente lo que sucede desde el instante en que hago estallar los argumentos de la función que quiero invocar hasta el final del prólogo? Quiero saber cómo evoluciona el ebp, especialmente durante esos momentos (ya entendí cómo funciona el prólogo, solo quiero saber qué está sucediendo después de haber introducido los argumentos en la pila y antes del prólogo).

elysium devorado
fuente
23
Una cosa importante a tener en cuenta es que la pila crece "hacia abajo" en la memoria. Esto significa que para mover el puntero de la pila hacia arriba, disminuye su valor.
BS
44
Una pista para diferenciar lo que están haciendo EBP / ESP y EIP: EBP y ESP manejan datos, mientras que EIP maneja código.
mmmmmmmm
2
En su gráfico, ebp (generalmente) es el "puntero de cuadro", especialmente el "puntero de pila". Esto permite acceder a los locales a través de [ebp-x] y los parámetros de la pila a través de [ebp + x] de forma coherente, independiente del puntero de la pila (que con frecuencia cambia dentro de una función). El direccionamiento se puede hacer a través de ESP, liberando EBP para otras operaciones, pero de esa manera, los depuradores no pueden decir la pila de llamadas o los valores de los locales.
Peterter
44
@Ben. No necesariamente. Algunos compiladores ponen marcos de pila en el montón. El concepto de pila que crece es solo eso, un concepto que lo hace fácil de entender. La implementación de la pila puede ser cualquier cosa (el uso de fragmentos aleatorios del montón hace que los hacks que sobrescriben partes de la pila sean mucho más difíciles ya que no son tan deterministas).
Martin York
1
en dos palabras: el puntero de pila permite que funcionen las operaciones push / pop (por lo que push and pop sabe dónde colocar / obtener datos). El puntero base permite que el código haga referencia de forma independiente a los datos que se han insertado previamente en la pila.
tigrou

Respuestas:

229

esp es como tú dices, la parte superior de la pila.

ebpgeneralmente se establece al espinicio de la función. Se accede a los parámetros de función y a las variables locales sumando y restando, respectivamente, un desplazamiento constante de ebp. Todas las convenciones de llamadas x86 se definen ebpcomo preservadas en las llamadas a funciones. ebpen sí mismo apunta al puntero base del cuadro anterior, lo que permite que la pila recorra un depurador y vea otras variables locales de cuadros para que funcionen.

La mayoría de los prólogos de funciones se parecen a:

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.

Luego, más adelante en la función, puede tener un código similar (suponiendo que ambas variables locales sean de 4 bytes)

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

La optimización de omisión de puntero de cuadro o FPO que puede habilitar en realidad eliminará esto y lo usará ebpcomo otro registro y accederá a los locales directamente desde fuera esp, pero esto hace que la depuración sea un poco más difícil ya que el depurador ya no puede acceder directamente a los cuadros de la pila de llamadas a funciones anteriores.

EDITAR:

Para su pregunta actualizada, las dos entradas que faltan en la pila son:

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

Esto se debe a que el flujo de la llamada a la función es:

  • Parámetros de inserción ( hInstance, etc.)
  • Función de llamada, que empuja la dirección de retorno
  • empujar ebp
  • Asignar espacio para locales
Miguel
fuente
1
¡Gracias por la explicación! Pero ahora estoy un poco confundido. Supongamos que llamo a una función y estoy en la primera línea de su prólogo, aún sin haber ejecutado una sola línea desde ella. En ese punto, ¿cuál es el valor de ebp? ¿La pila tiene algo en ese punto además de los argumentos empujados? ¡Gracias!
devorado elysium
3
El EBP no cambia mágicamente, por lo que hasta que haya establecido un nuevo EBP para su función, seguirá teniendo el valor de las personas que llaman. Y además de los argumentos, la pila también tendrá el antiguo EIP (dirección de retorno)
MSalters
3
Buena respuesta. Aunque no puede estar completo sin mencionar lo que hay en el epílogo: instrucciones de "dejar" y "retirarse".
Calmarius
2
Creo que esta imagen ayudará a aclarar algunas cosas sobre el flujo. También tenga en cuenta que la pila crece hacia abajo. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre
¿Soy yo o faltan todos los signos menos del fragmento de código anterior?
BarbaraKwarc
96

ESP es el puntero actual de la pila, que cambiará cada vez que una palabra o dirección se empuja o se saca de la pila. EBP es una forma más conveniente para que el compilador realice un seguimiento de los parámetros y las variables locales de una función que usar el ESP directamente.

Generalmente (y esto puede variar de un compilador a otro), todos los argumentos de una función que se llama son empujados a la pila por la función de llamada (generalmente en el orden inverso al declarado en el prototipo de la función, pero esto varía) . Luego se llama a la función, que empuja la dirección de retorno (EIP) a la pila.

Al ingresar a la función, el antiguo valor de EBP se inserta en la pila y EBP se establece en el valor de ESP. Luego, el ESP disminuye (porque la pila crece hacia abajo en la memoria) para asignar espacio para las variables locales y temporales de la función. A partir de ese momento, durante la ejecución de la función, los argumentos de la función se ubican en la pila en compensaciones positivas de EBP (porque fueron empujados antes de la llamada a la función), y las variables locales se ubican en compensaciones negativas de EBP (porque se asignaron en la pila después de la entrada de la función). Es por eso que el EBP se llama puntero de cuadro , porque apunta al centro del cuadro de llamada de función .

Al salir, todo lo que la función tiene que hacer es establecer ESP en el valor de EBP (que desasigna las variables locales de la pila y expone la entrada de EBP en la parte superior de la pila), luego extrae el antiguo valor de EBP de la pila, y luego la función regresa (ingresando la dirección de retorno en EIP).

Al regresar a la función de llamada, puede incrementar ESP para eliminar los argumentos de la función que introdujo en la pila justo antes de llamar a la otra función. En este punto, la pila vuelve al mismo estado que tenía antes de invocar la función llamada.

David R Tribble
fuente
15

Lo tienes bien El puntero de la pila apunta al elemento superior de la pila y el puntero base apunta a la parte superior "anterior" de la pila antes de que se llamara a la función.

Cuando llama a una función, cualquier variable local se almacenará en la pila y el puntero de la pila se incrementará. Cuando regresa de la función, todas las variables locales en la pila quedan fuera de alcance. Para ello, vuelva a configurar el puntero de la pila en el puntero base (que era la parte superior "anterior" antes de la llamada a la función).

Hacer la asignación de memoria de esta manera es muy , muy rápido y eficiente.

Robert Cartaino
fuente
14
@Robert: Cuando dice la parte superior "anterior" de la pila antes de que se llamara a la función, ignora los dos parámetros, que se insertan en la pila justo antes de llamar a la función y al EIP del llamante. Esto podría confundir a los lectores. Digamos que en un marco de pila estándar, EBP apunta al mismo lugar donde ESP señaló justo después de ingresar a la función.
wigy
7

EDITAR: Para una mejor descripción, vea Desmontaje / Funciones x86 y Marcos de pila en un WikiBook sobre ensamblaje x86. Intento agregar información que pueda interesarle usar Visual Studio.

El almacenamiento del EBP de la persona que llama como la primera variable local se denomina marco de pila estándar, y esto se puede usar para casi todas las convenciones de llamadas en Windows. Existen diferencias si la persona que llama o la persona que llama desasigna los parámetros pasados ​​y qué parámetros se pasan en los registros, pero estos son ortogonales al problema estándar del marco de la pila.

Hablando de programas de Windows, es probable que use Visual Studio para compilar su código C ++. Tenga en cuenta que Microsoft usa una optimización llamada Frame Pointer Omission, que hace que sea casi imposible recorrer la pila sin usar la biblioteca dbghlp y el archivo PDB para el ejecutable.

Esta omisión de puntero de trama significa que el compilador no almacena el antiguo EBP en un lugar estándar y utiliza el registro EBP para otra cosa, por lo tanto, le resulta difícil encontrar el EIP del llamador sin saber cuánto espacio necesitan las variables locales para una función determinada. Por supuesto, Microsoft proporciona una API que le permite realizar recorridos de pila incluso en este caso, pero buscar la base de datos de la tabla de símbolos en archivos PDB lleva demasiado tiempo para algunos casos de uso.

Para evitar FPO en sus unidades de compilación, debe evitar usar / O2 o agregar explícitamente / Oy- a los indicadores de compilación de C ++ en sus proyectos. Probablemente se vincule con el tiempo de ejecución de C o C ++, que usa FPO en la configuración de lanzamiento, por lo que tendrá dificultades para realizar recorridos de pila sin el dbghlp.dll.

wigy
fuente
No entiendo cómo se almacena EIP en la pila. ¿No debería ser un registro? ¿Cómo puede un registro estar en la pila? ¡Gracias!
devorado elysium
La instrucción CALL empuja a la persona que llama EIP a la pila. La instrucción RET solo busca la parte superior de la pila y la coloca en el EIP. Si tiene desbordamientos de búfer, este hecho podría usarse para saltar al código de usuario desde un hilo privilegiado.
wigy
@devouredelysium El contenido (o valor ) del registro EIP se coloca (o copia) en la pila, no en el registro en sí.
BarbaraKwarc
@BarbaraKwarc Gracias por el valor de entrada -able. No pude ver lo que faltaba en el OP de mi respuesta. De hecho, los registros permanecen donde están, solo su valor se envía a la RAM desde la CPU. En el modo amd64, esto se vuelve un poco más complejo, pero deje eso a otra pregunta.
Wigy
¿Qué hay de eso amd64? Soy curioso.
BarbaraKwarc
6

En primer lugar, el puntero de la pila apunta a la parte inferior de la pila, ya que las pilas x86 se acumulan desde valores de dirección altos a valores de dirección más bajos. El puntero de la pila es el punto donde la próxima llamada para empujar (o llamar) colocará el siguiente valor. Su operación es equivalente a la declaración 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

El puntero base está en la parte superior del marco actual. ebp generalmente apunta a su dirección de devolución. ebp + 4 señala el primer parámetro de su función (o el valor de este método de clase). ebp-4 apunta a la primera variable local de su función, generalmente el valor anterior de ebp para que pueda restaurar el puntero de fotograma anterior.

jmucchiello
fuente
2
No, ESP no apunta al fondo de la pila. El esquema de direccionamiento de memoria no tiene nada que ver con eso. No importa si la pila crece a direcciones más bajas o más altas. La "parte superior" de la pila siempre es donde se empujará el siguiente valor (se colocará sobre la parte superior de la pila) o, en otras arquitecturas, donde se colocó el último valor empujado y dónde se encuentra actualmente. Por lo tanto, ESP siempre apunta a la parte superior de la pila.
BarbaraKwarc
1
La parte inferior o base de la pila, por otro lado, es donde se ha puesto el primer valor (o el más antiguo ) y luego se cubre con los valores más recientes. De ahí proviene el nombre "puntero base" para EBP: se suponía que apuntaba a la base (o al fondo) de la pila local actual de una subrutina.
BarbaraKwarc
Barbara, en el Intel x86, la pila está al revés. La parte superior de la pila contiene el primer elemento empujado sobre la pila y cada elemento posterior se empuja POR DEBAJO del elemento superior. La parte inferior de la pila es donde se colocan los nuevos elementos. Los programas se colocan en la memoria a partir de 1k y crecen hasta el infinito. La pila comienza en el infinito, realísticamente max mem menos ROM, y crece hacia 0. ESP apunta a una dirección cuyo valor es menor que la primera dirección introducida.
jmucchiello
1

Hace mucho tiempo que no hago programación de ensamblaje, pero este enlace puede ser útil ...

El procesador tiene una colección de registros que se utilizan para almacenar datos. Algunos de estos son valores directos, mientras que otros apuntan a un área dentro de la RAM. Los registros tienden a usarse para ciertas acciones específicas y cada operando en el ensamblaje requerirá una cierta cantidad de datos en registros específicos.

El puntero de la pila se usa principalmente cuando llama a otros procedimientos. Con los compiladores modernos, un montón de datos se volcará primero en la pila, seguido de la dirección de retorno para que el sistema sepa dónde regresar una vez que se le indique que regrese. El puntero de la pila apuntará a la siguiente ubicación donde se pueden enviar nuevos datos a la pila, donde permanecerán hasta que vuelvan a aparecer.

Los registros base o los registros de segmento solo apuntan al espacio de direcciones de una gran cantidad de datos. Combinado con un segundo registrador, el puntero Base dividirá la memoria en grandes bloques, mientras que el segundo registro apuntará a un elemento dentro de este bloque. Los punteros de base apuntan a la base de bloques de datos.

Tenga en cuenta que el ensamblaje es muy específico de la CPU. La página a la que he vinculado proporciona información sobre los diferentes tipos de CPU.

Wim ten Brink
fuente
Los registros de segmento están separados en x86: son gs, cs, ss y, a menos que esté escribiendo software de administración de memoria, nunca los toca.
Michael
ds también es un registro de segmento y en los días de MS-DOS y código de 16 bits, definitivamente necesitaba cambiar estos registros de segmento ocasionalmente, ya que nunca podrían apuntar a más de 64 KB de RAM. Sin embargo, DOS podía acceder a la memoria de hasta 1 MB porque usaba punteros de dirección de 20 bits. Más tarde obtuvimos sistemas de 32 bits, algunos con registros de direcciones de 36 bits y ahora registros de 64 bits. Así que hoy en día ya no necesitará cambiar más estos registros de segmento.
Wim ten Brink
Ningún sistema operativo moderno utiliza 386 segmentos
Ana Betts
@Paul: ¡ERROR! ¡INCORRECTO! ¡INCORRECTO! Los segmentos de 16 bits se reemplazan por segmentos de 32 bits. En modo protegido, esto permite la virtualización de la memoria, básicamente permitiendo que el procesador asigne direcciones físicas a las lógicas. Sin embargo, dentro de su aplicación, las cosas todavía parecen ser planas, ya que el sistema operativo ha virtualizado la memoria para usted. El kernel funciona en modo protegido, lo que permite que las aplicaciones se ejecuten en un modelo de memoria plana. Ver también en.wikipedia.org/wiki/Protected_mode
Wim ten Brink
@ Workshop ALex: Eso es un tecnicismo. Todos los sistemas operativos modernos establecen todos los segmentos en [0, FFFFFFFF]. Eso realmente no cuenta. Y si lees la página vinculada, verás que todo lo elegante se hace con páginas, que son mucho más finas que los segmentos.
MSalters
-4

Editar Sí, esto está muy mal. Describe algo completamente diferente en caso de que alguien esté interesado :)

Sí, el puntero de la pila apunta a la parte superior de la pila (ya sea que sea la primera ubicación de pila vacía o la última llena, no estoy seguro). El puntero base apunta a la ubicación de la memoria de la instrucción que se está ejecutando. Esto está en el nivel de los códigos de operación: la instrucción más básica que puede obtener en una computadora. Cada código de operación y sus parámetros se almacenan en una ubicación de memoria. Una línea C o C ++ o C # podría traducirse a un código de operación, o una secuencia de dos o más, dependiendo de lo complejo que sea. Estos se escriben en la memoria del programa secuencialmente y se ejecutan. En circunstancias normales, el puntero base se incrementa una instrucción. Para el control del programa (GOTO, IF, etc.) puede incrementarse varias veces o simplemente reemplazarse con la siguiente dirección de memoria.

En este contexto, las funciones se almacenan en la memoria del programa en una determinada dirección. Cuando se llama a la función, cierta información se inserta en la pila que permite que el programa encuentre su lugar donde se llamó la función, así como los parámetros de la función, luego la dirección de la función en la memoria del programa se inserta en el puntero base. En el siguiente ciclo de reloj, la computadora comienza a ejecutar instrucciones desde esa dirección de memoria. Luego, en algún momento, VOLVERÁ a la ubicación de la memoria DESPUÉS de la instrucción que llamó a la función y continuará desde allí.

Stephen Friederichs
fuente
Tengo problemas para entender qué es el ebp. Si tenemos 10 líneas de código MASM, ¿eso significa que a medida que bajemos corriendo esas líneas, ebp siempre aumentará?
devorado elysium
1
@Devoured - No. Eso no es cierto. EIP se incrementará.
Michael
¿Quiere decir que lo que dije es correcto pero no para EBP, sino para IEP, es eso?
devorado elysium
2
Si. EIP es el puntero de la instrucción y se modifica implícitamente después de ejecutar cada instrucción.
Michael
2
Oooh mi mal Estoy pensando en un puntero diferente. Creo que iré a lavarme el cerebro.
Stephen Friederichs
-8

esp significa "puntero de pila extendido" ..... ebp para "puntero de base de algo" ... y eip para "puntero de instrucción de algo" ...... El puntero de pila apunta a la dirección de desplazamiento del segmento de pila . El puntero base apunta a la dirección de desplazamiento del segmento adicional. El puntero de instrucción apunta a la dirección de desplazamiento del segmento de código. Ahora, sobre los segmentos ... son pequeñas divisiones de 64 KB del área de memoria de los procesadores ... Este proceso se conoce como Segmentación de memoria. Espero que esta publicación haya sido útil.

Adarsha Kharel
fuente
3
Esta es una vieja pregunta, sin embargo, sp significa puntero de pila, bp significa puntero base e ip para puntero de instrucción. La e al principio de todos solo dice que es un puntero de 32 bits.
Hyden
1
La segmentación es irrelevante aquí.
BarbaraKwarc