c++ - stdcall y cdecl
(9)
a) Cuando el llamador llama a una función cdecl, ¿cómo sabe una persona que llama si debería liberar la pila?
El modificador cdecl
es parte del prototipo de función (o tipo de puntero de función, etc.) para que la persona que llama obtenga la información de allí y actúe en consecuencia.
b) Si una función que se declara como stdcall llama a una función (que tiene una convención de llamada como cdecl), o al revés, ¿sería inapropiado?
No, esta bien.
c) En general, ¿podemos decir qué llamada será más rápida - cdecl o stdcall?
En general, me abstendré de tales declaraciones. La distinción importa, por ejemplo. cuando quieras usar funciones va_arg. En teoría, podría ser que stdcall
sea más rápido y genere código más pequeño porque permite combinar los argumentos con popping the locals, pero OTOH con cdecl
, también puede hacer lo mismo si es inteligente.
Las convenciones de llamadas que apuntan a ser más rápidas generalmente hacen algún movimiento de registro.
Existen (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 una persona que llama si debería liberar la pila? En el sitio de la llamada, ¿sabe la persona que llama si la función a la 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 la responsabilidad de los enlazadores?
- Si una función que se declara 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?
En CDECL, los argumentos se envían a la pila en orden inverso, la persona que llama borra la pila y el resultado se devuelve a través del registro del procesador (más adelante lo llamaré "registro A"). En STDCALL hay una diferencia, la persona que llama no limpia la pila, la calle do.
Estás preguntando cuál es más rápido. Ninguno. Debe usar convenciones de llamadas nativas todo el tiempo que pueda. Cambiar la convención solo si no hay salida, cuando se usan bibliotecas externas que requieren ciertas convenciones para ser usadas.
Además, hay otras convenciones que el compilador puede elegir como predeterminadas, es decir, el compilador de Visual C ++ usa FASTCALL que es teóricamente más rápido debido a un uso más extenso de los registros del procesador.
Por lo general, debe otorgar una firma de convención de llamadas adecuada a las funciones de retrollamada pasadas a alguna biblioteca externa; es decir, la retrollamada a qsort
de la biblioteca C debe ser CDECL (si el compilador usa por defecto otra convención, debemos marcarla como CDECL) o varias devoluciones de llamada WinAPI ser 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 se muestra 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)
Esas cosas son específicas del compilador y de la plataforma. Ni el estándar C ni el estándar C ++ dicen nada acerca de las convenciones de llamada excepto para la extern "C"
en C ++.
¿Cómo sabe una persona que llama si debería liberar la pila?
El llamante conoce la convención de llamada de la función y maneja la llamada en consecuencia.
En el sitio de la llamada, ¿sabe la persona que llama si la función a la que se llama es una función cdecl o stdcall?
Sí.
Como funciona ?
Es parte de la declaración de la función.
¿Cómo sabe la persona que llama si debería liberar la pila o no?
La persona que llama conoce las convenciones de llamadas y puede actuar en consecuencia.
¿O es la responsabilidad de los enlazadores?
No, la convención de llamadas forma parte de la declaración de una función, por lo que el compilador sabe todo lo que necesita saber.
Si una función que se declara como stdcall llama a una función (que tiene una convención de llamada como cdecl), o al revés, ¿sería inapropiado?
No. ¿Por qué debería?
En general, ¿podemos decir qué llamada será más rápida, cdecl o stdcall?
No lo sé. Pruébalo.
Está especificado en el tipo de función. Cuando tienes un puntero a la función, se supone que es cdecl si no explícitamente stdcall. Esto significa que si obtienes un puntero stdcall y un puntero cdecl, no puedes 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, en un lugar ligeramente diferente, es realmente irrelevante.
La persona que llama y la llamada deben usar la misma convención en el momento de la invocación: esa es la única forma en que podría funcionar de manera confiable. Tanto la persona que llama como la llamada siguen un protocolo predefinido, por ejemplo, quién necesita limpiar la pila. Si las convenciones no coinciden, su programa se topa con un comportamiento indefinido, es probable que se cuelgue de forma espectacular.
Esto solo se requiere por sitio de invocación: el código de llamada en sí puede ser una función con cualquier convención de llamadas.
No debe 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.
Las convenciones de llamada no tienen nada que ver con los lenguajes de programación C / C ++ y son más bien específicos sobre cómo un compilador implementa el lenguaje dado. Si utiliza sistemáticamente el mismo compilador, nunca tendrá que preocuparse por las convenciones de llamadas.
Sin embargo, a veces deseamos que el código binario compilado por diferentes compiladores funcione correctamente. Cuando lo hagamos, debemos definir algo llamado Application Binary Interface (ABI). El ABI define cómo el compilador convierte la fuente C / C ++ en código máquina. Esto incluirá convenciones de llamadas, cambio de nombre y diseño de tabla v. cdelc y stdcall son dos convenciones de llamadas diferentes comúnmente utilizadas en plataformas x86.
Al colocar la información en la convención de llamadas en el encabezado fuente, el compilador sabrá qué código debe generarse para interoperar correctamente con el ejecutable dado.
Noté una publicación que dice que no importa si llamas a __stdcall
desde un __cdecl
o viceversa. Lo hace.
El motivo: con __cdecl
los argumentos que pasan a las funciones llamadas se eliminan de la pila mediante la función de llamada, en __stdcall
, la función llamada __stdcall
los argumentos de la pila. Si llama a una función __cdecl
con un __stdcall
, la pila no se limpia en absoluto, por lo que eventualmente cuando __cdecl
utiliza una referencia basada en __cdecl
para los argumentos o la dirección de retorno usará los datos anteriores en el puntero de pila actual. Si llama a una función __stdcall
desde un __cdecl
, la función __stdcall
limpia los argumentos en la pila, y luego la función __cdecl
hace de nuevo, posiblemente eliminando la información de retorno de las funciones de llamada.
La convención de Microsoft para C intenta eludir esto al modificar los nombres. Una función __cdecl
se antepone con un guión bajo. Una función __stdcall
un __stdcall
con guion 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 _x
, __stdcall f(int x)
está vinculado como _f@4
donde sizeof(int)
es 4 bytes)
Si logra pasar el engarce, disfrute del desastre de depuración.
Quiero mejorar la respuesta de @adf88. Siento que el pseudocódigo para el 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, se abren con la instrucción ret
(se usaría ret 12
en este caso) que de una sola vez salta de vuelta a la persona que llama y al mismo tiempo saca ''a'', ''b'' y ''c'' de la pila.
Aquí está mi versión corregida según mi entendimiento:
STDCALL:
/* 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)/* 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
Raymond Chen ofrece una buena visión general de lo que hace __stdcall
y __cdecl
.
(1) La persona que llama "sabe" limpiar la pila después de llamar a una función porque el compilador conoce la convención de llamadas 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 llamadas , como esta:
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, ni siquiera es gracioso. Se supone que es así:
// CALLBACK is #define''d as __stdcall
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg
WPARAM wParam, LPARAM lParam);
// ...
windowClass.lpfnWndProc = &MyWndProc;
Sin embargo, suponiendo que el programador no ignore 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 ocurre 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 ++ de acuerdo con el compilador de Visual C ++ y las funciones de WinAPI utilizan la convención __stdcall
.
(3) No debería haber una diferencia de rendimiento real entre los dos.