Este sitio web usa cookies de terceros para analizar el tráfico y personalizar los anuncios. Si no está de acuerdo, abandone el sitio y no siga navegando por él. ×


Curso de Pascal. Ampliación 5. Ensamblador desde Turbo Pascal.

El ensamblador es un lenguaje de muy bajo nivel, que nos permite el máximo control del procesador y la máxima velocidad de ejecución.  Como inconveniente, es mucho más difícil de programar y depurar que los lenguajes de alto nivel como Pascal.

Para poder conseguir la máxima velocidad en los puntos críticos, sin necesidad de realizar todo el programa en ensamblador, la mayoría de los lenguajes actuales nos permiten incluir "trozos" de ensamblador en nuestros programas.
 

Ya desde las primeras versiones de Turbo Pascal podíamos incluir código máquina "en linea", entre líneas en Pascal, con la orden inline: por ejemplo para imprimir la pantalla, la secuencia de instrucciones en ensamblador sería:

 PUSH BP   ( Salva en la pila el registro BP, para que no se modifique )
 INT 5     ( Llama a la interrupcion 5, que imprime la pantalla )
 POP BP    ( Restaura el valor del registro BP )

Estas líneas, en código máquina (el ensamblador tiene una traducción casi directa) serían:

 PUSH BP   ->  $55
 INT 5     ->  $CD  $05
 POP BP    ->  $5D

Así que si introducimos esta secuencia de 4 bytes en un punto de nuestro programa, se imprimirá la pantalla.  Entonces, nos basta con hacer:

procedure PrintScreen;
begin
  inline($55/$CD/$05/$5D);
end;

Desde el cuerpo de nuestro programa escribimos "PrintScreen" y ya está.

Un comentario sobre este sistema: imprime la pantalla a través del DOS, por lo que no habrá problema si es una pantalla de texto, pero puede que nos haga falta tener cargado GRAPHICS o algún dispositivo similar si es una pantalla en modo gráfico.

 

Desde la versión 6.0 de Turbo Pascal, la cosa es aún más sencilla.  Con "inline" conseguíamos poder introducir órdenes en código máquina, pero es un sistema engorroso: tenemos que ensamblar "a mano" o con la ayuda de algún programa como DEBUG, después debíamos copiar los bytes en nuestro programa en Pascal, y el resultado era muy poco legible.  A partir de esta versión de TP, podemos emplear la orden "asm" para incluir ensamblador directamente:

procedure PrintScreen;
begin
  asm
    push bp
    int 5
    pop bp
  end
end;
 

Es decir, nos basta con encerrar entre asm y end la secuencia de órdenes en ensamblador que queramos dar.   Si queremos escribir más de una orden en una línea, deberemos separarlas por punto y coma (;), siguiendo la sintaxis normal de Pascal, pero si escribimos cada orden en una línea, no es necesario, como se ve en el ejemplo.  Los comentarios se deben escribir en el formato de Pascal: encerrados entre { y } ó (* y *).

Las etiquetas (para hacer saltos a un determinado punto de la rutina en ensamblador) pueden usar el formato de Pascal (tener cualquier nombre de identificador válido), y entonces tendremos que declararlas con label, o bien podemos emplear las llamadas "etiquetas locales", que no se pueden llamar desde fuera de la rutina en ensamblador, y que no hace falta declarar, pero su nombre debe empezar por @.  Un ejemplo puede ser la versión en ensamblador de la rutina para sincronizar con el barrido de la pantalla que vimos en la ampliación 2 ("Gráficos sin BGI"):

procedure Retrace;
begin
asm
  mov  dx, $03da
 @ntrace:                { Espera fin del barrido actual }
  in   al, dx
  test al, 8
  jnz  @ntrace
 @vtrace:                { Espera a que comience el nuevo barrido }
  in   al, dx
  test al, 8
  jz   @vtrace
end;
end;

Como son etiquetas locales, a las que sólo vamos a saltar desde dentro de este mismo procedimiento, comenzamos su nombre con @ y no necesitamos declararlas.

 

Tenemos a nuestra disposición todas las ordenes de ensamblador del 8086. También podemos acceder a las del 80286 si usamos la directiva {$G+}, y/o las del 8087 si empleamos {$N+}.  Por ejemplo, para dibujar puntos en la pantalla en modo 320x200 de 256 colores podemos usar:

{$G+}
Procedure Putpixel (X,Y : Integer; Col : Byte);
Begin
  Asm
    mov     ax,$A000
    mov     es,ax
    mov     bx,[X]
    mov     dx,[Y]
    mov     di,bx
    mov     bx, dx                  { bx = dx }
    shl     dx, 8                   { dx = dx * 256 }
    shl     bx, 6                   { bx = bx * 64 }
    add     dx, bx                  { dx = dx + bx (= y*320) }
    add     di, dx                  { Posición final }
    mov     al, [Col]
    stosb
  End;
End;

Es decir: para multiplicar por 320, no usamos las instrucciones de multiplicacion, que son lentas, sino las de desplazamiento, de modo que al desplazar 8 posiciones estamos multiplicando por 256, al desplazar 6 multiplicamos por 64, y como 256+64=320, ya hemos hallado la fila en la que debemos escribir el punto.  El {$G+} lo hemos usado porque instrucciones como "shl dx, 8" sólo están disponibles en los 286 y superiores; en un 8086 deberíamos haber escrito 8 instrucciones "shl dx,1" o haber usado el registro CL.

 

Podemos hacer una optimización: en todos los procedimientos que hemos visto, todo era ensamblador.  Esto no tiene por qué ocurrir así: podemos tener Pascal y ensamblador mezclados en un mismo procedimiento o función.  Pero cuando sea sólo ensamblador podemos emplear la directiva assembler, que permite al compilador de Turbo Pascal hacer una serie de optimizaciones cuando genera el código.  El formato de un procedimiento que emplee esta directiva es:

procedure Modo320; assembler;
asm
  mov ax,$13
  int $10
end;

(debemos indicar "assembler;" después de la cabecera, y no hacen falta el "begin" y el "end" del procedimiento).
 

Como ejemplo de todo esto, un programita que emplea estos procedimientos para dibujar unas líneas en pantalla, esperando al barrido antes de dibujar cada punto (al final de este tema hay otro ejemplo más):

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Dibujo de puntos en   }
 {    pantalla con ensam-   }
 {    blador                }
 {    GRAFASM.PAS           }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Turbo Pascal 7.0    }
 {--------------------------}

 program GrafAsm;
 {$G+}

 uses crt;

 procedure Modo320; assembler;
 asm
   mov ax,$13
   int $10
 end;

 procedure ModoTxt; assembler;
 asm
   mov ax,3
   int $10
 end;

 Procedure Putpixel (X,Y : Integer; Col : Byte); assembler;
   Asm
     mov     ax,$A000
     mov     es,ax
     mov     bx,[X]
     mov     dx,[Y]
     mov     di,bx
     mov     bx, dx                  { bx = dx }
     shl     dx, 8                   { dx = dx * 256 }
     shl     bx, 6                   { bx = bx * 64 }
     add     dx, bx                  { dx = dx + bx (= y*320) }
     add     di, dx                  { Posición final }
     mov     al, [Col]
     stosb
 end;

 procedure Retrace;
 begin
 asm
   mov  dx, $03da
  @ntrace:                { Espera fin del barrido actual }
   in   al, dx
   test al, 8
   jnz  @ntrace
  @vtrace:                { Espera a que comience el nuevo barrido }
   in   al, dx
   test al, 8
   jz   @vtrace
 end;
 end;

 var i, j: integer;

 begin
   Modo320;
   for i := 0 to 40 do
     for j := 1 to 200 do
       begin
       PutPixel(j+i*3,j,j);
       retrace;
       end;
   readkey;
   ModoTxt;
 end. 

Finalmente, también tenemos la posibilidad de usar un ensamblador externo, como Turbo Assembler (TASM, de Borland), o MASM, de Microsoft. Estos programas crean primero un fichero objeto (con extensión OBJ), antes de enlazar con el resto de módulos (si los hubiera) y dar lugar al programa ejecutable.

Pues nosotros podemos integrar ese OBJ en nuestro programa en Pascal usando la directiva {$L}, y declarando como "external" el procedimiento o procedimientos que hallamos realizado en ensamblador, así:

procedure SetMode(Mode: Word); external;
{$L MODE.OBJ}
 

 


 

Finalmente, otro ejemplo más elaborado.  Se trata de "sprites", figuras transparentes que se mueven por la pantalla.  Con "transparentes" me refiero a que si hay algún hueco, debe verse el fondo a través suyo. Esto es totalmente imprescindible en los videojuegos: por ejemplo, mientras que anda nuestro personaje, tiene que verse el fondo entre sus piernas o junto a su cabeza en vez de un fondo negro.

