cursos:ensamblador:ensambladores

Programas ensambladores

Ahora que ya conocemos la arquitectura del ZX Spectrum y el juego de instruccion es del microprocesador Z80, es el momento de empezar a abordar temas concretos como el acceso a las rutinas de la ROM, la lectura del teclado, la paginación 128K, o la impresión de texto y gráficos entre otros.

Para esas tareas escribiremos programas los cuales traduciremos mediante un programa ensamblador a código máquina el cual puede ser ejecutado en el Spectrum.

A lo largo del curso estamos usando pasmo como ensamblador porque, tal y como hemos visto para ensamblar alguno de los ejemplos, es muy sencillo de utilizar y basta un simple pasmo –tapbas fichero.asm fichero.tap para obtener un fichero TAP que cargar en un emulador de Spectrum o un Spectrum real.

Pasmo es perfecto para el proceso de aprendizaje e incluso, cómo no, para hacer programas y juegos completos. Es el primero de los tres ensambladores que son utilizados por los desarrolladores de Spectrum.

El segundo ensamblador es sjasmplus, el cual es más avanzado que pasmo en varios aspectos, y es el ensamblador preferido de muchos desarrolladores para hacer juegos. Así como pasmo está mantenido por una única persona (Julián Albo), sjasmplus está disponible en github, mantenido por una comunidad y por tanto en constante desarrollo. Proporciona más funcionalidades que pasmo (aunque pasmo sigue siendo la mejor opción para aprender, y desarrollar y probar rutinas en pequeños programas) y por tanto es una mejor opción para un “desarrollo final”.

El tercero es z80asm (o mejor dicho, z88dk-z80asm) y es el ensamblador que viene incluído con el compilador de C Z88DK. El compilador Z88DK básicamente lo que hace es código ensamblador que ensambla con z80asm. Si queremos escribir nuestro juego en C con partes del mismo en ensamblador, tendremos que usar z80asm.

Aunque estos 3 ensambladores comprenden los nmemónicos del Z80 (las instrucciones que hemos estado viendo en los anteriores capítulos), hay algunas pequeñas diferencias entre ellos. Por ejemplo (sólo citando dos de ellas para ver de qué tipo de diferencias hablamos):

  • pasmo y sjasmplus usan el nmenónico ex af, af' mientras que z80asm también puede utilizar ex af, af sin la comilla tras F.
  • sjasmplus soporta “Fake instructions” (expande instrucciones que no existen como ld hl, bc en ld h, b + ld l, c, por ejemplo) mientras que pasmo y z80asm no.sj

Pero la mayor incompatibilidad viene en las directivas del lenguaje ensamblador, es decir, en esas palabras clave que incluímos en el código y que no forman parte del programa final sino que son usadas por el ensamblador para generar el binario resultante. Son ejemplos de directivas ORG, EQU, END, DB/DEFB o INCLUDE, por citar algunas de las que ya hemos visto, y que sí que funcionan de la misma forma en los diferentes ensambladores.

Pero también hay directivas que funcionan de forma diferente entre ellos, o directivas que alguno de ellos tiene y que otros no.

Por ejemplo, sjasmplus tiene la directiva ALIGN que pasmo no soporta. Esta directiva es muy útil para alinear código o datos en memoria en direcciones por ejemplo múltiplos de 256, para montar tablas en memoria de forma que sólo tengamos que modificar la parte baja de la dirección porque sabemos que la parte alta no va a variar.

Estas directivas incompatibles, o directivas nuevas que ofrecen funcionalidades, hace que un fichero de código diseñado para uno de ellos no se pueda ensamblar con los otros si las usamos. No habrá problema ensamblando un programa que use ORG, EQU o DB, pero si colocamos un ALIGN en nuestro programa, pasmo dará un error al encontrar esa directiva si intentamos procesar el programa con él.

Por extensión, resulta imposible en este capítulo ver todas las directivas disponibles en los diferentes ensambladores. Para eso están disponibles sus manuales, pero vamos a ver una serie de directivas básicas pero importantes que pueden sernos útiles en nuestros programas.

Sea como sea, al final tendremos que elegir un programa ensamblador para trabajar, y por tanto tendremos que aprender a usar sus directivas y todas las funcionalidades que nos otorgue. Insistimos, nuestra recomendación para cualquier prueba básica, para seguir los ejemplos del curso y para muchas tareas es pasmo, pero también podremos usar sjasmplus para ello y de hecho, es muy probable que en algún momento para hacer algún programa más serio acabemos usando sjasmplus. Z80asm es simplemente la opción que nos veremos obligados a usar si queremos embeber ASM en C.

