Tabla de Contenidos

Código Máquina y Lenguaje Ensamblador

Este capítulo está diseñado para contestar a la siguiente pregunta: ¿Qué es el código máquina y en qué se diferencia del ensamblador?.

Veremos cómo los programas ensambladores convierten nuestro código fuente en lenguaje ensamblador, en un código máquina utilizable directamente por el Spectrum, y cómo podemos cargar dicho código en el Spectrum y “ejecutarlo”.

Veremos también la sintaxis utilizada en los programas en ensamblador. Para ello comenzaremos con una definición general de la sintaxis para el ensamblador Pasmo, que será el “traductor” que usaremos entre el lenguaje ensamblador y el código máquina del Z80.

Esta entrega del curso es delicada y complicada: por un lado, tenemos que explicar las normas y sintaxis del ensamblador cruzado Pasmo antes de que conozcamos la sintaxis del lenguaje ensamblador en sí, y por el otro, no podremos utilizar Pasmo hasta que conozcamos la sintaxis del lenguaje. Esto implica que hablaremos de Pasmo comentando reglas, opciones de instrucciones y directivas que todavía no conocemos. Por eso, en ocasiones volver atrás a releer algún concepto ayudará a rellenar huecos y lagunas en la compresión global de la programación en ensamblador.


El código máquina del microprocesador Z80

El microprocesador Z80 (Z80A en el caso del Spectrum) es un pequeño chip de 40 pines de conexión, cada uno de los cuales está conectada a diferentes señales. Uno de los pines es la alimentación eléctrica, otro la conexión al reloj/cristal de 3.50Mhz, 8 pines suponen el bus de datos y 16 el bus de direcciones, etc.


| Z80A

Estas “patillas” de datos y direcciones están físicamente conectadas a través de pistas eléctricas a la memoria, el teclado, el cassette, etc. Utilizando las patillas de direcciones el procesador selecciona “posiciones de memoria” en la memoria, y recibe las instrucciones de los programas a través de las 8 señales del bus de datos.

Una señal (el estado de cada una de las patillas del micro en un instante concreto) puede tener 2 estados: sin tensión eléctrica (0 Voltios físicos, o señal lógica “0”), o con tensión eléctrica (5 Voltios físicos, o señal lógica “1”). El procesador recibe a través de las 8 patillas del bus de datos 8 señales que conforman una ristra de unos y ceros como puedan serlo 01000100 o 11001100, por ejemplo.

Los diseñadores del Z80 le otorgaron mediante circuitos en su interior una serie de registros de almacenamiento (A, B, C, D, E, F, H, L, etc.) que pueden alojar números, y la capacidad de ejecutar una serie de instrucciones (sumar, restar, comparar, etc.) entre ellos (y también entre ellos y otras posiciones de memoria).

Cada posible conjunto de señales entre 00000000 y 11111111 se corresponde con una de estas posibles operaciones mediante un “diccionario interno” que le dice al Z80 qué debe de hacer según la instrucción que se le está solicitando.

Cuando el microprocesador obtiene de la memoria la siguiente instrucción del programa a ejecutar y obtiene, por ejemplo, un conjunto de señales “01010000”, el Z80 sabe que tiene que sumar el contenido de su registro interno A con el del registro interno B, y dejar el resultado en A.

Es decir, entiende un número binario de 8 digítos que recibe en forma de señales binarias como una instrucción concreta a ejecutar. Este valor numérico es lo que se conoce como un Opcode o código de operación, ya que un código (01010000) le indica al procesador qué operación ejecutar (A = A + B).

Un programa en código máquina no es más que una ristra de código binarios de 8 dígitos (de instrucciones) que le indican al Z80 qué operaciones ejecutar en un orden concreto. El procesador leerá una a una la ristra de códigos binarios que forman el programa y ejecutará cada una de las instrucciones con que se corresponde cada código.

El Z80 utiliza un registro interno especial llamado PC (Program Counter o Contador de Programa) para saber cuál es la dirección de la instrucción actual con la que está trabajando y lo incrementa tras cada instrucción para poder seguir el flujo del programa.

Cuando arrancamos nuestro Spectrum, todos los registros del Z80 (A, B, C, PC, etc) valen 0, por lo que el Spectrum empieza a leer desde la memoria en la posición 0, instrucción tras instrucción, incrementando el valor de PC tras ejecutar cada una de ellas. Este programa “inicial” que ejecuta nuestro Spectrum es nada más y nada menos que el intérprete de BASIC, escrito para Sinclair por ingenierios de Nine Tiles Information Handlind Ltd.

Este código máquina con todo el programa que supone el intérprete BASIC está almacenado como ristra de instrucciones en un chip del Spectrum llamado ROM cuyo contenido no se borra al apagar el ordenador.

Programar en código máquina no es fácil, puesto que no es inmediata la correspondencia entre una ristra de unos y ceros y la instrucción que ejecutará el procesador. Una vez escrito un programa, es también muy complicado de depurar en busca de errores, puesto que todo lo que tenemos son miles o decenas de miles de ristras de 8 dígitos binarios.

Veamos algunas instrucciones en código máquina y el efecto que tienen en el procesador cuando le pedimos ejecutarlas:


Instrucción en hexadecimal Señales en bus de datos (binario) Instrucción ejecutada
$09 00001001 HL = HL + BC
$50 01010000 A = A + B
$3c 00111100 Incrementar A → A = A + 1
$3d 00111101 Decrementar A → A = A - 1


El conjunto completo de operaciones que puede realizar el procesador representado por los opcodes asociados a los mismos se conoce como juego de instrucciones del procesador.

Recordar todos los códigos de operación del juego de instrucciones es muy complejo y la programación en base a utilizar ristras de números es prácticamente inmanejable.

Veamos un ejemplo de programa en lenguaje máquina que rellena toda la pantalla del Spectrum con un patrón de colores:

00100001b 00000000b 01000000b 00111110b 10100010b 01110111b 00010001b 00000001b 01000000b 00000001b 11111111b 00011010b 11101101b 10110000b 11001001b

El mismo programa, representado en hexadecimal, sería:

$21, $00, $40, $3e, $a2, $77, $11, $01, $40, $01, $ff, $1a, $ed, $b0, $c9

Incomprensible en ambos casos, ¿verdad?

Este sería el resultado de ejecutar el programa anterior en un Spectrum:


Salida del programa de ejemplo

Por ejecutar, nos referimos a copiar la ristra de bytes anterior a memoria uno detrás de otro, así:

Dirección de memoria Valor en esa celdilla
40000 $21
40001 $00
40002 $40
40003 $3e
40014 $c9

Y una vez el programa está en memoria, pedirle al procesador que empiece a ejecutar instrucciones a partir del inicio del programa con un call 40000 (equivalente al GO SUB de BASIC, pero con una dirección de memoria en lugar de un número de línea).

Es evidente que, para un humano, recordar todos los “opcodes” (códigos de instrucción) y escribir los programa así es una tarea imposible. Debido a esta complejidad y dificultad, nunca se programa directamente en código máquina sino que se realiza en lenguaje ensamblador.


El lenguaje ensamblador

