¿Qué es exactamente el puntero base y el puntero de pila? ¿A qué apuntan?

Resuelto devoured elysium asked hace 15 años • 6 respuestas

Usando este ejemplo proveniente de Wikipedia, en el que DrawSquare()se llama DrawLine():

diagrama de pila con anotaciones

(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 espson en este contexto?

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


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

edit2: ¿Y cómo funciona eiptambié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 que ocupan 4 bytes cada uno. Entonces puedo ver que hay una brecha hInstancede var_44 bytes. ¿Qué son? Supongo que es la dirección del remitente, como se puede ver en el diagrama de Wikipedia.


(nota del editor: se eliminó una cita larga 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:

  • Parámetros push (hInstance, etc.)
  • Función de llamada, que envía la dirección del remitente
  • Empuje epp
  • Asignar espacio para los locales

Mi pregunta (¡la última, espero!) ahora es: ¿qué sucede exactamente desde el momento en que presento los argumentos de la función que quiero llamar hasta el final del prólogo? Quiero saber cómo evolucionan ebp, esp durante esos momentos (ya entendí cómo funciona el prólogo, solo quiero saber qué sucede después de que puse los argumentos en la pila y antes del prólogo).

devoured elysium avatar Sep 09 '09 01:09 devoured elysium
Aceptado

espes como usted dice que es, la parte superior de la pila.

ebpnormalmente se establece espal inicio 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 todas las llamadas a funciones. ebpen realidad apunta al puntero base del marco anterior, lo que permite caminar por la pila en un depurador y ver las variables locales de otros marcos 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 es posible que tenga un código similar (suponiendo que ambas variables locales tengan 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 marco o FPO que puede habilitar eliminará esto y lo utilizará ebpcomo otro registro y accederá a los locales directamente desde esp, pero esto dificulta un poco la depuración ya que el depurador ya no puede acceder directamente a los marcos de pila de llamadas a funciones anteriores.

EDITAR:

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

nShowCmd          = dword ptr +14h
hlpCmdLine        = dword ptr +10h
PrevInstance      = dword ptr +0Ch
hInstance         = dword ptr +08h
return address    = dword ptr +04h     <==
savedFramePointer = dword ptr +00h     <==
var_4             = dword ptr -04h
var_8             = dword ptr -08h
var_C             = dword ptr -0Ch

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

  • Insertar parámetros ( hInstance, PrevInstance, hlpCmdLine, nShowCmd)
  • Función de llamada, que envía la dirección del remitente
  • Empujarebp
  • Asignar espacio para los locales
Michael avatar Sep 08 '2009 18:09 Michael

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

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

Al ingresar a la función, el EBPvalor anterior se coloca en la pila y EBPse establece en el valor de ESP. Luego se ESPdisminuye (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 desplazamientos positivos desde EBP(porque fueron enviados antes de la llamada a la función), y las variables locales se ubican en desplazamientos negativosEBP desde (porque fueron asignados en la pila después de la entrada de la función). Es por eso que se EBPllama puntero de marco , porque apunta al centro del marco de llamada de función .

Al salir, todo lo que la función tiene que hacer es establecer ESPel valor de EBP(lo que desasigna las variables locales de la pila y expone la entrada EBPen la parte superior de la pila), luego extrae el EBPvalor anterior de la pila y luego la función devoluciones (escribiendo la dirección del remitente en EIP).

Al regresar a la función que llama, puede incrementarse ESPpara eliminar los argumentos de la función que insertó 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 avatar Sep 08 '2009 19:09 David R Tribble

Tienes razón. 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 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 de la pila quedan fuera de alcance. Para ello, vuelva a establecer 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 avatar Sep 08 '2009 18:09 Robert Cartaino

EDITAR: Para obtener una mejor descripción, consulte Desmontaje/funciones de x86 y marcos de pila en un WikiBook sobre el ensamblaje de x86. Intento agregar información que podría interesarle al usar Visual Studio.

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

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

Esta omisión del puntero de marco significa que el compilador no almacena el EBP antiguo en un lugar estándar y usa el registro EBP para otra cosa, por lo tanto, le resulta difícil encontrar el EIP que llama 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 el uso de /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 dbghlp.dll.

wigy avatar Sep 08 '2009 19:09 wigy

En primer lugar, el puntero de la pila apunta a la parte inferior de la pila, ya que las pilas x86 se construyen desde valores de dirección altos hasta valores de dirección más bajos. El puntero de la pila es el punto donde la próxima llamada a presionar (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 fotograma actual. ebp generalmente apunta a su dirección de remitente. ebp+4 apunta al primer parámetro de su función (o al valor this de un 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 del marco anterior.

jmucchiello avatar Sep 08 '2009 18:09 jmucchiello