Utilización de múltiples lenguajes en un programa Al utilizar múltiples lenguajes en un programa se necesitan conocimientos y posibilidad del parte de compilador de utilizar los siguientes convenciones: 1. Convenciones con respecto a cambios del nombre de la función. Mirar Anexo A. 2. Convenciones de cómo se pasan los parámetros entre las funciones. Por ejemplo – en la pila dependiente del tamaño de los tipos o en los registros; con números desde 8 hasta 16 (HP 9000); en el principio del programa que se ejecuta (Real Time Celatron) etc. El Borland C los parámetros siempre se pasan por la pila en el orden reverso con respecto al prototipo. Nótese que esta descripción no es completa, tal como la granularidad de la pila y los tamaños de los punteros dependen del modelo de la memoria que se utilice. 3. Convenciones de como se pasa el resultado de una función al programa que llama. Por ejemplo – si el resultado es del tamaño 1-2 bytes en AX, si es del tamaño 3-4 bytes en DX:AX si es un numero real en ST0 del 80x87, si es mas de 4 bytes - en la pila (Borland C). 4. Convenciones de como se guarda, recupera y utiliza el contexto de ejecución en una función. Como se reparte esta actividad entre la función que llama y la función que se ejecuta. El general, el contexto de ejecución tiene que recuperase (salvo los recursos que se utilizan para pasar el resultado). El contexto de ejecución de un procesador contiene los registros del mismo, los registros y (pseudo) pilas de sus co - procesadores, el estado de su periferia, el estado de la pila, etc. Sin embargo al programar en lenguajes de alto nivel no nos interesan todos los recursos del sistema y todo el marco de ejecución. Además sería muy lento guardar todo el contexto de ejecución y recuperarlo. Por lo tanto el contexto de ejecución se limita sólo a unos pocos recursos necesarios para guardar la integridad del programa. Utilizando BorlandC el contexto consiste en SI, DI, BP, SP, DS, SS. Si desde nuestra función se llama a otra función estos registros no cambiaran. En cambio, si escribimos una función para utilizarla desde programas con BorlandC tenemos que preservar los valores de estos registros. Los registros SI y DI se utilizan como variables de clase de memoria register. Tal como las convenciones son muchas y dependen del sistema operativo, del tipo de compilador, del lenguaje de programación etc., es más importante saber cómo conseguir la información necesaria. Lo más fácil el mirar en la documentación si esta ultima se proporciona con el compilador. Sin embargo la documentación no siempre esta disponible o contiene demasiados detalles para resolver el problema y por lo tanto es inútil. Una manera fácil de conseguir la información necesaria es escribiendo un programa que contiene un dummy o stub de la función de bajo nivel y compilarla con opción de línea de comando que produce código de ensamblador (normalmente la opción es –S o –s). Después se mira el código producido por el compilador (con extensión ASM o s). Podemos utilizar el código generado por el compilador como punto de partida para escribir nuestro código. El error más frecuente que se comete en esta manera es no guardar del todo el contexto del procesador. Por lo tanto es aconsejable incluir en el stub suficientes variables register con todos los tipos que vamos a utilizar y compilar sin optimización o si la optimización es inevitable incluir código suficiente para que los variables no desaparezcan al no utilizarlas. Las palabras claves de C: cdecl, PASCAL, extern “C” etc. Para que sirven: Para poder llamar al una función de compilador de otro lenguaje o directo al sistema operativo. Por qué son necesarias: Son necesarias porque los diferentes compiladores tienen diferentes convenciones de llamada de funciones e incluso algunos de ellos especifican el método de llamada y el manejo de la pila en la definición del lenguaje. Detalles: PASCAL: probablemente las convenciones más compiladas. 1. Los parámetros se pasan en la pila en el mismo orden de la declaración (Esto significa que en el tope de la pila esta el último argumento). 2. La pila se libera en la función que se llama. Esto significa que si se declara incorrecto en el modulo principal el programa casca por desincronización de la pila. 3. El resultado se comunica en la misma manera que en C. 4. El contexto de la mayoría de los compiladores 80x86 contiene: SP, BP, DS, SS o ESP, EBP, DS y SS. Se utiliza: 1. Al llamar a una función desarrollada en PASCAL. 2. Al escribir una función en C que va a utilizarse desde un programa en PASCAL. 3. En entorno Windows: Al llamar una función de una DLL o al escribir una función que se exporta de una DLL (para escribir funciones de DLL hay métodos mejores). En particular al llamar las funciones del KERNEL.DLL. cdecl – Esta es la convencía normal de llamada de una función en C. 1. Los parámetros se pasan el la pila en orden reverso de la declaración. 2. La pila se libera del programa que llama después de utilizar el resultado si este ultimo es mas de 4 bytes. 3. El contexto esta compuesto de los siguientes registros: SP, BP, SI, DI, DS, SS, ES y los registros de 80x87. Se utiliza: 1. Si una función en C se llama desde un programa en C++ (en combinación con extern “C”). 2. Para forzar la transformación del nombre de la función según las convenciones en C (normalmente añadiendo un _ antes del nombre de la función). Esto es necesario si el compilador tiene opciones que evitan la conversión de los nombres de las funciones. 3. Para decir explícitamente que la función esta escrita en el lenguaje de maquina si el compilador utiliza por defecto pseudo código (hoy en día solo para unos microcontroladores con poca memoria interna). Anexo A Transformación de nombres de funciones de alto nivel: Por qué es necesaria: No permitir conflictos de nombres de bajo nivel Casi todos los compiladores de alto nivel tienen posibilidad de generar código fuente en ensamblador. Este código se utiliza en general en dos maneras: En el tiempo de desarrollo para comprobar el compilador y en el tiempo de explotación – para optimizar a mano el código generado del compilador (así se escriben la mayoría de las librerías de bajo nivel y los KERNELS de OS). Por lo tanto los nombres de los variables y las funciones no pueden coincidir. Ejemplo: int ddd; ... ddd--; ... se puede compilar (sin transformación de nombres): ddd dw 0 ... dec ddd ... que esta bien. Pero: int ax; ... ax--; ... se compilara: ax dw 0 ... dec ax ... que no es correcto. No permitir conflictos de nombres generados en alto nivel. Imaginemos que la función en modelo Medium (datos far proc near) y en modelo Large (todo far) se llama de la misma manera. Al utilizar la función del modelo médium en un programa del modelo Large se desincroniza la pila. Advertencia: Esto se hace solo de los compiladores buenos y hace poco tiempo. En los compiladores de DOS si existen este tipo de conflictos. Re - utilizar funciones con el mismo nombre y argumentos de distintos tipos (sobrecarga de funciones). Esta practica es común en lenguajes orientados a objetos donde métodos (funciones) con objetos diferentes comparten el mismo nombre. En esta manera también se permite comprobar los tipos de los argumentos. Ejemplo C++: void line(point x) { // pinta linea recta desde el ultimo punto hasta x. } void line(point x,point y) { // pinta linea recta de x a y } Si line(x) no cambia de nombre según sus argumentos no podrá distinguirse de line(z,y). Como cambian los nombres de las funciones: Las técnicas comunes son: 1. Añadir un prefijo constante (normalmente _ o __ o @). Esto evita la generación de nombres de registros y palabras claves. 2. Si el lenguaje de alto nivel no es sensible a mayúsculas/minúsculas – cambiar todos los nombres en mayúsculas. 3. Añadir sufijos del tipo del resultado, los tipos de los argumentos etc. Ejemplo de programa de bajo nivel que se llama del programa de alto nivel y al revés. #include <stdio.h> extern getlpt(void); int main(void) { register int a=getlpt(); printf("LPT1 is %d\n",a); return 0; } El programa escribe el contenido del puerto de la impresora a al pantalla. ; Demo how to call a c function from the ASM function ; ; Compile with ; tasm /ml /mx /zi tst3.asm ; \ \ ; \ --- Upper/lower case sensitive ; ------ Large memory model (far ptrs,far functions) ; segment getlpt_code public 'code' format db "The port is %x(hex) %d(decimal)",10,0 _getlpt proc far ; Get the port # of the LPT1 xor ax,ax mov ax,040h mov es,ax mov dx,word ptr es:[08] ; Call printf from the assembly program. push dx ; save dx because it is not in the context ; printf("The port is %x ... %d ... \n",dx,dx); ; Push the arguments into the stack push dx ; to print decimal push dx ; to print hex mov ax,getlpt_code ; the segment of the format push ax mov ax,offset format ; format push ax call far ptr _printf ; This is from stdlib add sp,8 ; clean the stack. ; 4 bytes format ptr (2 seg + 2 offset) ; 2 bytes first dx ; 2 bytes secon dx ;-------------------; 8 bytes total pop dx ; recover the port ; Debug in al,dx ; read the port xor ah,ah ; The result is in AX ret ; Done _getlpt endp public _getlpt getlpt_code ends extrn _printf:far end ; Notar!!! fuera del segmento!