El lenguaje ensamblador es una “versión humana” del lenguaje máquina en la que asociamos un “nombre” (técnicamente conocido como mnenónico) a cada instrucción de 8 bits del procesador.

Así, en nuestro programa, en lugar de representar la operación A = A + B como 001010000, la representamos como add a, b, lo cual es mucho más legible e intuitivo a la hora de programar y depurar y sigue siendo igual de compacto, existiendo una correspondencia exacta de una instrucción ASM = una instrucción en código máquina.

De esta forma, podemos programar utilizando un conjunto de instrucciones en lenguaje “humano”, que no llegan a ser tan especializadas y de tanto alto nivel como en BASIC ya que el objetivo del lenguaje ensamblador es dotar de un nombre “legible” a cada microinstrucción disponible en el procesador.

Al programar en lenguaje ensamblador, lo hacemos pues en este lenguaje humano con instrucciones como add a, b, ld a, 20 o call subrutina.

Recordemos el ejemplo del apartado anterior para rellenar la pantalla con un patrón de gráficos y colores:

$21, $00, $40, $3e, $a2, $77, $11, $01, $40, $01, $ff, $1a, $ed, $b0, $c9

¿Cómo se escribiría este programa en lenguaje ensamblador? Así:

    ld hl, $4000
    ld a, $a2
    ld (hl), a
    ld de, $4001
    ld bc, $1aff
    ldir
    ret

El problema es que el microprocesador no entiende este lenguaje ensamblador, ya que él sólo entiende las señales de 8 dígitos binarios que lee de la memoria. En realidad, sólo entiende el lenguaje máquina que hemos visto en el apartado anterior, ya que lo que hace es leer valores numéricos de memoria, ver a qué opcode corresponde ese valor numérico (qué operación) y ejecutarla. No entiende lo que es el texto LD ni lo que es RET.

Para solucionar esto se necesita un programa llamado programa ensamblador o simplemente ensamblador o assembler, que lee nuestros programas en texto escritos en lenguaje ensamblador y convierte cada instrucción a su correspondiente instrucción en código máquina. El resultado de la conversión de cada instrucción se va almacenando de forma consecutiva para acabar obteniendo un bloque de datos que contiene la traducción a código máquina de todo el programa que hemos solicitado ensamblar.

Para realizar este proceso, el programa ensamblador se vale de una tabla de ensamblado que relaciona cada instrucción en ensamblador con la instrucción en código máquina que realiza la misma acción. Así, cuando lee en nuestro programa add a, b, lo traduce por un 001010000b que es lo que realmente almacena en el programa en código máquina resultante.

En resumen: como resultado de un proceso de ensamblado, el ensamblador convierte un programa en este “lenguaje ensamblador” a una ristra de dígitos binarios en memoria que se corresponden, en código máquina, con las instrucciones que nosotros hemos solicitado realizar al procesador en ensamblador. Ensamblando nuestro programa de ejemplo obtendremos un bloque de datos (en un fichero binario) con la ristra de números que hemos visto ya varias veces en los últimos ejemplos.

Una vez el programa está totalmente acabado (asumiendo que no tenga fallos y no sea necesario depurarlo) sólo es necesario realizar una vez el proceso de ensamblado. Por ejemplo, los programadores de un juego ensamblarán el listado del mismo, obtendrán una ristra de dígitos binarios en memoria, y la salvarán en cinta. Lo que se distribuye a los usuarios es el programa en código máquina que el Spectrum cargará en memoria y ejecutará.

El proceso de ensamblado puede ser manual: nosotros podemos utilizar una tabla de traducción instrucciones → opcodes y traducir manualmente cada instrucción en el opcode correspondiente. No obstante, lo más normal es utilizar un programa ensamblador, que automatiza este proceso por nosotros.

En este curso, programaremos nuestras rutinas o programas en lenguaje ensamblador en un fichero de texto con extensión .asm, y con un programa ensamblador cruzado ejecutándose en un PC o MAC lo traduciremos al código binario que entiende la CPU del Spectrum. Ese código binario puede ser ejecutado, instrucción a instrucción, por el Z80, realizando las tareas que nosotros le encomendemos en nuestro programa.

En este capítulo no vamos a ver todavía la sintaxis e instrucciones disponibles en el ensamblador del microprocesador Z80. Por ahora nos debe bastar conocer que el lenguaje ensamblador es mucho más limitado en cuanto a instrucciones que BASIC (no hay PRINT, no hay FOR, e instrucciones similares), y que, a base de pequeñas piezas, debemos montar nuestro programa entero, que será sin duda mucho más rápido en cuanto a ejecución.

Como las piezas de construcción son tan pequeñas, para hacer tareas que son muy sencillas en BASIC, en ensamblador necesitaremos muchas líneas de programa, es por eso que los programas en ensamblador en general requieren más tiempo de desarrollo y se vuelven más complicados de mantener (de realizar cambios, modificaciones) y de leer conforme crecen. Debido a esto cobra especial importancia hacer un diseño en papel de los bloques del programa (y seguirlo) antes de programar una sóla línea del mismo. También se hacen especialmente importantes los comentarios que introduzcamos en nuestro código, ya que clarificarán su lectura en el futuro. El diseño es CLAVE y VITAL a la hora de programar: sólo se debe implementar lo que está diseñado previamente, y cualquier modificación de las especificaciones debe resultar en una modificación del diseño.

Así pues, resumiendo, lo que haremos a lo largo de este curso será aprender la arquitectura interna del Spectrum, su funcionamiento a nivel de CPU, y los fundamentos de su lenguaje ensamblador, con el objetivo de programar rutinas que integraremos en nuestros programas BASIC, o bien programas completos en ensamblador que serán totalmente independientes del lenguaje BASIC.


Ejemplo: Integrar código máquina en programas BASIC

Supongamos que sabemos ensamblador y queremos mejorar la velocidad de un programa BASIC utilizando una rutina en código máquina. El lector se preguntará: “¿cómo podemos hacer esto?”.

La integración de rutinas en código máquina dentro de programas BASIC se realiza a grandes rasgos de la siguiente forma:

Primero escribimos nuestra rutina en ensamblador, por ejemplo una rutina que realiza un borrado de la pantalla mucho más rápidamente que realizarlo en BASIC, o una rutina de impresión de Sprites o gráficos, etc.

Una vez escrito el programa o la rutina, la ensamblamos (de la manera que sea: manualmente o mediante un programa ensamblador) y obtenemos en lugar del código ASM una serie de valores numéricos que representan los códigos de instrucción en código máquina que se corresponden con nuestro listado ASM.

La siguiente figura muestra a título de ejemplo parte de una tabla de ensamblado manual, como la que utilizaban en la década de los 80 y 90 los programadores que no podían comprar un software ensamblador:


Parte de una tabla de ensamblado manual

Utilizando la anterior tabla, o bien un programa ensamblador, transformamos nuestro programa ensamblador en código máquina.

Tras el proceso de ensamblado y la obtención del código máquina, nuestro programa en BASIC debe cargar esos valores en memoria (mediante LOAD “” CODE o mediante instrucciones POKE) y después saltar a la dirección donde hemos POKEADO la rutina para ejecutarla.