Está tomado de una práctica que hice para una asignatura de la Universidad, en la que teníamos que manejar la pantalla VGA en modo gráfico 320x200x256.  Como el lenguaje era libre, empleé Pascal, que es mi favorito, y como había que conseguir el menor tamaño posible (en el ejecutable) y una cierta rapidez, incluí bastantes cosas en ensamblador. Este es el resultado...

 {--------------------------}
 {  Ejemplo en Pascal:      }
 {                          }
 {    Dibujo de "sprites"   }
 {    (imágenes transpa-    }
 {    rentes) en pantalla   }
 {    NSPRITE.PAS           }
 {                          }
 {  Este fuente procede de  }
 {  CUPAS, curso de Pascal  }
 {  por Nacho Cabanes       }
 {                          }
 {  Comprobado con:         }
 {    - Turbo Pascal 7.0    }
 {--------------------------}
 program nSprite;
 {$G+ Dibuja Sprites en Pantalla }
 const
   segVideo: word = $a000;
   NumSprites = 10;           { Número de sprites }
   xSize = 30; ySize = 30;    { Tamaño de cada uno }
 type
   tipoSprite = array[1..xSize, 1..ySize] of byte;  { El sprite en sí }
 const
   sprite : tipoSprite =
    ((0,0,0,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,0),
     (0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,0,0),
     (0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3,0),
     (2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
     (2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
     (2,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,2,2,2,2,2,2,2,2,1,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,2,3,0,2,1,1,3,0,0,0,0,0,2,3,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,3,3,0,2,1,1,3,0,0,0,0,0,3,3,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,2,1,1,3,0,0,0,0,0,0,0,0,2,1,1,1,3),
     (2,1,1,1,1,3,3,3,3,3,3,3,3,1,1,1,1,3,3,3,3,3,3,3,3,1,1,1,1,3),
     (2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
     (2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
     (2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,3),
     (2,1,1,1,1,2,2,2,2,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0,0,0,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,2,1,1,1,1,1,1,1,1,2,0,0,0,0,0,0,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,2,1,1,1,1,2,0,0,0,0,0,0,0,0,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,2,2,2,2,0,0,0,0,0,0,0,0,0,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,3),
     (2,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,3),
     (2,1,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,1,1,1,1,1,3),
     (2,1,1,1,1,1,1,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,1,1,1,1,1,1,3),
     (2,1,1,1,1,1,1,1,1,3,3,3,3,3,3,3,3,3,3,3,3,1,1,1,1,1,1,1,1,2),
     (0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0),
     (0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0),
     (0,0,0,2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,0,0),
     (0,0,0,0,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,0,0,0,0)
    );
 type SprDat= record
   x, y: word;              { Coordenadas de cada sprite }
   vx, vy: shortint;        { Velocidad (incremento) según cada eje }
 end;

 { --- Componentes R,G,B para un color --- }
 procedure setpal(col,r,g,b:byte); assembler;
 asm
   mov dx,03c8h;
   mov al,col;
   out dx,al;
   inc dx;
   mov al,r; out dx,al;
   mov al,g; out dx,al;
   mov al,b; out dx,al;
 end;

 { --- Copia 64000 bytes de una pantalla virutal a otra (física) --- }
 procedure copia(org,dst:word); assembler;
 asm
   push ds;
   mov ds,[org];
   xor si,si;
   mov es,[dst]
   xor di,di;
   mov cx,320*200/2;
   rep movsw;
   pop ds;
 end;

 { --- Borra 64000 bytes en una zona de memoria --- }
 procedure cls(dst:word); assembler;
 asm
   mov es,[dst];
   xor di,di;
   xor ax,ax;
   mov cx,320*200/2;
   rep stosw;
 end;

 { --- Sincroniza con el barrido de la pantalla --- }
 procedure retrace; assembler; asm
   mov dx,03dah;
 @vert1: in al,dx;
   test al,8;
   jnz @vert1
 @vert2: in al,dx;
   test al,8;
   jz @vert2;
   end;

 { --- Escribe un sprite en la pantalla --- }
 procedure putsprite(x,y,sprseg, sprofs,virseg:word); assembler;
 asm
   push ds
   mov ds,sprseg;
   mov si,sprofs            { Segmento y desplaz. del sprite }
   mov es,virseg; xor di,di { Segmento y desp. de la pantalla virtual }
   mov ax,[y];              { Numero de fila }
   shl ax,6;
   mov di,ax;
   shl ax,2;
   add di,ax                { Fila * 320 }
   add di,[x]               { Fila * 320 + columna }
   mov dx,320-xsize         { Pixels restantes en la línea }
   mov bx,ysize             { Altura del dibujo }
  @l1:
   mov cx,xsize             { Anchura de cada fila }
  @l0:
   lodsb;                   { Leo un byte del sibujo }
   or al,al;
   jz @noDib                { Si es 0 (transp.), lo salto }
   mov [es:di],al           { Si no, lo dibujo }
  @noDib:
   inc di;                  { Siguiente pixel }
   dec cx;                  { Queda uno menos por dibujar en la fila }
   jnz @l0;                 { Si aun quedan, repito }
   add di,dx;               { Si no quedan, voy al principio de la sgte fila }
   dec bx;                  { Queda una fila menos }
   jnz @l1;                 { Repito hasta que se acaben las filas }
   pop ds
 end;

 { --- Variables que usaré en el cuerpo --- }
 var
   PantVirt: pointer;  { La pantalla virtual }
   SegVirt: word;      { Segmento donde se encuentra }
   Fondo: pointer;     { La pantalla de fondo }
   SegFon: word;       { y del fondo }
   D:                  { Datos de cada sprite }
     array[1..numSprites] of SprDat;
   i,j: integer;       { Bucles }
   label bucle;

 {---------------------------- }
 { --- Cuerpo del programa --- }
 {---------------------------- }
 begin
   asm mov ax,13h; int 10h; end;  { Cambio a modo 320x200x256 }
   randomize;                     { Números aleatorios }
   getmem(PantVirt,320*200);      { Reservo y vacío pantalla virtual }
     SegVirt := seg(PantVirt^);
     cls(SegVirt);
   getmem(Fondo,320*200);         { Y la de fondo }
     SegFon:=seg(Fondo^);
     cls(SegFon);
   for i := 1 to NumSprites do    { Datos aleatorios de los Sprites }
     with d[i] do
       begin
       x := random (219-xSize)+50;
       y := random (99-ySize)+50;
       repeat
         vx := random(6) - 3;
       until vx<>0;
       repeat
         vy := random(6) - 3;
       until vy<>0;
       end;
   for i:=1 to 128 do             { Paleta de colores del fondo }
     setpal(127+i,20+i div 5,i div 3,20+i div 7);
   SetPal(1,10,10,45);
   SetPal(2,0,0,25);
   SetPal(3,20,20,60);

   for i:=0 to 319 do             { Dibujo el patrón de fondo }
     for j:=0 to 199 do
       mem[SegFon:j*320+i]:=128+abs(i*i-j*j) and 127;
 bucle:
     copia(SegFon,SegVirt);       { Copio el fondo en la pantalla virtual }
     for i := 1 to numSprites do  { Dibujo los sprites }
       begin
       PutSprite( d[i].x, d[i].y, seg(Sprite), ofs(Sprite), SegVirt);
       inc(d[i].x,d[i].vx);           { Actualizo las coordenadas }
       if (d[i].x < 5) or (d[i].x > (315-xSize)) then
         d[i].vx := -d[i].vx;
       inc(d[i].y,d[i].vy);
       if (d[i].y < 5) or (d[i].y > (195-ySize)) then
         d[i].vy := -d[i].vy;
       end;
     retrace;                     { Sincronizo con el barrido }
     copia(SegVirt,SegVideo);     { Y copio la pantalla virtual en la visible }
   { Repito hasta que se pulse una tecla }
   asm  mov ah, 1;  int 16h;  jz bucle; end;
   { Absorbo esa pulsación de tecla }
   asm  mov ah, 0;  int 16h;  end;
   freemem(PantVirt,320*200);     { Libero la memoria reservada }
   freemem(Fondo,320*200);
   asm mov ax,3; int 10h; end;    { Vuelvo a modo texto }
 end. 

Este programa está comprobado con Turbo Pascal 7.0.  Eso sí, como reserva dos pantallas virtuales puede que no quede memoria suficiente para ejecutarlo desde el IDE normal (TURBO.EXE).  Entonces habría que usar el compilador de línea de comandos (TPC.EXE) o el de modo protegido (TPX.EXE).