Hay (entre otros) dos tipos de convenciones de llamadas: stdcall y cdecl . Tengo algunas preguntas sobre ellos:
- Cuando se llama a una función cdecl, ¿cómo sabe la persona que llama si debe liberar la pila? En el sitio de la llamada, ¿sabe la persona que llama si la función que se llama es una función cdecl o stdcall? Como funciona ? ¿Cómo sabe la persona que llama si debería liberar la pila o no? ¿O es responsabilidad de los enlazadores?
- Si una función declarada como stdcall llama a una función (que tiene una convención de llamada como cdecl), o al revés, ¿sería inapropiado?
- En general, ¿podemos decir qué llamada será más rápida: cdecl o stdcall?
Respuestas:
Raymond Chen ofrece una buena descripción de lo que hace
__stdcall
y lo que__cdecl
hace .(1) El llamador "sabe" limpiar la pila después de llamar a una función porque el compilador conoce la convención de llamada de esa función y genera el código necesario.
void __stdcall StdcallFunc() {} void __cdecl CdeclFunc() { // The compiler knows that StdcallFunc() uses the __stdcall // convention at this point, so it generates the proper binary // for stack cleanup. StdcallFunc(); }
Es posible no coincidir con la convención de llamada , así:
LRESULT MyWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); // ... // Compiler usually complains but there's this cast here... windowClass.lpfnWndProc = reinterpret_cast<WNDPROC>(&MyWndProc);
Tantas muestras de código se equivocan y ni siquiera es gracioso. Se supone que debe ser así:
// CALLBACK is #define'd as __stdcall LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg WPARAM wParam, LPARAM lParam); // ... windowClass.lpfnWndProc = &MyWndProc;
Sin embargo, asumiendo que el programador no ignora los errores del compilador, el compilador generará el código necesario para limpiar la pila correctamente, ya que conocerá las convenciones de llamada de las funciones involucradas.
(2) Ambas formas deberían funcionar. De hecho, esto sucede con bastante frecuencia al menos en el código que interactúa con la API de Windows, porque
__cdecl
es el valor predeterminado para los programas C y C ++ según el compilador de Visual C ++ y las funciones de WinAPI usan la__stdcall
convención .(3) No debería haber una diferencia real de rendimiento entre los dos.
fuente
En CDECL, los argumentos se insertan en la pila en orden inverso, la persona que llama borra la pila y el resultado se devuelve a través del registro del procesador (luego lo llamaré "registro A"). En STDCALL hay una diferencia, la persona que llama no borra la pila, la calle sí.
Estás preguntando cuál es más rápido. Ninguno. Debe usar la convención de llamadas nativas siempre que pueda. Cambie la convención solo si no hay salida, cuando utilice bibliotecas externas que requieran el uso de cierta convención.
Además, hay otras convenciones que el compilador puede elegir como predeterminada, es decir, el compilador de Visual C ++ usa FASTCALL, que en teoría es más rápido debido al uso más extenso de los registros del procesador.
Por lo general, debe dar una firma de convención de llamada adecuada a las funciones de devolución de llamada pasadas a alguna biblioteca externa, es decir, la devolución de llamada
qsort
desde la biblioteca C debe ser CDECL (si el compilador usa otra convención por defecto, debemos marcar la devolución de llamada como CDECL) o se deben realizar varias devoluciones de llamada de WinAPI. STDCALL (todo WinAPI es STDCALL).Otro caso habitual puede ser cuando está almacenando punteros a algunas funciones externas, es decir, para crear un puntero a la función WinAPI, su definición de tipo debe estar marcada con STDCALL.
Y a continuación hay un ejemplo que muestra cómo lo hace el compilador:
/* 1. calling function in C++ */ i = Function(x, y, z); /* 2. function body in C++ */ int Function(int a, int b, int c) { return a + b + c; }
CDECL:
/* 1. calling CDECL 'Function' in pseudo-assembler (similar to what the compiler outputs) */ push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x' call (jump to function body, after function is finished it will jump back here, the address where to jump back is in registers) move contents of register A to 'i' variable pop all from the stack that we have pushed (copy of x, y and z) /* 2. CDECL 'Function' body in pseudo-assembler */ /* Now copies of 'a', 'b' and 'c' variables are pushed onto the stack */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code (a, b and c still on the stack, the result is in register A)
STDCALL:
/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */ push on the stack a copy of 'z', then a copy of 'y', then a copy of 'x' call move contents of register A to 'i' variable /* 2. STDCALL 'Function' body in pseaudo-assembler */ pop 'a' from stack to register A pop 'b' from stack to register B add A and B, store result in A pop 'c' from stack to register B add A and B, store result in A jump back to caller code (a, b and c are no more on the stack, result in register A)
fuente
Noté una publicación que dice que no importa si llamas a
__stdcall
desde a__cdecl
o viceversa. Lo hace.La razón: con
__cdecl
los argumentos que se pasan a las funciones llamadas son eliminados de la pila por la función que llama, en__stdcall
, los argumentos son eliminados de la pila por la función llamada. Si llama a una__cdecl
función con a__stdcall
, la pila no se limpia en absoluto, por lo que eventualmente cuando__cdecl
use una referencia basada en apilamiento para argumentos o dirección de retorno, usará los datos antiguos en el puntero de pila actual. Si llama a una__stdcall
función desde a__cdecl
, la__stdcall
función limpia los argumentos en la pila, y luego la__cdecl
función lo vuelve a hacer, posiblemente eliminando la información de retorno de las funciones de llamada.La convención de Microsoft para C intenta eludir esto modificando los nombres. Una
__cdecl
función tiene el prefijo de subrayado. Una__stdcall
función tiene como prefijo un guión bajo y un sufijo con un signo “@” y el número de bytes que se eliminarán. Por ejemplo,__cdecl
f (x) está vinculado como_f
,__stdcall f(int x)
está vinculado como_f@4
dóndesizeof(int)
es 4 bytes)Si logra pasar el enlazador, disfrute del lío de depuración.
fuente
Quiero mejorar la respuesta de @ adf88. Siento que el pseudocódigo para STDCALL no refleja la forma en que sucede en la realidad. 'a', 'b' y 'c' no se extraen de la pila en el cuerpo de la función. En su lugar, aparecen con la
ret
instrucción (ret 12
se usaría en este caso) que de un solo golpe vuelve a la persona que llama y al mismo tiempo extrae 'a', 'b' y 'c' de la pila.Aquí está mi versión corregida de acuerdo a mi entendimiento:
STDCALL:
/* 1. calling STDCALL in pseudo-assembler (similar to what the compiler outputs) */ push on the stack a copy of 'z', then copy of 'y', then copy of 'x' call move contents of register A to 'i' variable
/* 2. STDCALL 'Function' body in pseaudo-assembler */ copy 'a' (from stack) to register A copy 'b' (from stack) to register B add A and B, store result in A copy 'c' (from stack) to register B add A and B, store result in A jump back to caller code and at the same time pop 'a', 'b' and 'c' off the stack (a, b and c are removed from the stack in this step, result in register A)
fuente
Se especifica en el tipo de función. Cuando tiene un puntero de función, se asume que es cdecl si no explícitamente stdcall. Esto significa que si obtiene un puntero stdcall y un puntero cdecl, no podrá intercambiarlos. Los dos tipos de funciones pueden llamarse entre sí sin problemas, solo obtiene un tipo cuando espera el otro. En cuanto a la velocidad, ambos realizan los mismos roles, solo que en un lugar ligeramente diferente, es realmente irrelevante.
fuente
La persona que llama y el destinatario de la llamada deben usar la misma convención en el punto de invocación; esa es la única forma en que podría funcionar de manera confiable. Tanto la persona que llama como la persona que llama siguen un protocolo predefinido, por ejemplo, quién necesita limpiar la pila. Si las convenciones no coinciden, su programa tiene un comportamiento indefinido, es probable que se bloquee espectacularmente.
Esto solo es necesario para cada sitio de invocación: el código de llamada en sí puede ser una función con cualquier convención de llamada.
No debería notar ninguna diferencia real en el rendimiento entre esas convenciones. Si eso se convierte en un problema, generalmente necesita hacer menos llamadas; por ejemplo, cambie el algoritmo.
fuente
Esas cosas son específicas del compilador y la plataforma. Ni el estándar C ni el C ++ dicen nada acerca de las convenciones de llamada, excepto
extern "C"
en C ++.La persona que llama conoce la convención de llamada de la función y maneja la llamada en consecuencia.
Si.
Es parte de la declaración de función.
La persona que llama conoce las convenciones de la llamada y puede actuar en consecuencia.
No, la convención de llamada es parte de la declaración de una función, por lo que el compilador sabe todo lo que necesita saber.
No. ¿Por qué debería hacerlo?
No lo sé. Pruébalo.
fuente
El
cdecl
modificador es parte del prototipo de función (o tipo de puntero de función, etc.) por lo que la persona que llama obtiene la información de allí y actúa en consecuencia.No, esta bien.
En general, me abstendría de tales declaraciones. La distinción importa, por ejemplo. cuando desee utilizar las funciones va_arg. En teoría, podría ser que
stdcall
sea más rápido y genere un código más pequeño porque permite combinar la extracción de argumentos con la extracción de los locales, pero OTOH concdecl
, también puede hacer lo mismo, si es inteligente.Las convenciones de llamada que pretenden ser más rápidas suelen pasar de registros.
fuente
Las convenciones de llamada no tienen nada que ver con los lenguajes de programación C / C ++ y son bastante específicas sobre cómo un compilador implementa el lenguaje dado. Si usa constantemente el mismo compilador, nunca tendrá que preocuparse por las convenciones de llamadas.
Sin embargo, a veces queremos que el código binario compilado por diferentes compiladores interactúe correctamente. Cuando lo hacemos, necesitamos definir algo llamado Interfaz binaria de aplicación (ABI). La ABI define cómo el compilador convierte la fuente C / C ++ en código de máquina. Esto incluirá convenciones de llamadas, alteración de nombres y diseño de tabla v. cdelc y stdcall son dos convenciones de llamada diferentes que se utilizan comúnmente en plataformas x86.
Al colocar la información sobre la convención de llamada en el encabezado de la fuente, el compilador sabrá qué código debe generarse para interactuar correctamente con el ejecutable dado.
fuente