Veamos un ejemplo de todo esto. Supongamos el siguiente programa en BASIC, que hace lo mismo que el ejemplo que hemos visto en los 2 apartados anteriores, es decir, rellenar toda la pantalla con un patrón de píxeles determinado:

10 FOR n=16384 TO 23295
20 POKE n, 162
30 NEXT n

Si ejecutamos el programa en BASIC veremos (de nuevo) el patrón siguiente:


Salida del programa BASIC de ejemplo

Una vez tecleado y ejecutado el programa, si medimos el tiempo necesario para “dibujar” toda la pantalla obtendremos que tarda aproximadamente 1 minuto y 15 segundos.

A continuación tomemos el mismo programa escrito en lenguaje ensamblador:

; Listado 2: Rellenado de pantalla
    ORG 40000
 
    ld hl, $4000
    ld a, $a2
    ld (hl), a
    ld de, $4001
    ld bc, $1aff
    ldir
    ret

Si ensamblamos este programa con un programa ensamblador y lo ejecutamos, veremos que tarda menos de 1 segundo en ejecutar la misma tarea. El tiempo de ejecución es prácticamente inapreciable. Es en ejemplos tan sencillos como este donde podemos ver la diferencia de velocidad entre BASIC y ASM.

Supongamos que ensamblamos a mano el listado anterior, mediante una tabla de conversión de Instrucciones ASM a Códigos de Operación (opcodes) del Z80, ensamblando manualmente (tenemos una tabla de conversión en el mismo manual del +2, por ejemplo).

Ensamblar a mano, como ya hemos dicho, consiste en escribir el programa y después traducirlo a códigos de operación consultando una tabla que nos dé el código correspondiente a cada instrucción en ensamblador. Utilizamos cualquier tabla de ensamblado manual (ya sea online, o la que incluye el propio manual del +2A, por ejemplo) y obtendremos el siguiente código máquina (una rutina de 15 bytes de tamaño):

$21, $00, $40, $3e, $a2, $77, $11, $01, $40, $01, $ff, $1a, $ed, $b0, $c9

Si no queremos ensamblar a mano podemos ensamblar el programa con un ensamblador como pasmo (pasmo --bin ejemplo.asm ejemplo.bin) y después obtener esos números abriendo el fichero .bin resultante con un editor hexadecimal (que no de texto), para poder obtener los valores numéricos de cada byte del fichero.

Como ya hemos visto en la definición de “código máquina”, esta extraña ristra de bytes para nosotros incomprensible tiene un total significado para nuestro Spectrum: cuando él encuentra, por ejemplo, los bytes $3e $a2, sabe que eso quiere decir ld a, $a2; cuando encuentra el byte $c9, sabe que tiene que ejecutar un RET, y así con todas las demás instrucciones.

A continuación vamos a BASIC y tecleamos el siguiente programa:

10 CLEAR 39999
20 DATA 33, 0, 64, 62, 162, 119, 17, 1, 64, 1, 255, 26, 237, 176, 201
30 FOR n=0 TO 14
40 READ I
50 POKE (40000+n), I
60 NEXT n

El objetivo de este programa es guardar a partir de la dirección 40000 los diferentes bytes del DATA (usando POKE), almacenando así nuestra rutina pre-ensamblada, en memoria.

Una vez tecleado el programa, ejecutamos:

RUN

Tras ejecutar el programa, si examinamos la memoria del Spectrum a partir de la dirección 40000 veremos los siguientes valores:

Dirección Valor Significado
40000 $21 ld hl,
40001 $00 parte baja de $4000
40002 $40 parte alta de $4000
40003 $3e ld a,
40004 $a2 $a2
40014 $c9 ret

Ya tenemos nuestro programa en memoria, a partir de la dirección 40000. Nótese la pecularidad de que los números de 16 bits se escriben en memoria al revés, es decir, para almacenar $4000, el Z80 guarda primero la parte baja ($00) y luego la alta ($40). Más adelante hablaremos sobre esta característica del Z80.

Lo importante es que en este momento, nuestro programa BASIC ha cargado el código máquina en memoria y podemos ejecutarlo con:

RANDOMIZE USR 40000

El comando RUN ejecutó el código BASIC (el cual POKEa en memoria el código ensamblador a partir de la dirección 40000) y el RANDOMIZE USR 40000 lo que provoca es la ejecución de la rutina posicionada en dicha dirección, que justo es la rutina que hemos ensamblado a mano y pokeado mediante el programa en BASIC.

Lo que hemos hecho en este ejercicio es:



Siguiendo este mismo procedimiento podemos generar todas las rutinas que necesitemos y ensamblarlas, obteniendo ristras de código máquina que meteremos en un o varias sentencias DATA y pokearemos en memoria.

Otra opción, para evitar DATA y POKE, es grabar en cinta el fichero BIN resultante del ensamblado (convertido a TAP) tras nuestro programa en BASIC, y realizar en nuestro programa un LOAD “” CODE DIRECCION_DESTINO de forma que carguemos todo el código binario ensamblado en memoria.

Podemos así realizar muchas rutinas en un mismo fichero ASM y ensamblarlas y cargarlas en memoria de una sola vez. Tras tenerlas en memoria, tan sólo necesitaremos saber la dirección de inicio de cada una de las rutinas para llamarlas con el RANDOMIZE USR DIRECCION_RUTINA correspondiente en cualquier momento de nuestro programa BASIC.

Para hacer esto, ese fichero ASM podría tener una forma como la siguiente:

    ; La rutina 1
    ORG 40000
 
rutina1:
    (...)
    ret
 
    ; La rutina 2
    ORG 41000
 
rutina2:
    (...)
    ret

También podemos ensamblarlas por separado y después cargarlas con varios LOAD “” CODE en las diferentes direcciones.

Hay que tener mucho cuidado a la hora de teclear los DATA (y de ensamblar) si lo hacemos a mano, porque equivocarnos en un sólo número cambiaría totalmente el significado del programa, por lo que este no hará lo que debería haber hecho el programa correctamente pokeado en memoria.

Un detalle más avanzado sobre ejecutar rutinas desde BASIC es el hecho de que podamos necesitar pasar parámetros a una rutina, o recibir un valor de retorno desde una rutina.

Pasar parámetros a una rutina significa indicarle a la rutina uno o más valores para que haga algo con ellos. Por ejemplo, si tenemos una rutina que borra la pantalla con un determinado patrón o color, podría ser interesante poder pasarle a la rutina el valor a escribir en memoria (el patrón). Esto se puede hacer de muchas formas: la más sencilla sería utilizar una posición libre de memoria para escribir el patrón, y que la rutina lea de ella. Por ejemplo, si cargamos nuestro código máquina en la dirección 40000 y consecutivas, podemos por ejemplo usar la dirección 50000 para escribir uno (o más) parámetros para las rutinas:

; Listado 3: Rellenado de pantalla
; recibiendo el patron como parametro.
 
    ORG 40000
 
    ; En vez de 162, ponemos en A lo que hay en la
    ; dirección de memoria 50000
    ld a, (50000)
 
    ; El resto del programa es igual:
    ld hl, $4000
    ld (hl), a
    ld de, $4001
    ld bc, 6911
    ldir
    ret

