una - parametros por referencia c++
Si no paso suficientes parámetros cuando llamo a una función en una DLL, ¿qué pasará? (1)
En un proyecto dll, la función es así:
extern "C" __declspec(dllexport) void foo(const wchar_t* a, const wchar_t* b, const wchar_t* c)
En un proyecto diferente, foo
función foo
, pero declaro la función foo
en el archivo de encabezado con
extern "C" __declspec(dllimport) void foo(const wchar_t* a, const wchar_t* b)
y lo llamo con solo dos parámetros.
El resultado es un éxito, creo que se trata de __cdecl
llamada __cdecl
, pero me gustaría saber cómo y por qué funciona.
32 bits
La convención de llamadas predeterminada es __cdecl
, lo que significa que la persona que llama empuja los parámetros a la pila de derecha a izquierda y luego limpia la pila después de que la llamada retorna.
Entonces en tu caso, la persona que llama:
- Empuja b
- Empuja a
- Empuja la dirección de retorno
- Llama a la función.
En este punto, la pila se ve así (supongamos indicadores de 4 bytes, por ejemplo, y recuerda que el puntero de la pila viaja hacia atrás cuando empujas cosas):
+-----+ <--- this is where esp is after pushing stuff
| ret | [esp]
+-----+
| a | [esp+4]
+-----+
| b | [esp+8]
+-----+ <--- this is where esp was before we started
| ??? | [esp+12 and beyond]
+-----+
Vale genial. Ahora el problema ocurre en el lado de callee. El destinatario espera que los parámetros estén en ciertas ubicaciones en la pila, entonces:
-
a
se supone que está en[esp+4]
-
b
se supone que está en[esp+8]
-
c
se supone que está en[esp+12]
Y aquí es donde está el problema: no tenemos idea de qué hay en [esp+12]
. Entonces el destinatario de la llamada verá los valores correctos de b
, pero interpretará cualquier basura desconocida que esté en [esp+12]
como c
.
En ese punto, es bastante indefinido, y depende de lo que tu función realmente haga con c
.
Después de que todo esto termine y el destinatario vuelva, suponiendo que el programa no se bloqueó, el llamador restaurará esp
y el puntero de pila volverá donde debería estar. Por lo tanto, desde el Punto de vista de la persona que llama todo está bien y el puntero de la pila termina donde debería estar, pero el destinatario ve basura por c
.
64 bits
La mecánica en las máquinas de 64 bits es diferente, pero el resultado final es más o menos el mismo efecto. Microsoft utiliza la siguiente convención de llamadas en máquinas de 64 bits, independientemente de __cdecl
o lo que sea (cualquier convención que especifique se ignorará y todas se tratarán de manera idéntica):
- Los primeros cuatro argumentos enteros o de puntero colocados en los registros
rcx
,rdx
,r8
yr9
, en ese orden, de izquierda a derecha. - Los primeros cuatro argumentos de coma flotante colocados en los registros
xmm0
,xmm1
,xmm2
yxmm3
, en ese orden, de izquierda a derecha. - Todo lo que queda se empuja a la pila, de derecha a izquierda.
- La persona que llama es responsable de restaurar
esp
y restaurar los valores de todos los registros volátiles después de la llamada.
Entonces en tu caso, la persona que llama:
- Pone
a
enrcx
. - Pone
b
enrdx
. - Asigna 32 bytes adicionales de "espacio sombra" en la pila (ver ese artículo MS).
- Empuja la dirección de retorno.
- Llama a la función.
Pero el llamado está esperando:
-
a
supone que está enrcx
(¡rcx
!) -
b
supone que está enrdx
(¡rdx
!) -
c
supone que está enr8
(problema)
Y así, al igual que en el caso de 32 bits, el destinatario interpreta lo que sucedió que está en r8
como c
, y se producen posibles "hijinks", con el efecto final dependiendo de lo que el destinatario haga con c
. Cuando vuelve, suponiendo que el programa no falla, la persona que llama restaura todos los registros volátiles ( rcx
y rdx
, y también generalmente incluye r8
y amigos) y restaura esp
.