¿Qué es exactamente el puntero base y el puntero de pila? ¿A qué apuntan?
Usando este ejemplo proveniente de Wikipedia, en el que DrawSquare()
se llama DrawLine()
:
(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é ebp
y esp
son 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 eip
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 que ocupan 4 bytes cada uno. Entonces puedo ver que hay una brecha hInstance
de var_4
4 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).
esp
es como usted dice que es, la parte superior de la pila.
ebp
normalmente se establece esp
al 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 ebp
como preservadas en todas las llamadas a funciones. ebp
en 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á ebp
como 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
- Empujar
ebp
- Asignar espacio para los locales
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 EBP
valor anterior se coloca en la pila y EBP
se establece en el valor de ESP
. Luego se 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 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 EBP
llama 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 ESP
el valor de EBP
(lo que desasigna las variables locales de la pila y expone la entrada EBP
en la parte superior de la pila), luego extrae el EBP
valor 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 ESP
para 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.
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.
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.
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.