Español | English
rss facebook linkedin Twitter

¿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

4 comentarios:

Mario dijo...

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 dijo...

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 dijo...

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:
http://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
http://msdn2.microsoft.com/en-us/
library/zxk0tw93(VS.71).aspx

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

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

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

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

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



Muchas gracias por tu opinión.

Otro anonimo dijo...

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.


(+34 902 222 521)


24 horas / 7 días a la semana



© Copyright S21sec 2013 - Todos los derechos reservados


login