Nuestro programa en BASIC a la hora de llamar a esta rutina (una vez ensamblada y pokeada en memoria) haría:

    POKE 50000, 162
    RANDOMIZE USR 40000

Este código produciría la misma ejecución que el ejemplo anterior, porque como parámetro estamos pasando el valor 162, pero podríamos llamar de nuevo a la misma función en cualquier otro punto de nuestro programa pasando otro parámetro diferente a la misma, cambiando el valor de la dirección 50000 de la memoria. Esto rellenaría la pantalla con un patrón que deseemos, pudiendo ser éste diferente del utilizado en el anterior ejemplo, simplemente variando el valor pokeado en la dirección 50000 (el parámetro de la rutina).

En el caso de necesitar más de un parámetro, podemos usar direcciones consecutivas de memoria: en una rutina de dibujado de sprites, podemos pasar la X en la dirección 50000, la Y en la 50001, y en la 50002 y 50003 la dirección en memoria (2 bytes porque las direcciones de memoria son de 16 bits) donde tenemos el Sprite a dibujar, por ejemplo. Todo eso lo veremos con más detalle en posteriores capítulos. En este ejemplo hemos utilizado la dirección 50000, pero lo normal es utilizar direcciones concretas y reservadas dentro del propio programa ensamblado para asegurar que no hay colisión con otras rutinas que pueda haber o podamos necesitar instalar en la dirección 50000.

Además de recibir parámetros, puede sernos interesante la posibilidad de devolver a BASIC el resultado de la ejecución de nuestro programa. Por ejemplo, supongamos que realizamos una rutina en ensamblador que hace un determinado cálculo y debe devolver, tras todo el proceso, un valor. Ese valor lo queremos asignar a una variable de nuestro programa BASIC para continuar trabajando con él.

Un ejemplo: imaginemos que realizamos una rutina que calcula el factorial de un número de una manera mucho más rapida que su equivalente en BASIC. Para devolver el valor a BASIC en nuestra rutina ASM, una vez realizados los cálculos, debemos dejarlo dentro del registro BC justo antes de hacer el ret. Una vez programada la rutina y pokeada, la llamamos mediante:

LET VALOR=USR 40000

Con esto la variable de BASIC “VALOR” contendrá la salida de nuestra rutina (concretamente, el valor del registro BC antes de ejecutar el ret). Las rutinas sólo pueden devolver un valor (el registro BC), aunque siempre podemos (dentro de nuestra rutina BASIC) escribir valores en direcciones de memoria y leerlos después con PEEK dentro de BASIC (al igual que hacemos para pasar parámetros).


Código máquina en MICROHOBBY

Lo que hemos visto hasta ahora es que podemos programar pequeñas rutinas y llamarlas desde programas en BASIC fácilmente. Todavía no hemos aprendido nada del lenguaje en sí mismo, pero se han asentado algunos de los conceptos necesarios para los próximos capítulos.

En realidad, muchos de nosotros seguramente hemos introducido código máquina en nuestros Spectrums sin saberlo, cuando tecleabamos los listados de programa que venían en la fabulosa revista Microhobby. Muchos de los programas BASIC publicados en la revista tenían rutinas en código máquina que después eran llamadas desde el BASIC.

Algunas veces introducíamos el código máquina en forma de DATA, integrados en el programa BASIC que estábamos tecleando, pero otras lo hacíamos mediante el famoso Cargador Universal de Código Máquina (CUCM).

Para que os hagáis una idea de qué era el CUCM de Microhobby, no era más que un programa en el cual tecleabamos los códigos binarios de rutinas ASM ensambladas previamente. Se tecleaba una larga línea de números en hexadecimal agrupados juntos, y cada 10 bytes o pares de dígitos se debía introducir un número a modo de CRC que aseguraba que los 10 dígitos (20 caracteres) anteriores habían sido introducidos correctamente. Este CRC podía no ser más que la suma de todos los valores anteriores, para asegurarse de que no habíamos tecleado incorrectamente el listado.


Ejemplo de listado para el CUCM de MH

Al acabar la introducción en todo el listado en el CUCM, se nos daba la opción de grabarlo. Al grabarlo indicábamos el tamaño de la rutina en bytes y la dirección donde la ibamos a alojar en memoria (en el ejemplo de la captura, la rutina se alojaría en la dirección 53000 y tenía 115 bytes de tamaño). El CUCM todo lo que hacía era un simple:

SAVE "DATOS.BIN" CODE 53000, 115