Los siguientes 3 apartados mostrarán lo siguiente:


  • Directivas comunes entre ensambladores: aquellas directivas de ensamblador básicas, presentes en prácticamente todos los programa ensambladores, y cuyo uso no suele variar entre ellos (tienen el mismo formato y se utilizan de la misma forma y con los mismos parámetros), como ORG, EQU, DB/DEFB/DW/DEFW, INCLUDE, etc.
  • Directivas propias de pasmo: directivas y peculiaridades de pasmo. Hablaremos de cómo se ensamblan programas en pamo y de algunas directivas que se utilizan en pasmo de diferente forma que en otros ensambladores, como las etiquetas locales.
  • Directivas propias de sjasmplus: en esta sección veremos cómo se ensamblan programas en sjasmplus para obtener ficheros TAP, algo que no es tan sencillo como en pasmo. También veremos directivas muy útiles que implementa este ensamblador y que no están presentes en otros ensambladores, como DISPLAY, IFUSED, o ALIGN, o que lo hacen de forma diferente, como las etiquetas locales.
  • Una visión general de z80asm: lo normal es utilizar z80asm sólo en aquellos casos en que vamos a programar ASM embebido dentro del compilador de C Z88DK. Debido a esto, no lo vamos a estudiar con profundidad y sólo veremos algunas directivas y datos de interés sobre él.


Una vez elegido el ensamblador que utilizaremos en nuestro proyecto se recomienda acudir a la web del mismo a consultar la documentación, ya que incluyen funcionalidades más avanzadas (que suelen resultar útiles) más allá de los detalles básicos que se describen en este capítulo.


Las siguientes directivas son comunes a los diferentes ensambladores, y se utilizan de una forma muy similar. Se suelen indentar 4 espacios desde el inicio de la línea por legibilidad, y porque sjasmplus lo requiere para muchos de ellos (mientras que pasmo no).

Ya hemos utilizado algunas de ellas en programas de ejemplo, y otras las utilizaremos en posteriores capítulos.

  • ORG dirección - Indica la dirección a partir de la cual se deben calcular las posiciones relativas del código, las etiquetas y los saltos del código que viene después de la dirección indicada. Eso quiere decir que el código que el primer opcode o dato que aparece tras el ORG se ensamblará a partir de esa dirección. Pueden haber múltiples ORG en un mismo programa.
  • END [dirección] - Indica el final del programa. Si se añade una dirección (opcional), ensambladores como pasmo la utilizarán para marcar la dirección de inicio del programa para el cargador BASIC. Esta directiva en sjasmplus no es estríctamente necesaria en sjasmplus si usamos MakeTape o SAVESNA como veremos más adelante.
  • EQU - Permite definir una constante para utilizarla a lo largo del programa (por ejemplo: VIDAS_INICIALES EQU 10). No es una variable que podamos cambiar en el código, es un valor fijo para usarlo y aumentar la legibilidad del programa. Las podemos definir al principio, al final o en cualquier parte del programa, la posición no importa porque el ensamblador las resolverá en las diferentes pasadas para “sustituir” la etiqueta por su valor.
  • INCLUDE “fichero.asm” - Esta directiva es vital porque permite separar nuestro programa en múltiples ficheros e incluirlos en ese punto del programa. Dada la naturaleza del código ensamblador (una instrucción por línea, y decenas de instrucciones para conseguir cualquier tarea básica) el código tiende a crecer y se hace costoso moverse a través de él. Lo ideal es por tanto no sólo crearse librerías que incluir, sino también separar el código en módulos, como por ejemplo:


    ORG 35000
 
    call Inicializar
 
menu:
    call Inicializar_Variables_Juego
    call Menu_Principal
 
