UNIDAD 4 PROGRAMACION HIBRIDA
4.1 DIRECTIVAS PARA COMPILACION HIBRIDA
Directivas de Programación Híbrida
EL ENSAMBLADOR Y EL LENGUAJE C
El lenguaje C es sin duda el más apropiado para la programación de sistemas,
pudiendo sustituir al ensamblador en muchos casos. Sin embargo, hay ocasiones
en que es necesario acceder a un nivel más bajo por razones de operatividad e
incluso de necesidad (programas residentes que economicen memoria, algoritmos
rápidos para operaciones críticas, etc.). Es entonces cuando resulta evidente la
necesidad de poder emplear el ensamblador y el C a la vez.
Para comprender este capítulo, basta tener unos conocimientos razonables de C
estándar. Aquí se explicarán las funciones de librería necesarias para acceder al
más bajo nivel, así como la manera de integrar el ensamblador y el C.
Sentencias ensamblador
C++ dispone de la opción de poder escribir directamente instrucciones en lenguaje
ensamblador junto con el resto del código fuente. Para esto dispone de una
palabra clave específica: asm.
La palabra asm indica que la cadena literal que sigue será incluida en el código
objeto en la posición indicada.
La sintaxis empleada depende del compilador. En el caso de C++Builder, es la
siguiente:
asm <opcode> <operandos> <; o NL>
Ejemplo
asm pop dx;
asm push ebx;
asm call @@std@%basic_ostream$c19std@%char_traits$c%%@put$qc;
asm add esp,8;
asm push ebx;
Se permite que varias instrucciones ensamblador pueden ser agrupadas en un
solo bloque precedido por el indicador asm. Ejemplo:
asm {
mov ax, 0x0e07;
xor bx, bx;
int 0x10;
}
La sintaxis empleada por el compilador C++ GNU para Windows utilizado por
Dev-C++ no utiliza la sintaxis de Intel, sino la de AT&T.
Ejemplo [2]:
int AdrIO; // variable global
static char ValIO; // variable global
void foo() {
__asm("mov _AdrIO, %dx") ; // cargar un registro de 16 bits
__asm("mov _ValIO, %al") ; // cargar un registro de 8 bits
__asm("mov %ax,%dx") ; // mover contenido registro AX a DX
}
Nota: no olvidar incluir el guión bajo ("underscore") precediendo los nombres
de variables globales.
Ni que decir tiene que cualquier código ensamblador insertado mediante asm, es
específico, y por tanto dependiente, de la plataforma sobre la que se ejecutará el
programa. Este tipo de sentencias deben reducirse al máximo si queremos facilitar
la portabilidad del código.
4.3 BLOQUES EN LENGUAJE ENSAMBLADOR
Bloques en ensamblador
MODELOS DE MEMORIA.
Los modelos de memoria constituyen las diversas maneras de acceder a la
memoria por parte de los compiladores de C. En el caso del Turbo C se pueden
distinguir los siguientes:
TINY: Se emplea en los programas donde es preciso apurar el consumo de
memoria hasta el último byte. Los 4 registros de segmento (CS, DS, ES, SS) están
asignados a la misma dirección, por lo que existe un total de 64 Kb donde se
mezclan código, datos y pila. Los programas de este tipo pueden convertirse a
formato COM.
SMALL: Se utiliza en aplicaciones pequeñas. Los segmentos de código y datos
son diferentes y no se solapan. Por ello, hay 64 kb para código y otros 64 Kb a
repartir entre datos y pila.
Segmentos Punteros
Modelo Código Datos Pila Código Datos
Tiny 64 Kb near near
Small 64 Kb 64 Kb near near
Medium 1 Mb 64 Kb far near
Compact 64 Kb 1 Mb near far
Large 1 Mb 1 Mb far far
Huge 1 Mb 1 Mb
(Bloques > 64 Kb) far far
MEDIUM: Este modelo es ideal para programas largos que no manejan
demasiados datos. Se utilizan punteros largos para el código (que puede
extenderse hasta 1 Mb) y cortos para los datos: la pila y los datos juntos no
pueden exceder de 64 Kb.
COMPACT: Al contrario que el anterior, este modelo es el apropiado para los
programas pequeños que emplean muchos datos. Por ello, el programa no puede
exceder de 64 Kb aunque los datos que controla pueden alcanzar el Mb, ya que
los punteros de datos son de tipo far por defecto.
LARGE: Empleado en las aplicaciones grandes y también por los programadores
de sistemas que no tienen paciencia para andar forzando continuamente el tipo de
los punteros (para rebasar el límite de 64 Kb). Tanto los datos como el código
pueden alcanzar el Mb, aunque no se admite que los datos estáticos ocupen más
de 64 Kb. Este modo es el que menos problemas da para manejar la memoria, no
siendo quizá tan lento y pesado como indica el fabricante.
HUGE: Similar al anterior, pero con algunas ventajas: por un lado, todos los
punteros son normalizados automáticamente y se admiten datos estáticos de más
de 64 Kb. Por otro, y gracias a esto último, es factible manipular bloques de datos
de más de 64 Kb cada uno, ya que los segmentos de los punteros se actualizan
correctamente. Sin embargo, este modelo es el más costoso en tiempo de
ejecución de los programas.
Usando la pila
Una sección de la memoria del programa es reservado para el uso de una pila. La
Intel 80386 y procesadores superiores contienen un registro llamado puntero a la
pila, esp, el cual almacena la dirección del tope de la pila, la figura 1 de abajo
muestra 3 valores enteros, 49, 30 y 72, almacenados en la pila(cada entero
ocupando 4 bytes) con el registro esp apuntando a la dirección del tope de la pila.
A diferencia de una pila creciendo hacia arriba, en las máquinas intel crecen hacia
abajo. En la Figura 2 muestra las capas de la pila después de la ejecución pushl
$15.
El punter de la pila es decrementado de cuatro en cuatro y el número 15 es
almacenando como lugares de 4 bytes, 1988, 1989, 1990 y 199
La instrucción popl %eax copia el valor del tope de la pila(4 bytes) a eax e
incrementa esp en 4. Qué sucede si no quieres copiar el valor del tope de la pila a
un registro?. Puedes ejecutar la instrucción addl $4, %esp el cual simplemente
incrementa el puntero de la pila.
#Listing 3
.globl main
main:
movl $10, %eax
call foo
ret
foo:
addl $5, %eax
ret
En Listing 3, la instrucción call foo pone la dirección de la instrucción después de
call en la llamada al programa sobre la pila y va hacia foo. La subrutina termina
con ret, el cual transfiere el control a la instrucción cuya dirección se toma desde el
tope de la pila. Obviamente el tope de la pila debe contener una dirección válida.
4.4 OPERADORES LENGUAJE ENSAMBLADOR
Operadores
VARIABLES GLOBALES PREDEFINIDAS INTERESANTES.
_version /* devuelve la versión del DOS de manera completa */
_osmajor /* devuelve el número principal de versión del DOS: ej., 5 en el DOS
5.0 */
_osminor /* devuelve el número secundario de versión del DOS: ej., 0 en el
DOS 5.0 */
_psp /* segmento del PSP */
_stklen /* contiene el tamaño de la pila, en bytes */
_heaplen /* almacena el tamaño inicial del heap, en bytes (0 para maximizarlo)
*/
De estas variables predefinidas, las más útiles son quizá las que devuelven la
versión del DOS, lo que ahorra el esfuerzo que supone averiguarlo llamando al
DOS o empleando la función de librería correspondiente. También es útil _psp,
que permite un acceso a este área del programa de manera inmediata.
Operadores Aritméticas, de Comparación y Ciclos
El siguiente programa realize un factorial de un número almacenado en eax. El
factorial es almacenado en ebx.
#Listing 2
.globl main
main:
movl $5, %eax
movl $1, %ebx
L1: cmpl $0, %eax //compare 0 with value in eax
je L2 //jump to L2 if 0==eax (je - jump if equal)
imull %eax, %ebx // ebx = ebx*eax
decl %eax //decrement eax
jmp L1 // unconditional jump to L1
L2: ret
L1 and L2 son etiquetas. Cuando el flujo de control llega a L2, ebx contendrá el
factorial de un número almacenado en eax.
INSERCIÓN DE CÓDIGO EN LÍNEA.
void _ _emit_ _ (argumento,...);
void geninterrupt (int interrupción);
Por medio de _ _emit_ _() se puede colocar código máquina de manera directa
dentro del programa en C. No es conveniente hacerlo así porque así, ya que
alterar directamente los registros de la CPU acabará alterando el funcionamiento
esperado del compilador y haciendo fallar el programa. Sin embargo, en un
procedimiento dedicado exclusivamente a almacenar código inline (en línea), es
seguro este método, sobre todo si se tiene cuidado de no alterar los registros SI y
DI (empleados muy a menudo por el compilador como variables de tipo register).
Por medio de geninterrupt() se puede llamar directamente a una interrupción:
geninterrupt (interr) es exactamente lo mismo que _ _emit_ _(0xCD, interr) ya que
0xCD es el código de operación de INT. Por ejemplo, para volcar la pantalla por
impresora se puede ejecutar geninterrupt(5). Con los símbolos _AX, _AL, _AH,
_BX, _BL, _BH, _CX, _CL, _CH, _DX, _DL, _DH, _SI, _DI, _BP, _SP, _CS, _DS,
_ES, _SS y _FLAGS se puede acceder directamente a los registros de la CPU.
Hay que tomar también precauciones para evitar efectos laterales (una asignación
tipo _DS=0x40 no afectará sólo a DS).
El GNU C soporta la arquitectura x86 muy bien, e incluye la habilidad de insertar
código en programas C, tal como los espacios de registros. Por supuesto, las
instrucciones son dependientes de la arquitectura.
La instrucción asm permite insertar instrucciones dentro de C y C++, como las
siguientes:
asm ("fsin" : "=t" (answer) : "0" (angle));
que es una manera de codificar en C la siguiente instrucción:
answer = sin(angle);
Se puede ver que a diferencia de ensamblador común se permite especificar
entradas y salidas usando la sintaxis de C. Pero como no se deben usar
indiscriminadamente por qué razones debemos usarlas?:
Sentencias Asm permiten acceder al hardware de la computadora
directamente. Esto puede generar que los programas se ejecuten más
rápidamente. Se puede usar cuando se escriben códigos de sistema
operativo que necesita directamente interactuar con el hardware.
Instrucciones inline también aumentan la velocidad del recorrido de ciertos
ciclos de los programas. Por ejemplo, seno y coseno de los mismos
ángulos pueden encontrarse desde fsincos x86. Probablemente, los
siguientes ejemplos ayuden a entender mejor lo expuesto en estos dos
puntos.
#Listing 11
#Name : bit-pos-loop.c
#Description : Find bit position using a loop
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
long max = atoi (argv[1]);
long number;
long i;
unsigned position;
volatile unsigned result;
for (number = 1; number <= max; ; ++number) {
for (i=(number>>1), position=0; i!=0; ++position)
i >>= 1;
result = position;
}
return 0;
}
#Listing 12
#Name : bit-pos-asm.c
#Description : Find bit position using bsrl
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
long max = atoi(argv[1]);
long number;
unsigned position;
volatile unsigned result;
for (number = 1; number <= max; ; ++number) {
asm("bsrl %1, %0" : "=r" (position) : "r" (number));
result = position;
}
return 0;
}
Compile the two versions with full optimizations as given below:
$ cc -O2 -o bit-pos-loop bit-pos-loop.c
$ cc -O2 -o bit-pos-asm bit-pos-asm.c
Mide el tiempo de ejecución de cada versión usando el comando time y
especificando el valor longitud en los argumentos de la línea de comandos para
asegurarte que cada versión toma al menos unos segundos ejecutarse.
$ time ./bit-pos-loop 250000000
and
$ time ./bit-pos-asm 250000000
El resultado puede variar en diferentes máquinas. Sin embargo se notará que la
versión que usa funciones inline se ejecuta más rápido que la otra.
Nota: El optimizador de GCC intenta reorganizar y escribir el código del programa
para minimizar el tiempo de ejecución incluso en presencia de expresiones asm.
Si el optimizador determina que la salida de un asm no es usada, la instrucción
será omitida a menos que la palabra volatile esté entre asm y sus argumentos.
Cualquier asm puede ser movido de manera que sea difícil de predecir, incluso en
saltos cruzados. La única manera de garantizar ensamble en particular con un
orden de instrucción es incluir todas las instrucciones en el mismo asm.