Esto grababa el bloque de código máquina en cinta (justo tras nuestro programa en BASIC), de forma que el juego en algún momento cargaba esta rutina con LOAD “” CODE, y podía utilizarla mediante un RANDOMIZE USR 53000 (o, si el código contenía más de una rutina, saltando a la dirección adecuada de cada una de ellas, en este caso desde 53000 a 53114.


Ensambladores cruzados

El lector se preguntará: “Ensamblar programas a mano es muy costoso y complejo, ¿cómo vamos a ensamblar los listados que veamos a lo largo del curso, o los que yo realice para ir practicando o para que sean mis propias rutinas o programas?”.

Sencillo: lo haremos con un programa ensamblador cruzado. Un ensamblador cruzado nos permitirá programar en un PC o MAC (utilizando nuestro editor de textos habitual), y después ensamblar ese fichero .asm que hemos realizado, obteniendo un fichero .BIN (o directamente un .TAP), que cargar en un emulador.

Los programadores “originales” en la época del Spectrum tenían que utilizar programas ensamblador nativos como MONS y GENS para todo el proceso de desarrollo. Estos programas (que corren sobre el Spectrum) implicaban teclear los programas en el teclado del Spectrum, grabarlos en cinta, ensamblar y grabar el resultado en cinta, etc. Actualmente es mucho más cómodo usar ensambladores cruzados como los que usaremos en nuestro curso.

Hoy en día lo normal es utilizar un ensamblador cruzado que se ejecute en un sistema moderno, para permitirnos usar herramientas de edición modernas (como Visual Studio Code, o Sublime Text Editor) pero a su vez obtener como resultado del ensamblado un “binario” para ZX Spectrum. Con un ensamblador cruzado podremos programar en nuestro PC con nuestro editor de texto favorito, grabar un fichero ASM y ensamblarlo cómodamente, probándolo en un emulador, sin cintas de por medio. Tras el proceso de desarrollo, podremos llevar el programa resultante a una cinta (o disco) y ejecutarlo por lo tanto en un Spectrum real, pero lo que es el proceso de desarrollo se realiza en un PC, con toda la comodidad que eso conlleva.

En el momento de escribir estas líneas disponemos de tres opciones recomendables como ensamblador cruzado:

  1. pasmo: ensamblador cruzado opensource y multiplataforma.

  2. sjasmplus: completísimo ensamblador cruzado, en desarrollo continúo con una gran cantidad de directivas, soporte de macros, etc.

  3. z80asm: z80asm (o, mejor dicho z88dk-z80asm) es el ensamblador que viene incluído con el compilador de C Z88DK, y será el que tendremos que utilizar (en cuanto a peculiaridades concretas de su sintaxis) cuando programemos ensamblador embebido en rutinas en C, o en rutinas externas que queramos que sean compiladas y llamables desde un programa en C.

El ensamblador cruzado que usaremos en este curso es Pasmo, debido a que es muy sencillo de instalar y de utilizar, y podemos ejecutarlo tanto en Windows, como en MAC OS, como en Linux. Pasmo en su versión para Windows/DOS es un simple ejecutable (pasmo.exe) acompañado de ficheros README de información. Podemos mover el fichero pasmo.exe a cualquier directorio que esté en el PATH o directamente ensamblar programas (siempre desde la línea de comandos o CMD, no directamente mediante “doble click” al ejecutable) en el directorio en el que lo tengamos copiado.

La versión para Linux viene en formato código fuente (y se compila con un simple make) y su binario “pasmo” lo podemos copiar, por ejemplo, en /usr/local/bin.

Aunque para proyectos completos prefiero utilizar sjasmplus (proyecto en constante desarrollo y en general con más funcionalidades que pasmo), la sencillez con la que se puede ensamblar cualquier programa en pasmo y su opción de crear un TAP con cargador integrado con un simple parámetro de línea de comandos lo hace la herramienta perfecta para seguir este curso.

El lector se preguntará que cuál es la importancia de elegir uno u otro programa ensamblador, si el lenguaje ensamblador es propio del Z80 y debería ser el mismo para los 3 programas. Esto es así para las instrucciones del procesador (LD, call, PUSH, POP…) y también para las directivas más básicas del ensamblador (EQU, DB, ORG…), pero por desgracia cada uno de estos programas puede tratar las cosas de forma diferente (etiquetas temporales, macros, directivas condicionales…).

Esto implica que si utilizamos funciones “avanzadas” de sjasmplus (como MACROs en lenguaje LUA) no podremos ensamblar el programa con pasmo, por ejemplo. Algunas de esas funciones avanzadas están disponibles en varios de ellos, como las directivas INCLUDE (para incluir otro fichero ASM dentro del fichero actual) o INCBIN (para incluir un binario como un sprite o un sonido en el punto en que invocamos esta directiva), pero otras directivas que suponen ayudas a la programación pueden estar sólo disponibles en uno de ellos.

Normalmente, la elección de un ensamblador cruzado suele ser por obligación (“z80asm” si vamos a trabajar con z88dk) o por preferencia (pura elección entre “pasmo” o “sjasmplus” según nos guste más lo que nos ofrece uno u otro), pero no hay que preocuparse mucho porque lo normal es que elijamos el que elijamos, no nos encontremos ninguna limitación para programar.

Mi sugerencia es que sigas el curso con Pasmo (por la facilidad para ensamblar rutinas) y que no decidas entre pasmo y sjasmplus hasta que te enfrentes a tu primer programa “grande”. En ese momento (con los conocimientos de ensamblador ya adquiridos) es cuando tendrás la capacidad de elegir uno u otro en base a ver la documentación en línea de ambos. Intentar tomar esa decisión ahora sería absurdo, por lo que lo mejor que se puede hacer en este punto es empezar a trabajar con pasmo.



Generando código binario para programas BASIC desde Pasmo

Veamos ahora cómo se ensamblaría en pasmo el programa que vimos en el apartado de integración de BASIC y ASM.

Primero tecleamos el programa en un fichero de texto y después utilizamos pasmo para ensamblarlo de forma que no nos genere un TAP para el emulador, sino sólo el código máquina resultante:

pasmo --bin ejemplo1.asm ejemplo1.bin

Como resultado del proceso de ensamblado obtendremos un fichero .bin que contiene el código máquina que podremos utilizar directamente en los DATAs de nuestro programa en BASIC.

El fichero .bin es binario, por lo que para obtener los valores numéricos que introducir en los datas debemos utilizar un editor hexadecimal o alguna utilidad como hexdump de Linux:

$ hexdump -C ejemplo1.bin
00000000  21 00 40 3e a2 77 11 01 40 01 ff 1a ed b0 c9

Ahí tenemos los datos listos para convertirlos a decimal y pasarlos a sentencias DATA. Si el código es largo y no queremos teclear en DATAs la rutina, podemos convertir el BIN en un fichero TAP ensamblando el programa mediante:

pasmo --tap ejemplo1.asm ejemplo1.tap

Este fichero tap contendrá ahora un tap con el código binario compilado tal y como si lo hubieras introducido en memoria y grabado con SAVE “” CODE, para ser cargado posteriormente en nuestro programa BASIC con LOAD “” CODE.

Esta segunda opción (LOAD “” CODE) es la más cómoda, pues nos evita el pokeado de valores en memoria, pero implica ubicar el bloque de datos a cargar con LOAD “” CODE a continuación del programa en BASIC dentro del fichero .tap.

Para realizar esta concatenación escribimos las rutinas en un fichero .ASM y las compilamos con pasmo --tap fichero.asm bloque_cm.tap. Después, escribimos nuestro programa en BASIC y lo salvamos en cinta, obteniendo otro fichero tap (programa_basic.tap).

Tras esto tenemos que crear un TAP o un TZX que contenga primero el bloque BASIC y después el bloque de código máquina, de forma que el bloque BASIC podrá cargar el bloque de código máquina con un LOAD “” CODE DIRECCION, LONGITUD_BLOQUE_CM.

Podemos realizar esto con herramientas de gestión de ficheros TAP/TZX o, sin necesidad de utilizar emuladores o herramientas adicionales, mediante concatenación de ficheros:



Generando un binario desde un programa íntegramente en ensamblador

Si estamos realizando un programa completo en ensamblador, sin ninguna parte del mismo en BASIC, puede interesarnos generar directamente un TAP con un cargador de autoejecución (que cargue nuestro código máquina en memoria). En este caso podemos utilizar el flag --tapbas con 2 posibilidades: que cargue el código máquina en memoria pero no lo ejecute, o que lo cargue y lo ejecute.

Para la primera opción basta con que nuestro programa tenga una directiva ORG y ensamblar el programa mediante pasmo --tapbas fichero.asm fichero.tap. La opción --tapbas nos añadirá una cabecera BASIC que cargará el bloque código máquina en la dirección indicada por las sentencia ORG del programa en ensamblador (por ejemplo, 40000).

; Pruebas de ensamblador. Ensamblar con:
; pasmo --tapbas fichero.asm fichero.tap
 
    ORG 40000
 
    ld hl, 16384
    ld a, 162
    ld (hl), a
    ld de, 16385
    ld bc, 6911
    ldir
    ret

El fichero resultante del ensamblado será un TAP sin autoejecución listo para cargar en el Spectrum (o en un emulador). Cuando la carga del TAP termine no ocurrirá nada, porque sólo se habrá hecho un CLEAR y un LOAD “” CODE. Si queremos ejecutar nuestro código en cualquier momento, lo haremos manualmente tecleando RANDOMIZE USR 40000. Esto puede ser útil cuando queremos cargar el código máquina en memoria pero no ejecutarlo directamente.

Si agregamos la directiva END a nuestro programa y le especificamos la dirección de ejecución del mismo, en ese caso pasmo --tapbas agregará el RANDOMIZE USR correspondiente al listado BASIC y el programa se autoejecutará al ser cargado.

; Pruebas de ensamblador
    ORG 40000
 
    ld hl, 16384
    ld a, 162
    ld (hl), a
    ld de, 16385
    ld bc, 6911
    ldir
    ret
 
    END 40000            ; Pasmo añade RANDOMIZE USR 40000 al cargador

La sentencia END marcará pues el punto de autoejecución del programa, el valor para el RANDOMIZE USR N que pasmo introducirá en el cargador BASIC. Como ya hemos dicho, sin el END pasmo nos generará un cargador también, pero sin el RANDOMIZE USR.

El resultado del ensamblado de este ejemplo con --tapbas será directamente ejecutable en un Spectrum con un simple LOAD “”.

¿Por qué existe END si ya tenemos ORG para iniciar el inicio de nuestro programa? Muy sencillo: ORG indica la dirección de inicio del programa, el lugar en memoria a partir del cual se cargan todos los datos binarios, mientras que END indica la dirección a la que hay que saltar desde BASIC para ejecutar el programa.

Y es que la dirección de ejecución del programa no tiene por qué ser la primera del mismo. Nuestro programa podría empezar por variables y rutinas justo debajo del ORG, pero el punto principal de entrada podría ser otro, como el menú del juego:

; Pruebas de ensamblador
    ORG 40000
 
    vidas DB 3
    tiempo DB 60
 
dibujar_nave:
    ; codigo para dibujar la nave aqui
    ret
 
menu:
    ; codigo principal del programa aqui
    ret
 
    END menu
 
; Pasmo añade RANDOMIZE USR X al cargador,
; siendo X la direccion donde se ha ensamblado
; el codigo tras la etiqueta "menu"

A continuación veremos cómo seleccionar un valor adecuado para la dirección de inicio ORG.


Dónde cargar el programa y cómo ejecutarlo

En este capítulo hemos visto qué aspecto tiene un programa en lenguaje ensamblador, cómo se convierte a código máquina y cómo podemos escribir dicho programa en memoria (ya sea con POKE*DATA o con un LOAD “” CODE).

En este punto, al lector le podrían surgir dos preguntas:

  1. ¿Dónde cargo el programa? ¿En qué lugar de la memoria? ¿Dónde POKEo los datos o dónde hago el LOAD “” CODE?
  2. ¿Cuál es la mejor forma de ejecutar el programa?

Veamos la respuesta a esas preguntas:


ORG, CLEAR, RAMTOP y la memoria libre

Sea la siguiente pregunta:

¿Dónde cargo mis programas en código máquina?” es decir “¿Qué dirección elijo para ORG? ¿32000? ¿35000? ¿40000? ¿50000?”.

La respuesta a esta pregunta sería: “En cualquier dirección de la memoria libre, la que no esté siendo utilizada por BASIC ni por el Sistema, donde quepa el programa”.

La memoria libre en un Spectrum empieza allá donde acaba el intérprete de BASIC, y acaba donde están definidos los gráficos definidos por el usuario (que son los últimos 168 bytes de la memoria RAM, es decir, en un Spectrum 48K desde 65368 hasta 65535).

Para determinar un valor seguro que establecer en ORG, tenemos que decirle a BASIC que no utilice memoria por encima de la dirección donde queremos cargar nuestros programa.

Existe para eso una variable de sistema llamada RAMTOP (los 2 bytes alojados en $5cb2 o 23730) la cual indica al sistema cuál es la dirección de memoria más alta que se le permite a BASIC utilizar.

Se puede modificar la dirección de RAMTOP desde BASIC mediante el comando CLEAR dirección.

Un CLEAR dirección hace lo siguiente:



Incluso el comando NEW, que limpia la RAM del Spectrum, no tocará ni eliminará nada por encima de esa dirección, y por tanto ni eliminará nuestros programas en código máquina ni cambiará los UDG o Gráficos Definidos por el Usuario (que como hemos dicho, están ubicados al final de la RAM).

Así, cuando tengamos un programa en lenguaje ensamblador y lo queramos cargar en memoria (ya sea con POKE, o con LOAD “”), hay que empezar por decidir el lugar de la memoria en que queremos situarlo, porque nuestro programa tiene que empezar con un ORG dirección para que el programa ensamblador convierta las etiquetas de texto para los saltos o las variables “en memoria” a las direcciones correctas relativas a esa dirección de inicio. El programa deberá ser POKEado o cargado en la misma dirección de memoria que el ORG que se ha utilizado, para que las referencias coincidan. Así, si nuestro programa usa ORG 40000, deberemos cargar el binario compilado en 40000 (a menos que hayamos diseñado las rutinas para no utilizar ninguna dirección absoluta, lo que se conoce como “rutinas reubicables”).

Lo normal, como hemos visto, es que carguemos nuestro programa en una dirección de memoria X (que coincida con el ORG X del programa), y hagamos un CLEAR X-1 (siendo X el valor que hemos puesto como ORG en nuestro programa en ensamblador).

Si por ejemplo nuestro programa utiliza un ORG 40000, haremos un CLEAR 39999 en el cargador BASIC, y cargaremos el código máquina desde 40000 en adelante.

La respuesta básica e inicial para cualquiera de los ejemplos que usaremos en este curso, o las pruebas que desees realizar, es:



Nosotros, a lo largo del curso, utilizaremos diferentes direcciones de ORG/END en nuestros ejemplos, típicamente 33500, 35000, 40000 ó 50000. Prácticamente ninguno de los programas que mostraremos excederá el KiloByte de tamaño con lo que en realidad podríamos cargarlos en cualquier posición de inicio sin riesgo de que no quepan en la memoria.

No obstante, al final del curso, en el capítulo sobre Consideraciones Avanzadas, veremos cómo seleccionar el valor de ORG más adecuado para exprimir al máximo la memoria del Spectrum, colocando el programa lo más bajo posible en memoria para disponer de la mayor cantidad de memoria posible para datos y código. Esto sólo es necesario si estamos haciendo un juego o programa que sea muy exigente en cuanto a necesidades de memoria. En ese capítulo es donde se explica y justifica por qué una buena dirección mínima de ORG para un programa “final” (no para pruebas, sino para el programa o juego finalizado) sería 33500.

Por ahora, como hemos dicho, en este curso ensamblaremos y cargaremos la mayoría de nuestros programas de ejemplo (y recomendamos al lector hacerlo también para sus pruebas y programas) en cualquier posición por encima de 32768, como 33500, 35000, 40000 o 50000. Verás diferentes valores en diferentes ejemplos, sin que se haya elegido ese valor por ningún motivo en concreto, ya que el programa cabrá en memoria y se ejecutará correctamente en cualquiera de ellos.


Cómo cargar y ejecutar nuestros programas en Código Máquina

Una vez hemos decidido dónde vamos a cargar el código máquina, veamos cómo podemos cargarlo y cómo podemos ejecutarlo.

Usaremos un ejemplo con un pequeño programa de 4 bytes que vamos a cargar a partir de 40000:

    ORG 40000
 
    ld bc, 99
    ret

Ensamblamos el programa y obtenemos los siguientes dígitos en decimal:

1, 99, 0, 201

Vamos a introducirlos en la memoria del Spectrum:

10 CLEAR 39999
20 DATA 1, 99, 0, 201
30 FOR n=0 TO 4
40 READ I
50 POKE (40000+n), I
60 NEXT n

La orden CLEAR 39999 reserva la memoria a partir de de la dirección siguiente (40000), donde podemos pokear nuestro programa con READ y POKE.

Para ejecutar el programa de código máquina vamos a utilizar la función USR seguida de la dirección a la que “saltar”. Como queremos imprimir el resultado de la rutina (el valor devuelto en bc), podemos hacer:

PRINT USR 40000

(lo cual devolverá por pantalla el valor 99).

El comando USR dirección causa que BASIC le ceda el control a la rutina C.M. en la dirección indicada. Cuando la rutina vuelve al BASIC (porque se ejecuta un ret) el número en el registro BC se devuelve al BASIC como un valor, y por eso se imprime 99 por pantalla (es lo que ha hecho nuestro ld bc, 99 + RET).

En nuestro ejemplo hemos combinado PRINT con USR, lo cual significa:

PRINT USR dirección

Saltamos a la rutina en código máquina en la dirección indicada, y al finalizarse esta con un RET, por pantalla aparecerá el valor que hayamos dejado en el registro BC antes de ejecutarse ese ret, es decir, el valor de retorno de la subrutina. Esto es válido para pruebas, ya que normalmente no querremos que aparezca por pantalla nada como resultado de llamar a rutinas.

Si no queremos imprimir el valor retornado por BC sino sólo ejecutarlo, también podemos usar:

RANDOMIZE USR dirección

RANDOMIZE es un comando que se utiliza para definir la semilla para los números aleatorios (una variable del sistema en BASIC). Combinarlo con USR produce que se ejecute la rutina de la dirección de memoria indicada y que el valor devuelto por USR se use como semilla para números aleatorios. Esta es la forma más habitual de llamarlo desde programas cargadores BASIC que lanzan programas totalmente escritos en código máquina y que no vuelven a BASIC.

Como veremos más adelante, si estamos haciendo un programa mixto BASIC con llamadas a rutinas código máquina, este tipo de llamadas altera el valor de la semilla de números aleatorios, por lo que para programas mixtos se suele utilizar una variable temporal de forma que no aparezca nada por pantalla (como con PRINT) ni se altere la semilla (como con RANDOMIZE):

LET Z=USR dirección

Lo normal, dado que a lo largo del curso no mezclaremos BASIC con Ensamblador, será ver llamadas RANDOMIZE USR dirección desde cargadores BASIC creados sólo para poner nuestro código máquina en memoria y saltar a él.


Cómo guardar un programa CM de la memoria a cinta

Podemos grabar el programa en Código Máquina que acabamos de “POKEar” de la siguiente forma:

SAVE "nombre" CODE 40000,4

Este programa puede ser cargado en cualquier momento con LOAD “” CODE, pero no se autoejecutará. Para eso, deberíamos haber guardado primero en la cinta un pequeño programa cargador como:

10 CLEAR 39999
20 LOAD "" CODE 40000,4
30 PRINT USR 40000

Este cargador BASIC se debería grabar en cinta inmediatamente antes del código máquina, con:

SAVE "cargador" LINE 0

Y después de haberlo grabado, grabar el código máquina en la cinta, después del cargador, con:

SAVE "cm" CODE 40000, 4

Ahora tenemos una cinta (o un tap) que tiene al principio de ella un cargador BASIC, y después un bloque de código máquina.

Si ejecutamos un LOAD “cargador, se cargará y lanzará el cargador BASIC, el cual carga el bloque código máquina en memoria y lo ejecuta con el PRINT USR.


Aspecto de un programa en ensamblador

Antes de ver cuáles son las normas que debemos seguir al escribir programas en lenguaje ensamblador, eamos un ejemplo de programa para familiarizarnos con el aspecto del código con el que trabajaremos. Se han añadido comentarios para señalar algunos de los conceptos que veremos después al describir las normas que tiene este lenguaje:

; Programa de ejemplo para un programa típico en ensamblador para Pasmo.
; Copia una serie de bytes a la videoRAM con instrucciones simples.
 
    ORG 40000
 
    ; Aqui empieza nuestro programa que copia los
    ; 7 bytes desde la etiqueta "datos" hasta la
    ; videomemoria ([16384] en adelante).
 
    ld hl, destino                 ; HL = destino (VRAM)
    ld de, datos                   ; DE = origen de los datos
    ld b, 6                        ; numero de datos a copiar
 
bucle:                             ; etiqueta que usaremos luego
 
    ld a, (de)                     ; Leemos un dato de [DE]
    add a, valor                   ; Le sumamos 1 al dato leído
    ld (hl), a                     ; Lo grabamos en el destino [HL]
    inc de                         ; Apuntamos al siguiente dato
    inc hl                         ; Apuntamos al siguiente destino
 
    djnz bucle                     ; Equivale a:
                                   ; B = B-1
                                   ; if (B>0) goto Bucle
    ret
 
valor     EQU  1
destino   EQU  18384
 
datos     DEFB 127, %10101010, 0, 128, $fe, %10000000, 00h
 
    END

Algunos detalles que podemos ver al observar el programa:



Podéis ensamblar el ejemplo anterior con pasmo mediante:

pasmo --tapbas ejemplo.asm ejemplo.tap

Una vez cargado y ejecutado el TAP en el emulador de Spectrum, podréis ejecutar el código máquina en BASIC con un RANDOMIZE USR 40000, y deberéis ver una pantalla como la siguiente:


Salida del ejemplo 1

Los píxeles que aparecen en el centro de la pantalla (dirección de memoria 18384) se corresponden con los valores numéricos que hemos definido en “datos”, ya que los hemos copiado desde “datos” hasta la videomemoria. No os preocupéis por ahora si no entendéis alguna de las instrucciones utilizadas, las iremos viendo poco a poco y al final tendremos una visión global y concreta de todas ellas.

Si cambiáis el END al final del programa por END 40000, no tendréis la necesidad de ejecutar RANDOMIZE USR 40000 ya que pasmo lo introducirá en el listado BASIC de “arranque” que genera de forma automática con el flag –tapbas.


Sintaxis del lenguaje ASM en Pasmo

En este curso vamos a utilizar Pasmo como ensamblador cruzado para generar “ejecutables” para Spectrum (ficheros TAP) a partir de nuestro código en ensamblador. Este programa ensamblador (o simplemente ensamblador) traduce nuestros ficheros de texto .asm con el código fuente del programa (en lenguaje ensamblador) a ficheros .bin (o .tap) que contendrán el código máquina directamente ejecutable por el Spectrum.

Para seguir el curso deberéis instalar Pasmo (ya sea la versión Windows o la de UNIX/Linux) en vuestro sistema y saber utilizarlo de forma básica (bastará con saber realizar un simple ensamblado de programa con los comandos que iremos detallando en algunos de los ejemplos). También necesitaréis un emulador para ejecutar el fichero resultante.

El ciclo de desarrollo con Pasmo será el siguiente:



Con esto, ya sabemos ensamblar programas creados adecuadamente, de modo que la pregunta es: ¿cómo debo escribir mi programa para que Pasmo pueda ensamblarlo?

Es sencillo: escribiremos nuestro programa en un fichero de texto con extensión .asm. En este fichero de texto se ignorarán las líneas en blanco y los comentarios, que en ASM de Z80 se introducen con el símbolo ”;“ (punto y coma), de forma que todo lo que el ensamblador encuentre a la derecha de un ; será ignorado (siempre que no forme parte de una cadena). Ese fichero de texto será ensamblado por Pasmo y convertido en código binario.

Lo que vamos a ver a continuación son las normas que debe cumplir un programa para poder ser ensamblado en Pasmo. Es necesario explicar estas reglas para que el lector pueda consultarlas en el futuro, cuando esté realizando sus propios programas. No te preocupes si no entiendes alguna de las reglas, cuando llegues al momento de implementar tus primeras rutinas, las siguientes normas te serán muy útiles:










Guía de estilo para escribir ASM

Se recomienda adoptar una “guía de estilo” para escribir código en ensamblador. Por guía de estilo entendemos el elegir un estilo único para el código (indentación, mayúsculas/minúsculas, posición de los comentarios, formato de cabecera para las rutinas, etc) y mantenerlo durante todo nuestro programa, librerías incluídas.


Para los listados y ejemplos del curso hemos utilizado el siguiente estilo para el código (siempre y cuando las limitaciones de espacio y tamaño en los ejemplos lo han permitido, en otro caso se ha modificado algún aspecto del estilo en los ejemplos):



Lo importante es que el lector encuentre su propio “código de estilo” y que lo siga en todos los programas que realice, tanto en el código principal como en las librerías que se desarrolle.


Listado de direcciones, opcodes y símbolos

Algunos programas ensambladores disponen de la utilísima opción de generar no sólo el resultado del ensamblado (un fichero .bin o .tap) sino también un fichero resumen que contiene información sobre dicho resultado.

En sjasmplus, disponemos de la opción –lst la cual genera un fichero de extensión .lst que muestra en paralelo el código del programa con la dirección resultante de cada instrucción y los opcodes que la conforman. Veamos un ejemplo:

El siguiente ejemplo de programa compara el valor de A y B e imprime en pantalla '=' si son iguales o '!' si son diferentes. Ahora mismo no debemos preocuparnos ni por las instrucciones ni por la sintaxis, símplemente lo vamos a usar como programa de ejemplo para ilustrar la generación de “listings”:

; Programa de ejemplo para comparar A y B
    ORG $8000
 
    call $0daf                    ; call CLS (borrar pantalla)
 
    ld a, (variable2)
    ld b, a                       ; B = 20
    ld a, (variable1)             ; A = 10
    cp b
    jr nz, es_diferente
 
es_igual:                         ; A = B
    ld a, '='
    rst 16
    jr fin
 
es_diferente:                     ; A != B
    ld a, '!'
    rst 16
 
fin:
    ret                           ; Fin del programa
 
variable1  DEFB  10
variable2  DEFB  20
 
    END $8000


Si ejecutamos sjasmplus –lst ejemplo_lst.asm obtenemos un fichero llamado ejemplo_lst.lst con el siguiente contenido:


# file opened: ejemplo_lst.asm
 1    0000              ; Programa de ejemplo para comparar A y B
 2    0000                  ORG $8000
 3    8000
 4    8000 CD AF 0D         call $0daf           ; call CLS (borrar pantalla)
 5    8003
 6    8003 3A 17 80         ld a, (variable2)
 7    8006 47               ld b, a              ; B = 20
 8    8007 3A 16 80         ld a, (variable1)    ; A = 10
 9    800A B8               cp b
10    800B 20 05            jr nz, es_diferente
11    800D
12    800D              es_igual:                ; A = B
13    800D 3E 3D            ld a, '='
14    800F D7               rst 16
15    8010 18 03            jr fin
16    8012
17    8012              es_diferente:            ; A != B
18    8012 3E 21            ld a, '!'
19    8014 D7               rst 16
20    8015
21    8015              fin:
22    8015 C9               ret                  ; Fin del programa
23    8016
24    8016 0A           variable1  DEFB  10
25    8017 14           variable2  DEFB  20
26    8018
27    8018                  END $8000
# file closed: ejemplo_lst.asm

En el fichero LST resultante tenemos un listado de todas las líneas del programa (columna uno), con las celdillas de memoria que ocupará cada una de esas líneas (columna 2). En la columna 3 se puede ver el opcode o dato que se ubicará en memoria y a la derecha el código ensamblador que lo ha generado.

Este tipo de ficheros es una herramienta muy útil no sólo cuando se está aprendiendo sino también para ver el “mapa de memoria” del programa.

El ensamblador pasmo tiene una opción similar con –listing (requiere la versión 0.6beta para utilizarla). Si no tenemos dicha versión, y queremos usar pasmo igualmente y no cambiar a sjasmplus, podemos usar este último simplemente para generar ficheros LST cuando necesitemos estudiarlos, y pasmo para el ensamblado “final” en sí.

El parámetro –listing nombre_de_fichero.lst debemos posicionarlo al principio del comando:

$ pasmo --listing ejemplo_lst.lst ejemplo_lst.asm ejemplo.bin

$ cat ejemplo_lst.lst
        pasmo 0.6.0.20070113.0  PAGE 1

                         Programa de ejemplo para comparar A y B
                            ORG $8000

  8000   CD 0DAF            call $0daf            call CLS (borrar pantalla)

  8003   3A 8017            ld a, (variable2)
  8006   47                 ld b, a               B = 20
  8007   3A 8016            ld a, (variable1)     A = 10
  800A   B8                 cp b
  800B   20 05              jr nz, es_diferente

                        es_igual:                 A = B
  800D   3E 3D              ld a, '='
  800F   D7                 rst 16
  8010   18 03              jr fin

                        es_diferente:             A != B
  8012   3E 21              ld a, '!'
  8014   D7                 rst 16

                        fin:
  8015   C9                 ret                   Fin del programa

  8016   0A             variable1  DEFB  10
  8017   14             variable2  DEFB  20

                            END $8000

        pasmo 0.6.0.20070113.0  PAGE S

Macros:

Symbols:
8012    es_diferente    800D    es_igual        8015    fin
8016    variable1       8017    variable2


En resumen

En esta entrega hemos definido las bases del curso de ensamblador de Z80. Hemos visto qué aspecto tiene el código en ensamblador (aunque todavía no conozcamos la sintaxis) y, muy importante, hemos visto cómo se integra este código en ensamblador dentro de programas en BASIC.

Por último, hemos conocido una utilidad (pasmo) que nos permitirá, a lo largo del curso, ensamblar todos los programas que realicemos, así como probarlos en un emulador o integrar rutinas en nuestros programas BASIC.


Ficheros


Enlaces


[ | | ]