domingo, noviembre 27, 2005

 

Convenciones de llamada y parametros variantes

Aunque debo admitir, que no investigué mucho, las principales diferencias entre MIPS e INTEL, al menos las que puedo señalar empiricamente, es, por supuesto, que INTEL pasa parámetros via stack, mientras que MIPS dispone de cuatro registros especiales para ello. Aunque Tambien es cierto que en MIPS podría hacerse todo el paso de parametros por medio del stack. No se para que, pero alguien podría estar usando esos cuatro registros para otra cosa.

Fuera de eso, lo más que sé al respecto, es que, MIPS es arquitectura RISC, mientras INTEL es CISC. Eso significa que MIPS posee un juego de Instrucciones reducido, mientras que Intel, tiene uno extendido. En lo que a mi respecta, eso significa que Intel tiene instrucciones equivalente a la secuencia de Varias instrucciones, lo cual por ejemplo, permite que lo que MIPS hace en dos o más instrucciones, INTEL lo puede hacer en una sola instruccion atómica.

En Realidad, el tema RISC vrs CISC es sumamente extenso, dado que se trata de filosofías de diseño, y como tales, tienen ventajas y desventajas...

En cuanto a la segunda pregunta, pues, si, en efecto hice el programa, pero solo fue "un vil copy-paste" de un ejemplo que encontre en MSDN.

De modo que decidí hacer algo parecido, pero escarbando un poquito, usando direcciones de memoria para ver algunas cosas...

Lo primero que hice, es ver la direccion de memoria de los parámetros, para constatar sus posiciones relativas en la menmoria. Pude confirmar, que C, introduce primero el último parámetro, dejando el primero más cerca del tope del stack. En el ejemplo que proporciono se puede ver como second está en la direccion de memoria siguiente a first.

Eso me indujo a mi siguiente experimento. Dado el procedimiento cuyos parámetros son todos de tipo int, es posible declarar un int*, y asignarle la direccion del primer parámetro. El resultado, si se tiene:
int * ptr = &primerParam
Entonces se puede decir:
ptr[0] = ///
ptr[1] = ///
/// = ptr[n],
donde ptr[n] es el (n+1)-ésimo parámetro.

En mi experimento, pude accesar todos los parámetros, indistintamente si éstos eran fijos o variantes.

ABRE-PARENTESIS

No hay manera de saber cuantos parámetros se han enviado, al menos desde el lado del callee, si no se utiliza algun terminador, bandera, valor especial, ... etc. ni siquiera mediante las macros va_start, va_arg, o va_end (que no se que hace por cierto), y esto, aun en las versiones ansi y unix.

Solamente el caller "sabe exactamente" cuantos argumentos utilizó en la llamada.

Aqui entra en juego el asunto de las CALLING_CONVENTIONS.
En las lecturas que hice, encontré lo siguiente:

hay basicamente tres: __stdcall, __cdecl, y __fastcall.

En realidad no encontré una diferencia entre las dos primeras, excepto que en __cdecl, el caller es el responsable de sacar los parametros del stack una vez que la llamada retorna; no así en __stdcall, donde es el callee el encargado.

En cuanto al orden de los parámetros en la pila, experimenté con __stdcall y con __cdecl, en ambas obtuve las mismas direcciones relativas entre parámetros, i.e. no vi ningun cambio, el primer argumento está siempre mas cerca del tope de la pila que los demás.

De hecho, eso también lo leí por ahi en mis averiguatos.

En lo que más relevancia encontre que la diferenciacion era importante, es en el tema ya de enlace de símbolos (linking), donde tienen mucho que estudiar los que desarrollan DLL, (Dynamic Link Libraries). Aqui lo importante es que, si hay una disparidad entre convenciones de llamada, (que un lado utilice __stdcall y el otro __cdecl) la pila se ve destruida, pues bien, o nadie hace "pop", o ambos lo harán, ( y la máquina hace "puff" y uno queda con "cara de what?").

En un comantario sin importancia, hasta ahora sé porque hay que ponerle __stdcall a las funciones que uno usa en una DLL, jeje, por cierto, eso de #define, es mala manía, porque en las DLL uno usa DLLIMPORT, pero al final, #define DLLIMPORT __stdcall. (Ni los mareros usan tanto alias...)

La consistencia entre convenciones de llamada es también crucial en los casos en que una aplicación se desarrolla desde varios lenguajes.

en cuanto a __fastcall, basicamente utiliza registros para pasar parámetros, pero como no todos los registros estarán disponibles, a fin de cuentas el compilador terminará decidiendo cuales poner y cuales no...

