¿Sabemos lo qué hacemos?

El día a día de muchos de nosotros es ponernos delante de nuestro editor de código y tirar líneas hasta conseguir que el código que desarrollamos cumpla los objetivos. Durante este proceso nos surgen problemas, el compilador se nos queja, ¡uff! la cosa no pinta bien.
Algunas veces por descuidos y otras veces porque no sabemos muy bien como solucionar un problema y vamos probando código hasta que el compilador se lo traga y el resultado es el correcto.

Llegados a este punto nos damos por satisfechos y seguimos desarrollando pero ahí surgen algunas preguntas:

¿Sabemos qué hace el compilador cuando aportamos una solución a nuestro problema?
¿Es esta la mejor solución posible?
¿Nos pueden surgir otros problemas partiendo de esta solución?

Todo esto viene a cuento porque el otro día al ejecutar un código en modo “debug” me encontré el siguiente mensaje de error del compilador (Visual Studio):

Mirando concienzudamente el código el error se producía en la siguiente declaración de puntero a función del API Win32:

DWORD (*SendARP) (IPAddr, IPAddr, DWORD*, DWORD*);

Bien, se estaba llamando a una función del API de Win32, en principio no había ningún problema, entonces, ¿dónde estaba el fallo?

El problema estaba en lo que en el argot nuestro se llama “calling convention” (convención de llamada), es decir, el modo en cómo se utiliza la pila cuándo llamamos a una función.

Para utilizar las funciones del API de Win32 se utiliza la “calling convention” stdcall, en nuestro caso, si nos fijamos, estamos utilizando un puntero a la función “SendARP” pero con “calling convention” por defecto cdecl. He aquí el problema. ¿Cuál es la diferencia? Con cdecl quien llama a la función es el encargado de “limpiar” la pila recogiendo los parámetros utilizados, en cambio, con stdcall el encargado de esta tarea es la propia función llamada. Total, en nuestro caso, en lugar de limpiar la pila una sola vez, se está limpiando la pila en 2 ocasiones, una nosotros desde nuestra función y otra internamente la propia función del API Win32 por lo que el compilador nos avisa de la incoherencia existente al final con el puntero de pila.

Dicho esto, logramos corregir el error cambiando la declaración del puntero de la siguiente forma:

DWORD (WINAPI *PSendARP) (IPAddr, IPAddr, DWORD*, DWORD*);

Nota: En el fichero de cabecera windef.h tenemos la siguiente macro “#define WINAPI __stdcall”

Podemos apreciar mejor la diferencia si observamos el código en ensamblador:

Volcado utilizando tipo cdecl:

004C6284 mov esi,esp
004C6286 lea eax,[mac_len] 004C6289 push eax
004C628A lea ecx,[mac_addr] 004C628D push ecx
004C628E push 0
004C6290 mov edx,dword ptr [ebp+8] 004C6293 push edx
004C6294 call dword ptr [SendARP]

004C6297 add esp,10h ——–> aquí da el fallo el compilador
004C629A cmp esi,esp
004C629C call @ILT+11125(__RTC_CheckEsp) (49FB7Ah)


Volcado utilizando tipo stdcall:

004C6284 mov esi,esp
004C6286 lea eax,[mac_len] 004C6289 push eax
004C628A lea ecx,[mac_addr] 004C628D push ecx
004C628E push 0
004C6290 mov edx,dword ptr [ebp+8] 004C6293 push edx
004C6294 call dword ptr [SendARP] 004C629A cmp esi,esp
004C629C call @ILT+11125(__RTC_CheckEsp) (49FB7Ah)

Como vemos, los códigos son idénticos excepto que cdecl incrementa el puntero de pila una vez que se ha llamado a la función del API.

En resumen, es necesario ser un poco curiosos, sacar conclusiones y “porqués” de las cosas que nos suceden y las soluciones que les damos.

Alonso Candado Sánchez
S21sec Labs


