[ USOS Y
ABUSOS DE LA PILA EN SESIONES DE DEPURACIÓN ]
Por Black Fenix
"La pila es un espacio de memoria reservado para la aplicación, en este lugar se almacenan los parámetros que se pasarán a las llamadas (CALLS) y las variables locales de estas. Su funcionamiento es muy simple, el último argumento que se almacena en la pila es el primero en salir, esto es lo que se llama una pila LIFO (Last In First Out). Básicamente tenemos dos instrucciones que se encargan de sacar y empujar parámetros en la pila: PUSH (EMPUJAR) y POP (SACAR), existe alguna más pero van siempre precedidos del prefijo PUSH o POP. Los parámetros que se suelen empujar a la pila suelen ser direcciones de memória (punteros) o valores inmediatos. se puede manejar la pila manualmente incrementando o decrementando los registros de 32 bits ESP (extended stack pointer) y EBP (extended stack base pointer), ESP siempre es un puntero a la cima de la pila (último parámetro empujado a la pila), y EBP es un puntero la la base de la pila (primer argumento que se empujo a la pila). EBP y ESP se combinan con el registro SS (stack segment para 16bits o stack selector en 32bits)."
_cdecl: Cuando se llama a una función declarada como _cdecl, esta función NO restaura el estado de la pila al regresar, y debemos realizar nosotros el reajuste manualmente. Veamos un ejemplo de función declarada como _cdecl:
void _cdecl
MyFunc( char c, short s, int i, double f )
{
}
Para llamar a esta función haremos:
MyFunc('x',12,8192,2.7138);
El código en ensamblador sería así:
Todas las funciones declaradas como _cdecl, estarán precedidas de un guión bajo "_", p. ej: MyFunc quedaria _MyFunc. Una vez dentro de la función declarada como _cdecl, encontraremos los parámetros en las siguientes posiciones:
Con este esquema nos hacemos una idea de donde están los parámetros pasados, así como la dirección de retorno, que siempre es una dato muy interesante, sobre todo para localizar la dirección de memória desde donde se llamó a la función declarada como _cdecl. Otro dato importante es saber que los registros ECX y EDX no son utilizados por la función, por lo que los valores que contengan resultan irrelevantes. Como puede apreciarse en la cima de la pila se guarda la dirección de retorno que ocupa 4bytes (un DWORD) y a partir de aquí los parámetros. Es importante saber que al final de una función declarada como _cdecl, siempre encontraremos un ret sin parámetros (digo siempre que NO haya sido programado en ensamblador). |
Veamos ahora el segundo tipo de declaración:
_stdcall y thiscall (solo C++) : Cuando se llama a una función declarada como _stdcall, esta función restaura el estado de la pila antes de regresar, y nosotros no deberemos realizar ningún reajuste. Veamos el código en C y ensamblador:
void _stdcall MyFuncStdCall( char c, short s, int i, double f );
LLamariamos a la rutina con:
MyFuncStdCall('x',12,8192,2.7138);
Si desensanblasemos el código encontrariamos:
Veamos ahora donde encontrariamos los parámetros:
Como podemos ver las posiciones de los parámetros son iguales que el tipo de llamada _cdecl, la única diferencia es que el registro ECX contiene el puntero a this, utilizado sólo en las funciones miembro de una clase de C++, el parámetro this es un puntero al objeto que llamó a su función miembro, sólo es válido para funciones miembro NO estáticas (static). El registro EDX no se utiliza y su contenido resulta irrelevante para la función. Es importante conocer que el modelo de llamada _stdcall es el utilizado por todas las funciones del API de Windows, y probablemente sea el más común de todos junto con el _cdecl. Una utilidad muy práctica es cuando ponemos un BPX sobre una función del API y queremos saber desde donde se llamó pero sin trazar el código hasta regresar, para obtener la dirección del código que llamó a la función del API, usando SoftIce: > dd *esp el primer dword que aparezca en la ventana de datos es la dirección que buscabamos, si ahora tecleamos > what *esp nos mostrará el módulo o ejecutable que llamó a esta función. Yo utilizo una macro que me muestra la dirección de retorno y 4 los 4 primeros parámetros que se le pasan a la función, la definición de la macro en SoftIce es como sigue: > macro getargs = "?*(esp);?*(esp+4);?*(esp+8);?*(esp+c);?*(esp+10);?*(esp+14)" Ahora solo tenemos que teclear getparams para que la macro se ejecute. Va de fabula para hacer seguimientos de pila.Que os parece? Es o no es práctico? |
Veamos ahora el tercer tipo de declaración:
_fastcall: Cuando se llama a una función declarada como _fastcall, esta función pasa los parámetros a la función en los registros internos del procesador siempre que sea posible. Veamos la implementación en C.
void _fastcall
MyFuncFastCall( char c, short s, int i, double f )
{
}
LLamariamos a la rutina con:
MyFuncFastCall('x',12,8192,2.7138);
Si desensamblaramos el código tendriamos:
Veamos ahora donde encontrariamos los parámetros:
Como podemos ver las posiciones de los parámetros varian ya que se han utilizado los registros ECX y EDX para almacenar dos de los parámetros utilizados por la función. |
Cabe mencionar también que según el compilador el prólogo de entrada y el epílogo de salida de una llamada puede variar, pero siempre se mantendrá la lógica básica de cada modelo de llamada.Aquí teneis el prólogo de entrada y el epílogo de salida de una función de 32bits típica en C:
push ebp
; Salva EBP
mov ebp, esp ; Establece la base de la pila en la cima,
esto hará que los parámetros pasados sean accesibles usando EBP
sub esp, localbytes ; Reserva espacio para las variables
locales
push <registers> ; Salva los registros oportunos
La variable localbytes representa el número de bytes necesarios en la pila para almacenar las variables locales, y la variable registers representa una lista de los registros que se deben salvar en la pila. Después de empujar los registros, ya se pueden añadir más datos a la pila. Notese que se copia el puntero a la cima de la pila en EBP (base de la pila), esto hará que los parámetros sean accesible mediante el direccionamiento de EBP más el desplazamiento del parámetro que necesitemos saber. Por último tendremos el epílogo de la función:
pop <registers>
; Restaura registros
mov esp, ebp ; Restaura el puntero a la cima de la pila
pop ebp ; Restaura la base de la pila
ret ; y retorna
La pila siempre crece hacia abajo (desde direcciones de memoria altas a bajas), ya que la instrucción PUSH decrementa el ESP y POP lo incrementa. El puntero base (EBP) apunta al valor empujado de EBP. El area de variables locales comienza en EBP-2. Para acceder a cualquier variable local, calcula el desplazamiento desde EBP sustrayendo el valor apropiado de EBP.
Espero que con esto os sea más fácil trazar el código, un buen trazado de la pila puede suponer un gran ahorro de horas de trazado. Pues nada, poner a punto el SoftIce, preparad vuestros mejores BPM y BPX y a la caza.
You are inside Reversed Minds pages. por
Mr. Silver
/ WKT! |