El otro punto a que encontré, concerniente a convenciones, tiene tambien que ver con linking, esta vez con los nombres decorados o "mangling", que es cuando los editores de enlace (equivalente de linkers) cambian los nombres de las funciones, "decorandolos" con simbologías que indican tipo de convencion de llamada, tipo y/o numero de parametros, etc. El "name mangling" depende del diseño del linker.

CIERRA-PARENTESIS

Tambien quise ver las variables locales, y constaté que se menten en la pila en el orden en que se declaran, y despues que los argumentos, es decir asi:

(direcciones bajas/tope)
...
variables locales
8 Bytes que no se que son
arg1
arg2
...
argn
...
(direcciones altas/fondo de pila)


Codigo Usado en el experimento final:

____________________________________________

#include
#include
#include

int recorre4(int first, int second, int third...){
printf("&first =%i\n&second=%i\n&third =%i\n",&first, &second, &third);
int a,b,c,d;
a = 1;
b = 2;
c = 3;
d = 4;
printf("\n&a=%i\n&b=%i\n&c=%i\n&d=%i\n",&a,&b,&c,&d);
int* ptr; ptr = &first;
printf("&p=%i\n",&ptr);
int x,y,z;
printf("\n&x=%i\n&y=%i\n&z=%i\n",&x,&y,&z);
printf("ptr:[%i][%i][%i][%i][%i][%i]\n",ptr[0],ptr[1],ptr[2],ptr[3],ptr[4],ptr[5]);
printf("ptr[7] = = = :%i\n", ptr[7]);
printf("ptr[6] = = = :%i\n", ptr[6]);
printf("arriba^5 de a:%i\n", *(&a+5));
printf("arriba^4 de a:%i\n", *(&a+4));
printf("arriba^3 de a:%i\n", *(&a+3));
printf("arriba^2 de a:%i\n", *(&a+2));
printf("arriba de a:%i\n", *(&a+1));
printf("arriba de b:%i\n", *(&b+1));
printf("arriba de c:%i\n", *(&c+1));
printf("exactamente c:%i\n", *(&c));
printf("debajo de c:%i\n", *(&c-1));
printf("debajo^2 de c:%i\n", *(&c-2));
printf("debajo^3 de c:%i\n", *(&c-3));
//return 224;
}

int __stdcall normal(int m, int n, int o){
int s, t, u;
printf("&m := %i\n", &m);
printf("&n := %i\n", &n);
printf("&o := %i\n", &o);
printf("&s := %i\n", &s);
printf("&t := %i\n", &t);
printf("&u := %i\n", &u);
}

int main(){
int r1 = recorre4(2,5,6,7,8,0);
printf(" retorna %i\n", r1);
int r2 = recorre4(2,5,6,7,8,0);
printf(" retorna %i\n", r2);
printf("\n");
system("pause");
normal(1,2,3);
system("pause");
return 0;
}

____________________________________________

SALIDA
____________________________________________

&first =37814056
&second=37814060
&third =37814064

&a=37814044
&b=37814040
&c=37814036
&d=37814032
&p=37814028

&x=37814024
&y=37814020
&z=37814016
ptr:[2][5][6][7][8][0]
ptr[7] = = = :4200443
ptr[6] = = = :37814112
arriba^5 de a:6
arriba^4 de a:5
arriba^3 de a:2
arriba^2 de a:4200463
arriba de a:37814112
arriba de b:1
arriba de c:2
exactamente c:3
debajo de c:4
debajo^2 de c:37814056
debajo^3 de c:4200624
retorna 22
&first =37814056
&second=37814060
&third =37814064

&a=37814044
&b=37814040
&c=37814036
&d=37814032
&p=37814028

&x=37814024
&y=37814020
&z=37814016
ptr:[2][5][6][7][8][0]
ptr[7] = = = :4200443
ptr[6] = = = :37814112
arriba^5 de a:6
arriba^4 de a:5
arriba^3 de a:2
arriba^2 de a:4200511
arriba de a:37814112
arriba de b:1
arriba de c:2
exactamente c:3
debajo de c:4
debajo^2 de c:37814056
debajo^3 de c:2147332096
retorna 25

Press any key to continue . . .
&m := 37814072
&n := 37814076
&o := 37814080
&s := 37814060
&t := 37814056
&u := 37814052
Press any key to continue . . .


____________________________________________

Comments: Publicar un comentario

<< Home

This page is powered by Blogger. Isn't yours?