Recommended Posts
Showing 4 comments
  • Mario
    Responder

    Hehe, ese error es típico, a mi me pasó algo parecido utilizando una librería estática para poner hooks, al definir mal la convención de llamada de una de las funciones que hookeaba, la pila se quedaba desajustada y acababa cascando en una función que no tenía nada que ver.. El problema quedó claro una vez depuré y desensamblé con IDA. Este tipo de problemas suelen dar muchos quebraderos de cabeza, sobre todo si no estás acostumbrado a ir a mirar a tan bajo nivel la raiz del problema 🙂

  • Anónimo
    Responder

    Hola suelo leer los articulos de este Blog. No es mi intención criticar vuestro trabajo, simplemente quería completar
    la información mostrada en este artículo que por descuido pienso que es algo errónea y no sólo hay que procurar saber lo que hacemos, si no que también lo que escribimos.

    La convención de llamada realmente indica al compilador el orden del paso de parámetros a la función. En este caso,STDCALL es de derecha-a-izquierda y además la llamada es la responsable de restaurar el estado de la pila
    (como ya se ha comentado en el artículo original). Las API de win32 utilizan exclusivamente el modelo STDCALL (excepto para la función wsprintf(), siempre hay alguna excepción 🙂 ).

    Es decir, con STDCALL se apilan primero los parámetros de más a la derecha hacia la izquierda miestras que CDECL pasa los parámetros de izquierda-a-derecha.

    Por lo tanto el código en ensamblador expuesto en el artículo es algo erroneo en una de las llamadas ya que el orden de paso de parámetros es el mismo en las 2 y tendría que ser diferente.

    un saludo y gracias por vuestro trabajo

  • Alonso
    Responder

    Hola “anónimo”, está bien que expreses tu opinión sobre lo que leas y que contribuyas a corregir algo si crees que es incorrecto pero en este caso no es así, más concretamente:

    En tu comentario dices que en stdcall se pasan los parámetros de
    derecha a izquierda y en cdecl de izquieda a derecha. Esta apreciación hecha por tu parte es TOTALMENTE INCORRECTA ya que los dos lo pasan en el mismo orden, esto es, de DERECHA A IZQUIERDA.

    Esto no lo digo yo, está documentado como puedes comprobar en el siguiente enlace:
    https://blogs.msdn.com/oldnewthing/
    archive/2004/01/08/48616.aspx

    Pongo un extracto literal de dicho enlace:

    C (__cdecl)
    The same constraints apply to the 32-bit world as in the 16-bit
    world. The parameters are pushed from right to left (so that the
    first parameter is nearest to top-of-stack), and the caller cleans the parameters. Function names are decorated by a leading underscore.

    __stdcall
    This is the calling convention used for Win32, with exceptions for
    variadic functions (which necessarily use __cdecl) and a very few functions that use __fastcall. Parameters are pushed from right to
    left [corrected 10:18am] and the callee cleans the stack. Function
    names are decorated by a leading underscore and a trailing @-sign
    followed by the number of bytes of parameters taken by the function.


    Por lo tanto todo lo dicho en el blog ES CORRECTO, además, para
    más INRI, el código en ensamblador que se puede ver ES UNA CAPTURA
    DE PANTALLA DEL CÓDIGO ASM GENERADO POR EL COMPILADOR, particularmente no creo que Visual Studio 2005 genere código ensamblador incorrecto, desconozco tu opinión al respecto.

    Por si el enlace anterior no te hubiera parecido suficientemente
    “confiable” aquí tienes más bibliografia al respecto:

    MSDN:
    C++ Language Reference
    __stdcall
    https://msdn2.microsoft.com/en-us/
    library/zxk0tw93(VS.71).aspx

    C++ Language Reference
    __cdecl
    https://msdn2.microsoft.com/en-us/
    library/zkwh89ks(VS.71).aspx

    The Old New Thing
    https://blogs.msdn.com/oldnewthing/
    archive/2004/01/08/48616.aspx

    Description of the calling conventions that the 32-bit compiler supports MS: KDB
    https://support.microsoft.com/
    ?scid=kb%3Ben-us%3B100832&x=12&y=11

    Blog de skywing
    Win32 calling conventions: __stdcall in assembler
    https://www.nynaeve.net/?p=53

    Win32 calling conventions: __cdecl in assembler
    https://www.nynaeve.net/?p=41

    Muchas gracias por tu opinión.

  • Otro anonimo
    Responder

    anonimo, creo que estas confundiendo algunas cosas. Stdcall y cdecl son dos formas de hacer llamadas a funciones/subrutinas que solo se diferencian en quien tiene la obligacion de restaurar el stack despues de hacer la llamada. El orden depende, por ejemplo del lenguaje que uses; en pascal y en c las llamadas al stack se realizan al reves, en pascal de izquierda a derecha y en c de derecha a izquierda. Siempre que desde pascal se usan librerias de c o c++ hay que especificar el cdecl o el stdcall para indicar al compilador que tiene que invertir el orden al hacer la llamada.

Leave a Comment