main:
    call Juego
    call GameOver
    jr menu
 
    INCLUDE "menu.asm"
    INCLUDE "juego.asm"
    INCLUDE "utils.asm"
    INCLUDE "graficos.asm"
    INCLUDE "sonido.asm"
    INCLUDE "texto.asm"
    INCLUDE "teclado.asm"
 
    END 35000


  • INCBIN “fichero.asm” - De la misma manera que podemos incluir ficheros ASM para su compilación, podemos incluir ficheros BINARIOS (gráficos, SCR's, sonidos, o cualquier otro fichero) en cualquier punto del programa. Normalmente se utiliza con una etiqueta delante para poder referenciar a ese punto:


pantalla_menu:
    INCBIN "menu.scr"
 
graficos_jugador:
    INCBIN "graficos.bin"


  • DB/DEFB/DW/DEFW/etc. - Permiten definir datos en el punto en que se utilizan, ya sean bytes, words, un “buffer” de un tamaño concreto, etc.
  • DS/DEFS - Permite definir un espacio en el binario resultante, por ejemplo si queremos reservar un espacio en blanco, ya sea a 0 o con algún valor (haciendo crecer el tamaño del BIN resultante). El formato es DS tamaño, valor, ya sea en la misma línea o en diferente línea a la etiqueta que usaremos para referenciar a ese espacio:


; Hueco de 256 bytes relleno de ceros en nuestro binario
buffer_trabajo    DS 256, 0
 
; Otro hueco de 1024 bytes, que podemos usar para colgar la pila de él
espacio_pila:
    DS 1024, 0




Al ensamblar ficheros con pasmo, cuando el dispositivo objetivo es el Spectrum podemos elegir entre generar ficheros BIN (el código máquina ensamblado), o ficheros TAP (sin cargador, con cargador pero sin RANDOMIZE USR, o con cargador y con RANDOMIZE USR).

  • pasmo –bin fichero.asm fichero.bin - Generar un fichero binario con el código máquina del programa ensamblado.
  • pasmo –tap fichero.asm fichero.tap - Generar un fichero TAP con el código ensamblado. El fichero TAP contiene el código como si lo hubiéramos grabado en un Spectrum con SAVE “” CODE“, y suele usarse para concatenarse con otros TAPs en un TAP mayor que contenga todos los bloques de un programa.
  • pasmo –tapbas fichero.asm fichero.tap - Generar un fichero TAP con el código ensamblado y un cargador BASIC al principio del TAP. El cargador BASIC hace un CLEAR de la dirección que hemos usado en el ORG (menos uno) y después carga el código máquina en la dirección indicada por dicho ORG. Si el programa en ensamblador indica una dirección después de la directiva END, añadirá al cargador BASIC un RANDOMIZE USR dirección para que se autoejecute.

Hay dos opciones más interesantes que podemos añadir al compilar programas, y debemos ponerlas siempre como primera opción, después de “pasmo”:

  • –name nombre - Permite indicar el nombre para el cargador, en lugar del nombre por defecto que es el nombre del fichero.
  • –listing fichero.lst - Permite generar un fichero de símbolos y con información detallada de cómo se ha ensamblado cada línea de programa. Por ejemplo, el fichero de listing de un programa de ejemplo cualquiera:


$ cat ejemplo.lst
pasmo 0.6.0.20070113.0  PAGE 1

                        ORG 33500
  82DC   CD 0DAF            call CLS

  82DF   06 FF              ld b, 255
                        bucle:
  82E1   C5                 push bc
  82E2   48                 ld c, b
  82E3   06 00              ld b, 0
  82E5   CD 82F1            call PrintNum
  82E8   C1                 pop bc
  82E9   CD 8304            call PrintSpace

  82EC   10 F3              djnz bucle

  82EE   C9                 ret

  82EF   FFFF           variable      DEFW 65535
  (...)

Macros:

Symbols:
82E1    bucle           0DAF    CLS
82EF    variable        82F1    PrintNum
8304    PrintSpace      (etc...)


Tanto pasmo como sjasmplus permiten una cosa que es extremedamente útil al programar en ensamblador: las etiquetas locales.

Una etiqueta local es una etiqueta que sólo existe durante el ámbito de la anterior etiqueta global que la precede y hasta que aparezca otra etiqueta global.

En pasmo se activan ensamblando el programa con el flag –alocal y nos permiten definir etiquetas que sólo existen dentro de una determinada rutina, y así usar el mismo nombre de etiqueta en varias rutinas. Las etiquetas se marcan como locales precediéndolas por un subrayado (_).

Por ejemplo, el siguiente código sería posible con –alocal:


Rutina1:
    ...
_loop:
    ...
    jr nz, _loop
    ret
 
Rutina2:
    ...
_loop:
    ...
    jr z, _loop
    ret


Gracias a esta funcionalidad, podemos utilizar nombres de etiquetas sencillos y legibles dentro de las rutinas (como _loop, _bucle, _fin y similar).

En el caso del ejemplo anterior, la primera etiqueta _loop “existe” sólo desde la etiqueta global que la precede (Rutina1) hasta que aparece la siguiente etiqueta global, que es Rutina2.

La segunda etiqueta _loop existe desde la etiqueta que la precede (Rutina2) hasta la siguiente etiqueta global que aparezca, o hasta alguna directiva de pasmo como PROC, LOCAL, MACRO, REPT, IRP, IRPC, ENDP o ENDM.

Antes, al no poder utilizar 2 etiquetas iguales (por ser globales), teníamos que crear etiquetas del tipo Rutina1_loop o Rutina2_fin, haciéndo el código visualmente menos legible.

Cómo las etiquetas locales desaparecen al aparecer una etiqueta global, lo normal es que todas las etiquetas del interior de una rutina las hagamos locales.

Por desgracia, pasmo y sjasmplus usan formatos diferentes para definirlas de modo que un programa que las use con un determinado formato no podrá ser ensamblado por el otro ensamblador. Si en el caso de pasmo se preceden de un carácter de subrayado (_), en el caso de sjasmplus se utiliza un punto (.).


La directiva REPT N nos permite repetir N veces el código que le sigue y hasta la marca de ENDM.

Por ejemplo, supongamos la siguiente porción de código de una rutina de impresión de Sprites de 8×8 píxeles en pantalla. Para imprimir las 8 líneas del sprite, utilizaríamos un bucle similar al siguiente:


    ld b, 8                 ; 8 scanlines
 
drawsp8x8_loopLD:
    ld a, (de)              ; Tomamos el dato del sprite
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite
    inc h                   ; Incrementamos puntero en pantalla (scanline+=1)
    djnz drawsp8x8_loopLD


Como imprimir sprites es una tarea que debe de ser lo más eficiente posible, si el programa no ocupa la totalidad de la memoria y nos queda memoria libre, es probable que optemos por “desenrollar el bucle”, y reescribir la rutina de arriba así:


    ld a, (de)              ; Tomamos el dato del sprite (scanline 0)
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite
    inc h                   ; Incrementamos puntero en pantalla (scanline+=1)
 
    ld a, (de)              ; Siguiente scanline (scanline 1)
    ld (hl), a
    inc de
    inc h
 
    ld a, (de)              ; Siguiente scanline (scanline 2)
    ld (hl), a
    inc de
    inc h
 
    (...)                   ; Y así 8 veces
 
    ld a, (de)              ; Siguiente scanline (scanline 7)
    ld (hl), a
    inc de
    inc h                   ; Esto podríamos eliminarlo


El código estaría repetido 8 veces y hace que nuestro programa crezca mucho en extensión y sea más difícil de leer.

Mediante REPT y ENDM podemos hacer lo siguiente:


    REPT 8
    ld a, (de)              ; Tomamos el dato del sprite (scanlines 0-6)
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite
    inc h                   ; Incrementamos puntero en pantalla (scanline+=1)
    ENDM


Incluso, como ya no nos haría falta incrementar H al final, si nos hace falta ahorrarnos esos t-estado, podríamos hacer lo siguiente:


    REPT 7
    ld a, (de)              ; Tomamos el dato del sprite (scanline 0-6)
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite
    inc h                   ; Incrementamos puntero en pantalla (scanline+=1)
    ENDM
 
    ld a, (de)              ; Ultimo scanline (scanline 7)
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite


Nótese que REPT y ENDM no se “ejecutan” en el Spectrum sino que el ensamblador repite por nosotros el código al compilar.


Para generar un BIN en sjasmplus basta con utilizar el flag de línea de comandos –raw=fichero.bin:

  • sjasmplus ejemplo.asm –raw=ejemplo.bin - ensambla ejemplo.asm y genera un BIN con el código máquina ensamblado.

Del mismo modo que pasmo tenía un flag –listing para generar información detallada sobre el resultado del ensamblado, sjasmplus tiene –lst=fichero.lst:

  • sjasmplus ejemplo.asm –raw=ejemplo.bin –lst=ejemplo.lst - ensambla ejemplo.asm y genera un BIN con el código máquina ensamblado, y además genera el fichero detallado.

El fichero .lst resulta realmente útil para ver el código generado por el ensamblador y el mapa de memoria del programa resultante.

La manera en que se generan TAPs y SNAs con sjasmplus es algo diferente a como lo hacemos con pasmo (donde usamos un simple flag de línea de comandos).

Para generar TAPs, debemos incluir en el mismo directorio de nuestro programa el fichero TapLib.asm que viene incluído con sjasmplus en su directorio examples/TapLib.

Con el fichero TapLib.asm en el mismo directorio que nuestro programa, editamos nuestro programa y lo modificamos de la siguiente forma:

Al principio del mismo, debajo del ORG, añadimos una etiqueta que nos permita identificar el punto de inicio, llamada por ejemplo main:


    ORG 35000
 
BORDCR EQU $5c48    ; Spectrum's system variable address: BORDER colour
 
;----------------------------------------------------------------------
;-- MAIN PROGRAM (ENTRY POINT)
;----------------------------------------------------------------------
main:
    ; aquí empieza nuestro programa


Y al final del mismo, en lugar del END, añadimos lo siguiente:


;----------------------------------------------------------------------
program_length = $-main
 
    include TapLib.asm
    MakeTape ZXSPECTRUM48, "ejemplo.tap", "Ejemplo", main, program_length, main


El esqueleto de un programa en sjasmplus para generar un TAP sería similar al siguiente (pudiendo añadir EQUs en cualquier punto del fichero y datos, variables, o INCLUDEs en cualquier lugar posterior a main):


    ORG 35000
 
main:
 
    (...)
 
program_length = $-main
 
    include TapLib.asm
    MakeTape ZXSPECTRUM48, "ejemplo.tap", "Ejemplo", main, program_length, main


Mediante este procedimiento, permitimos a sjasmplus saber el punto de inicio y fin del programa y a TapLib generar un TAP con el mismo, simplemente ejecutando sjasmplus programa.asm.

También es posible generar un fichero SNA directamente en vez de un TAP, para lo cual el procedimiento es ligeramente diferente.

No es necesario en este caso utilizar el fichero TapLib.asm, sino que debemos hacer lo siguiente:

Antes del ORG, añadimos la directiva DEVICE ZXSPECTRUM48 o DEVICE ZXSPECTRUM128, según el modelo para el cual queramos generar el SNA.

Después, al igual que en el caso del TAP, al principio del programa añadimos una etiqueta que nos permita identificar el punto de inicio, llamada por ejemplo main.

Por último, al final del programa, añadimos una directiva SAVESNA.

A continuación podemos ver el esqueleto de un programa en sjasmplus diseñado para generar un SNA como resultado del ensamblado:


    DEVICE ZXSPECTRUM48
 
    ORG 35000
 
main:
 
    (...)
 
    SAVESNA "fichero.sna", main


Ahora bastará con lanzar sjasmplus fichero.asm para que se genere fichero.sna indicado con el contador de programa apuntando a main.

La directiva DEVICE activa un “emulador” embebido en sjasmplus para poder ensamblar el código del programa en la memoria de este emulador, y poder hacer un SNA de dicha memoria al acabar (recuerda que un Snapshot es la copia de la memoria del emulador).

Generar SNAs permite probar muy rápido los programas recién compilados en el emulado, porque no necesitan tiempo de carga (son de carga instantánea). No obstante, dado que el tiempo de carga de los TAPs estándar también es prácticamente cero, puede no aportar ningún tipo de valor, salvo el hecho de no necesitar tener el fichero TapLib.asm junto a nuestro programa sólo para poder generar un TAP. Podemos trabajar generando SNAs mientras desarrollamos el programa, y cambiar a TAP para generar la cinta final que distribuiremos a los usuarios.

Como se explica en la documentación de sjasmplus, también existe un DEVICE NONE de forma que podamos desactivar la introducción de “datos” en el emulador virtual en aquellas zonas de código que no queramos incluir en el SNA:


    DEVICE ZXSPECTRUM128
    ; in this device the default slot is SLOT 3 with PAGE 0 paged in.
 
    ORG 32768
main:
    (...)
 
    DEVICE NONE
    ; do something, if you don't want to corrupt virtual memory with
    ; other code, for example, loader of code.
 
    ;return to our virtual device:
    DEVICE ZXSPECTRUM128
 
    SAVESNA "snapshotname.sna", main


Suponemos que el lector entenderá en este punto porqué elegimos pasmo como ensamblador para los ejemplos de este curso. Su simplicidad para ensamblar un programa con un simple pasmo –tapbas fichero.asm fichero.tap.

No obstante, cabe decir que “preparar” nuestro fichero principal para incluir estas directivas (DEVICE + SAVESNA o TapLib.asm + MakeTape) sólo lleva unos minutos y es algo que se realiza una sola vez cuando organizamos el fichero principal de nuestro proyecto.


Como hemos visto en el caso de Pasmo, sjasmplus soporta etiquetas locales que sólo “existen” (por simplificarlo) dentro de la rutina donde las definimos. Las etiquetas locales en sjasmplus empiezan por un punto (.) y nos permiten escribir código más legible repitiendo nombres de etiquetas sin tener que andar añadiéndoles prefijos para evitar que el nombre de la etiqueta coincida con otra usada en otra parte del programa y obtengamos un error:


Rutina1:
    ...
.loop:
    ...
    jr nz, .loop
    ret
 
Rutina2:
    ...
.loop:
    ...
    jr c, .loop
.loop2:
    jr z, .loop2
    ret


Al igual que pasmo, sjasmplus tiene directivas para repetir código, pero por desgracia no se finaliza con ENDM (end-macro) sino con ENDR (END-REPT):


    REPT 8
    ld a, (de)              ; Tomamos el dato del sprite (scanlines 0-6)
    ld (hl), a              ; Establecemos el valor en videomemoria
    inc de                  ; Incrementamos puntero en sprite
    inc h                   ; Incrementamos puntero en pantalla (scanline+=1)
    ENDR


La directiva DISPLAY nos permite imprimir en pantalla mensajes en el momento de la compilación. Estos mensajes pueden llevar información sobre etiquetas/variables del programa, y podemos imprimirlas en el formato deseado.

Por ejemplo:


; Fichero ejemplo.asm
 
    ORG $8000
 
    DISPLAY "-- Comienza ensamblado --"
 
inicio:
 
    ld b, 10
bucle:
 
    djnz bucle
    ;...
    ret
 
    DISPLAY "-- Final del ensamblado --"
    DISPLAY "La direccion de la etiqueta bucle es: ",/H,bucle
    DISPLAY "La direccion de la variable es: ",/A,variable
 
variable DB 0


El formato de salida de la dirección de ensamblado de bucle sería en hexadecimal, por el /H:


$ sjasmplus bucle.asm
SjASMPlus Z80 Cross-Assembler v1.20.3 (https://github.com/z00m128/sjasmplus)
Pass 1 complete (0 errors)
Pass 2 complete (0 errors)
> -- Comienza ensamblado --
> -- Final del ensamblado --
> La direccion de la etiqueta bucle es: 0x8002
> La direccion de la variable es: 0x8005, 32773
Pass 3 complete
Errors: 0, warnings: 0, compiled: 19 lines, work time: 0.000 seconds


Los posibles formatos de salida son:

    /D - Decimal
    /B - Binario (truncado a 8 bits)
    /C - Como caracteres.
    /H - Sólo en hexadecimal.
    /A - En hexadecimal, y también en decimal separado por una coma.


Estas directivas se encuadran dentro de lo que se conoce como ensamblado condicional y lo que permiten es ensamblar o no porciones de código según si una determinada condición se cumple o no.

Podemos definir “variables” (sólo visibles para el ensamblador), dotarles de un valor, o eliminarlas, y luego ensamblar o no código según condiciones basadas en esas variables.

Veamos algunos ejemplos.

Empecemos por poner código de debug, que sólo se ensamblará cuando cierta variable esté definido.


    DEFINE MODO_DEBUG 1
 
    IF (MODO_DEBUG == 1)
      DISPLAY "Ensamblando en modo debug..."
    ENDIF
 
main:
    ; (codigo de nuestro programa...)
 
    IF (MODO_DEBUG == 1)
        ld de, mensaje
        call PrintString
        call PrintFlags
    ENDIF
 
    ; (codigo de nuestro programa...)


Con código como el anterior, podemos añadir código de debug a nuestros programas y que sólo se ensamble e incluya en el ejecutable si MODO_DEBUG está definido y vale 1. Dentro de este código de debug podemos poner etiquetas, bucles o llamadas a rutinas. Sólo se ensamblarán si MODO_DEBUG es 1. Si es 0, no aparecerá ni un byte de ese código en nuestro programa final.

Evidentemente, esta es una funcionalidad utilísima y que produce una enorme diferencia entre usar un ensamblador serio o uno sencillo que no permita este tipo de funcionalidades.

De la misma forma, podemos utilizar el resto de directivas para comprobar todo tipo de situaciones y variables, incluidas algunas predefinidas:


   IFDEF _SJASMPLUS
     ; código para SJASMPLUS
   ELSE
     ; código para otro ensamblador
   ENDIF



La directiva IFUSED es una de las más útiles de sjasmplus.

Como sabes, todo código que introducimos en un fichero ASM es ensamblado y su código máquina resultante añadido al “binario” final (ya sea BIN o TAP), aunque no llamemos a ese código en ningún lado de nuestro programa.

Lo habitual cuando estamos desarrollando programas o juegos complejos es que creemos diferentes librerías en ficheros separados (los típicos teclado.asm, graficos.asm, utils.asm, etc) y que usemos directivas INCLUDE para incluirlos en nuestro programa.

El problema de incluir una librería utils.asm es que aunque nosotros sólo necesitemos usar una función de esa librería, el ensamblador incluirá todo el código de la misma, engordando el programa con funciones que no estamos llamando y no vamos a necesitar.

La directiva IFUSED evita esta problemática, tanto con rutinas como con variables (DB/DW/etc) o buffers definidos con DS:

El formato de uso es:


Nombre_Rutina:
    IFUSED
 
    ; código de la rutina
 
    ENDIF
 
Buffer_Datos:
    IFUSED
 
    DS 256, 0
 
    ENDIF


Ahora, sjasmplus sólo ensamblará e incluirá en el BIN/TAP final el código máquina de la rutina, si esta es llamada (call, jp/jr, etc) o referenciada (ld hl, buffer_Datos).

Veamos el caso de nuestra librería utils.asm. Cada vez que la incluímos en alguno de los ejemplos del curso, estamos añadiendo a nuestro programa varios cientos de bytes que ocupa el código ensamblado de la librería. Esto ocurre por el mero hecho de hacer un INCLUDE, aunque no llamemos a ninguna función, o sólo llamemos a una de ellas.

Supongamos que modificamos nuestra librería utils.asm e incluímos directivas IFUSED/ENDIF tras el nombre de cada rutina, y hasta el final de la misma, como en el siguiente ejemplo:


;-----------------------------------------------------------------------
; PrintSpace y PrintCR: para abreviar codigo, imprimen SPACE o ENTER.
; ENTRADA, SALIDA, MODIFICA: NADA
;-----------------------------------------------------------------------
PrintSpace:
    IFUSED
    push af
    ld a, ' '
    rst 16
    pop af
    ret
    ENDIF
 
PrintCR:
    IFUSED
    push af
    ld a, 13
    rst 16
    pop af
    ret
    ENDIF


A partir de ese momento, un INCLUDE “utils.asm” incrementaría el código de nuestro programa principal en 0 bytes (si no estamos llamando a ninguna rutina).

Si llamamos a PrintSpace, por ejemplo, entonces sjasmplus compilaría sólo esa rutina y se incrementaría el tamaño del binario resultante sólo en unos bytes. Además, la rutina funcionaría, por supuesto.

Si esta rutina internamente hace uso de alguna otra rutina, y la llama o referencia, también se incluiría como dependencia.

Así, podemos tener librería que no engorden nuestros programas en más de lo necesario: sólo aquellas rutinas que llamemos o variables que referenciemos serían añadidas al “ejecutable” final.

Pasmo no tiene esta funcionalidad, y es el motivo por el cual para un proyecto grande se recomienda sjasmplus.

Podríamos simular la parte más básica de esta funcionalidad en pasmo >= 0.6 con DEFL (“define label”) y su directiva IF:


; Libreria utils.asm
 
USAR_PRINTSPACE DEFL 1
USAR_PRINTCR    DEFL 1
USAR_PRINTNUM   DEFL 1
; (etc... repetir para cada función de la librería)
 
main:
ret
 
    IF USAR_PRINTSPACE = 1
PrintSpace:
    push af
    ld a, ' '
    rst 16
    pop af
    ret
    ENDIF
 
    IF USAR_PRINTCR = 1
PrintCR:
    push af
    ld a, 13
    rst 16
    pop af
    ret
    ENDIF
 
    ; Repetir para cada función de la librería


En este caso, podríamos definir manualmente qué funciones incluír y cuáles no en nuestro programa simplemente poniendo a 1 en la cabecera de utils.asm aquellas funciones que queramos incluir en el binario final, y a 0 aquella que no queremos que se ensamblen. Esto lo haríamos en la “copia local” de nuestro utils.asm, o lo podríamos extraer a un fichero utils_config.asm que incluyemos en la librería.

Evidentemente, el método automático de sjasmplus es vital en el desarrollo de programas en un ordenador con una memoria tan limitada como el ZX Spectrum, ya que eliminará del binario final todo aquel código que no estamos usando y no necesitamos dentro del ejecutable resultante del ensamblado.


Con la directiva ALIGN N, podemos forzar a que sjasmplus ensamble el código que viene después de la directiva en una posición de memoria múltiplo de N.

Supongamos que estamos haciendo una rutina que utiliza una tabla y queremos poder acceder a esa tabla de la forma más rápidamente posible utilizando la parte baja de un registro como índice.

Podríamos hacer:


    ALIGN 256
tabla:
    INCBIN "tabla_precalculada.bin"
 
    ; también podría haber sido una tabla de 128 ceros:
    ; DS 128, 0


Si apuntamos HL a la dirección tabla, al estar alineada en un valor múltiplo de 256, desde tabla[0] hasta tabla[255], estaremos en la misma “página de 256 bytes” en la cual el valor de “H” no cambia, y podremos direccionar una posición de la tabla rápidamente simplemente variando L, lo cual será más óptimo y rápido.

La desventaja de ALIGN es que el ensamblador lo que hace es rellenar con ceros el binario hasta conseguir que el siguiente byte del programa empiece en el múltiplo deseado, por lo que engordaremos el binario entre 0 (si da la casualidad de que tabla ya estaba en un múltiplo de 256) o 255 (si tabla casualmente ha caído 1 byte después de una dirección múltiple y necesitamos avanzar 255 bytes para llegar a la siguiente).


De nuevo, pasmo no tiene esta funcionalidad, pero la podemos simular con macros:

    ; Macro de alineacion para PASMO
    align   macro value
       if $ mod value
       ds value - ($ mod value)
       endif
       endm
 
    align 256


El ensamblador z80asm es un mundo en sí mismo ya que permite generar código para gran cantidad de sistemas Z80 (desde el Z80 del Spectrum, a la variante del Z80 que utiliza la GameBoy clásica).

Tiene un formato de código bastante diferente del que utilizan pasmo y sjasmplus, por lo que si nos vemos obligados a utilizarlo, necesitaremos leer la documentación presente en la siguiente dirección:

El ensamblador z80asm soporta directivas como MACRO/ENDM, REPT/ENDR, REPTC/ENDR, REPTI/ENDR, EXITM, LOCAL, DEFL, #define y #undef para proporcionar las mismas funcionalidades que podemos disfrutar en pasmo y sjasmplus, simplemente que con una sintaxis propia.

Podemos ensamblar un programa con z88dk-z80asm -b ejemplo.asm y nos generará un fichero ejemplo.bin.

Si añadimos el flag -s (symbols) nos generará un fichero de símbolos con los símbolos usados en el programa, y con -m (map) nos generará un fichero con el “mapa” de memoria del programa, es decir, en qué dirección podemos encontrar los diferentes símbolos del programa en base al ORG del mismo.

Finalmente, si añadimos -l (listing) obtendremos un fichero .lis similar a los que generan pasmo y sjasmplus con –listing y –lst:


$ z88dk-z80asm -l -s -m -b bucle.asm

$ cat bucle.sym
bucle                           = $0002 ; addr, local, , , , bucle.asm:4
variable                        = $000a ; addr, local, , , , bucle.asm:14

$ cat bucle.map
bucle                           = $82de ; addr, local, , bucle, , bucle.asm:4
variable                        = $82e6 ; addr, local, , bucle, , bucle.asm:14
__head                          = $82dc ; const, public, def, , ,
__tail                          = $82e8 ; const, public, def, , ,
__size                          = $000c ; const, public, def, , ,

$ cat bucle.lis
bucle.asm:
     1                              ORG 33500
     2
     3  0000  06ff                  ld b, 255
     4                          bucle:
     5  0002  c5                    push bc
     6  0003  48                    ld c, b
     7  0004  0600                  ld b, 0
     8  0006  c1                    pop bc
     9
    10  0007  10f9                  djnz bucle
    11
    12  0009  c9                    ret
    13
    14  000a  ffff              variable:      DW 65535


Z80asm soporta directivas como ALIGN, que no soporta pasmo, aunque sí sjasmplus.

El uso habitual de z80asm es el de generar “librerías” para Z88DK. Para ello se crea uno o más ficheros ASM dentro de los cuales está nuestro código en ensamblador con las rutinas que queremos que sean llamables desde C. Estas rutinas (así como las variables definidas con DB que nos interesen) se exportan con la directiva PUBLIC para que después sean visibles desde C. También podemos acceder desde ASM a variables y funciones externas al mòdulo si las declaramos con EXTERN. Por ejemplo:


; Fichero libreria.asm
SECTION code_user
 
; exportamos el nombre de nuestra rutina para que sea usable desde C:
 
PUBLIC _nuestra_rutina_asm
 
; Variables o funciones externas globales de C (definidas en el
; programa en C) a las que queremos acceder desde este código ASM:
 
EXTERN _una_variable_global_de_C
EXTERN _funcion_externa_de_C
 
_nuestra_rutina_asm:
 
   ; Llamar a función C
   call _funcion_externa_de_C
 
   ; Acceder a variable C
   ld a, _una_variable_global_de_C
 
loop:
   (...)
 
   djnz loop            ; do it for all five members of array a[]
   ret


Este programa en ensamblador define una rutina la cual hace pública con PUBLIC para que sea visible a otros módulos del programa, y además declara con EXTERN una serie de símbolos externos (variables, funciones…) a los que accederá a lo largo del código.

A continuación podemos crear un programa en C que haga uso de esta librería:


// Fichero ejemplo.c
#include <stdio.h>
 
unsigned char una_variable_global_de_C = 128;
 
void funcion_externa_de_C(void)
{
   // Hacer algo 
   return;
}
 
// Declaramos en C como extern nuestra rutina ASM (externa)
extern void nuestra_rutina_asm(void);
 
main()
{
   nuestra_rutina_asm();
 
   return 0;
}


Nótese el prefijo _ (subrayado) que tienen en el fichero libreria.asm todos los símbolos que tienen que ser visibles entre C y ASM. Tanto _una_variable_global_de_C, como _funcion_externa_de_C, como _nuestra_rutina_asm lo tienen, y se utiliza dicho prefijo en el código en ASM. Estos símbolos se referencian después sin el prefijo de subrayado en el fichero ejemplo.c. Por otra parte, los símbolos que serán “internos” del programa ensamblador, como loop, se pueden escribir sin el subrayado.

Ahora podemos compilar el programa de la siguiente forma:

zcc +zx -vn -clib=new ejemplo.c libreria.asm -o ejemplo.tap

Así, podemos crear diferentes módulos y librerías para tareas que requieran la máxima velocidad (impresión de gráficos, sonido, texto, etc) y utilizarlos dentro de programas en C. Pero eso nos obliga a realizar los ficheros ASM en formato z80asm, porque como se puede apreciar en la línea que hemos usado para compilar el programa, no se incluyen como librerías externas sino que se compilan junto al propio código C.

Los diferentes programas ensambladores tienen una gran cantidad de directivas en común, pero también una serie de directivas propias de cada uno de ellos (o bien que unos las implementan y otros no, o bien que lo hacen con un formato o modo de uso diferente).

Dejando de lado z80asm, que sólo recomendamos usar si necesitamos escribir código C+ASM para Z88DK, las dos opciones que nos quedan son pasmo y sjasmplus.

Si escribimos código ASM puro, sin utilizar las ventajas que proporcionan los ensambladores, podremos ensamblar el mismo programa de una forma sencilla en cualquiera de los 2, si acaso con cambios mínimos entre ellos.

Pero en cuanto utilizamos cualquier funcionalidad especial del ensamblador, como las etiquetas locales o las macros REPT, el código ya sólo ensamblará en aquel que hayamos elegido como referencia.

Estas funcionalidades (etiquetas locales, macros, DEFINES, etc) son básicas para facilitarnos la vida cuando se programa en un lenguaje como ensamblador, así que esto implica que tenemos que elegir un programa ensamblador para nuestro código y ceñirnos a él durante todo el ciclo de vida de nuestro proyecto.

Lo hemos dicho ya varias veces: nuestro consejo para seguir el curso, escribir un pequeño fragmento de código para comprobar algo, o hacer un programa básico o medio es pasmo, pero aconsejamos cambiar a sjasmplus (por directivas como ALIGN o IFUSED) para los programas y juegos “finales” que queramos realizar. Estas dos últimas directivas pueden ser vitales en máquinas como el ZX Spectrum para optimizar rutinas y reducir el espacio que ocupan los programas.


  • cursos/ensamblador/ensambladores.txt
  • Última modificación: 24-01-2024 08:01
  • por sromero