stdcall y cdecl

92

Hay (entre otros) dos tipos de convenciones de llamadas: stdcall y cdecl . Tengo algunas preguntas sobre ellos:

  1. 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?
  2. 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?
  3. En general, ¿podemos decir qué llamada será más rápida: cdecl o stdcall?
Rakesh Agarwal
fuente
9
Hay muchos tipos de convenciones de llamadas, de las cuales son solo dos. en.wikipedia.org/wiki/X86_calling_conventions
Mooing Duck
1
Por favor, marque una respuesta correcta
ceztko

Respuestas:

79

Raymond Chen ofrece una buena descripción de lo que hace __stdcally lo que __cdeclhace .

(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 __cdecles el valor predeterminado para los programas C y C ++ según el compilador de Visual C ++ y las funciones de WinAPI usan la __stdcallconvención .

(3) No debería haber una diferencia real de rendimiento entre los dos.

En silico
fuente
+1 para un buen ejemplo y la publicación de Raymond Chen sobre la historia de la convención de llamadas. Para cualquier persona interesada, las otras partes también son una buena lectura.
OregonGhost
+1 para Raymond Chen. Por cierto (OT): ¿Por qué no puedo encontrar las otras partes usando el cuadro de búsqueda del blog? ¿Google los encuentra, pero no los blogs de MSDN?
Mainframe nórdico
44

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 qsortdesde 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)
adf88
fuente
Nota: __fastcall es más rápido que __cdecl y STDCALL es la convención de llamada predeterminada para Windows de 64 bits
dns
ohh; por lo que debe mostrar la dirección de retorno, agregar el tamaño del bloque de argumentos y luego saltar a la dirección de retorno que apareció anteriormente? (una vez que llame a ret, ya no podrá limpiar la pila, pero una vez que limpie la pila, ya no podrá llamar a ret (ya que tendrías que enterrarte para regresar a la pila, lo que te traerá de vuelta al problema de que no has limpiado la pila)
Dmitry
alternativamente, regrese a reg1, establezca el puntero de la pila en el puntero base, luego salte a reg1
Dmitry
alternativamente, mueva el valor del puntero de la pila de arriba a abajo, limpie y luego llame a ret
Dmitry
15

Noté una publicación que dice que no importa si llamas a __stdcalldesde a __cdeclo viceversa. Lo hace.

La razón: con __cdecllos 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 __cdeclfunción con a __stdcall, la pila no se limpia en absoluto, por lo que eventualmente cuando __cdecluse 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 __stdcallfunción desde a __cdecl, la __stdcallfunción limpia los argumentos en la pila, y luego la __cdeclfunció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 __cdeclfunción tiene el prefijo de subrayado. Una __stdcallfunció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, __cdeclf (x) está vinculado como _f, __stdcall f(int x)está vinculado como _f@4dónde sizeof(int)es 4 bytes)

Si logra pasar el enlazador, disfrute del lío de depuración.

Lee Hamilton
fuente
3

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 retinstrucción ( ret 12se 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)

golem
fuente
2

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.

Perrito
fuente
1

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.

diente filoso
fuente
1

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

¿Cómo sabe una persona que llama si debe liberar la pila?

La persona que llama 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 que se llama es una función cdecl o stdcall?

Si.

Como funciona ?

Es parte de la declaración de 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 la llamada y puede actuar en consecuencia.

¿O es responsabilidad de los enlazadores?

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.

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?

No. ¿Por qué debería hacerlo?

En general, ¿podemos decir qué llamada será más rápida: cdecl o stdcall?

No lo sé. Pruébalo.

sellibitze
fuente
0

a) Cuando la persona que llama llama a una función cdecl, ¿cómo sabe la persona que llama si debe liberar la pila?

El cdeclmodificador 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.

b) 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?

No, esta bien.

c) En general, ¿podemos decir qué llamada será más rápida: cdecl o stdcall?

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 stdcallsea ​​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 con cdecl, también puede hacer lo mismo, si es inteligente.

Las convenciones de llamada que pretenden ser más rápidas suelen pasar de registros.

jpalecek
fuente
-1

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.

Doron
fuente