jueves, 16 de febrero de 2012

Curso de Ensamblador para C64: Cap. 3 - Ensamblando manualmente con BASIC


Para introducir nuestros programas en código máquina dentro de la memoria de nuestro ordenador, podemos hacerlo de varias maneras, tres de las formas más comunes son: cargar los bytes de los opcodes y operandos desde un programa cargador en BASIC, ensamblarlos mediante la ayuda de un programa monitor de código máquina, o ensamblando nuestro archivo .prg con la ayuda de un compilador de lenguaje ensamblador.



Evidentemente la forma más práctica es ésta última, y aún más versátil resulta si encima ese compilador de lenguaje ensamblador es un cross assembler (para desarrollar desde un ordenador superior).

Sin embargo de cara a tener una buena comprensión y aprender como funciona por dentro nuestro ensamblador, es muy importante que conozcáis primero como se ensambla de las formas más básicas.

Saber manejaros con estas primitivas formas de desarrollo, también os aportará práctica para depurar vuestros programas directamente en el C64 real. A parte de la corrección de posibles bugs, los "timings" y algunas cosillas podrian necesitar ser ajustadas ya que ningún emulador, por muy bueno que sea es 100% la máquina original, aunque Win Vice, por ejemplo se le acerca muchísimo (especialmente la versión para Mac). Los ejercicios que se irán viendo a lo largo del curso, funcionarán perfectamente en el emulador incluido en el IDE (el Vice).

Asi pues, por hoy, olvidaros que existen ensambladores, ¡como si no los hubieran ni inventado!, y haced frente al C64 como valientes, intentando programarlo en su lenguaje máquina nativo, y con ninguna otra herramienta más que su propio BASIC ...



Como se dijo en el anterior capítulo, cada instrucción de código máquina tiene un código numérico asociado, a menudo acompañada por uno o más números (sus operandos), cada número ocupa un byte (justo el "tamaño" de una celda de memoria).

Siendo un programa no más que una sucesión de instrucciones alojadas en la memoria (esperando ser ejecutados cuando les llegue el turno), nuestro primer paso obvio para programar en código máquina con ayuda del BASIC, está claro que consistirá en hallar la forma de transferir los bytes de nuestro programa en código máquina, a la memoria. Para ello necesitamos saber además ¿Qué bytes forman nuestro programa en CM?

Para la primera cuestión (transferir los bytes) el comando POKE viene al rescate (no obstante PEEK y POKE fueron diseñados para esto). También nos resultarán útiles las instrucciones SYS (y USR), para llamar a nuestro programa en CM, y por supuesto la instrucción DATA, para no tener que teclear cada byte con un POKE (¡que derroche de tiempo y memoria!). Ahora que ya tenemos claro como lo vamos a transferir, hablemos de los bytes en si que vamos a transferir (el código objeto del programa).

Pongámonos pues manos a la obra...

El programa de prácticas que vamos a compilar, no hará gran cosa, la verdad sea dicha, pero servirá para comprender a ensamblar de forma manual, lo cual es muy instructivo (¡os lo aseguro!).

Lo que haremos será crear un programa que sume dos números, cada uno de ellos en varias posiciones de memoria, que tranquilamente podéis llamarlas variables (asi es como son las variables en cualquier lenguaje en realidad: una simple posición de memoria que guarda un número).

Tras sumar dos variables, se almacenará el resultado en una tercera (aunque no la utilicemos en realidad) y comprobaremos si el resultado de la suma de las dos variables, es igual a otro que especificaremos en el programa. Si el resultado da = ese número, el color de fondo de la pantalla se incrementará en un bucle sin fin, de lo contrario, lo pondrá del color que indique el resultado obtenido.

Una vez presentado el programa, lo compilaremos a mano, y seguidamente cargaremos en la memoria, el "código objeto" obtenido de nuestra compilación manual, mediante un simple cargador en BASIC.

Lo primero que hay que hacer, antes de "compilar a mano" un programa en CM, es tener éste muy clarito. Para ello, en un bloc de notas, iremos anotando una tras otra, las instrucciones que componen nuestro programa. Previamente, a veces, aunque no es estrictamente necesario, en caso de rutinas/programas muy complejos, tambien podriamos haber dibujado un diagrama de bloques o un "diagrama de flujo", también llamados organigramas para ver claramente la estructura lógica del programa que queremos hacer antes de comenzar a escribirlo (esto es, su algoritmo).

En nuestro caso, éste es el programa que se ha escrito en la libreta:




Fijaos bien que en el ejemplo, hemos escrito los "mnemónicos" de las instrucciones, no los códigos de operación (a los que en realidad, representan). Éstos los veremos acto seguido, en la siguiente fase de nuestra compilación manual...

mnemónicos viene del nombre mnemotécnico que se le dió a cada código de operación, para que los técnicos los recordaran más fácilmente. Suelen ser nombres cortos, abreviaturas/siglas, de no más de tres letras que indican la función exacta de lo que hace cada instrucción.

En los primeros dias de la programación, para compilar un programa se consultaba una tabla escrita en papel (al final acababan memorizándose de tanto usarlos, doy fe de ello) para ver qué número correspondia con qué comando y lo introducian manipulando los bits de cada celda de memoria directamente: ¡Un tedioso trabajo electrónico!. Poco a poco se fueron incorporando tarjetas perforadas, consolas de visualización, y otros muchos sistemas de apoyo al pobre programador humano, y todo iba siendo cada vez más práctico.

Justo esto es lo que vamos a hacer nosotros, excepto abrir nuestros c64 y manipular los bits directamente... Por otra parte seria muy dificil, por no decir imposible, ¡debido a la miniaturización de los chips! ¡Ni lo intentéis!, ¡eh!...

Asi pues, abrid el archivo:

ASCII-opcodes reference.txt

en la carpeta "Manuales\Referencias" (C:\C64-Kickass-IDE\manuales\referencias) que se creó al instalar KICKASS IDE. Si no lo habéis hecho ya, es más que recomendable que lo instaléis, ya que os resultará más fácil seguir el curso. No obstante para eso se creó el paquete de desarollo.

En ese archivo encontraremos un listado más que útil, con varios códigos que se usan en la programación del C64, de entre los que se encuentran justamente los que necesitamos, en la columna CÓDIGO DE OPERACIÓN.
Como véis en la lista, cada mnemónico tiene un código de operación distinto para cada modo de direccionamiento. De momento no os preocupéis con esto de los "modos de direccionamiento", no tardando mucho, lo veremos con detenimiento. Ahora vamos a convertir el programa con la ayuda de esta lista. Nuestro programa empezaba en la linea 0, con org $033c. Ésto no es un mnemónico propiamente dicho, si no que es una directiva de ensamblador. Sirve para decirle al compilador (¡en este caso nosotros mismos!) cual va a ser la primera dirección de memoria donde se ensamblará el programa.

Asi pues, la directiva org, nos indica que el programa deberá ser ensamblado a partir de la dirección $033c, que corresponde precisamente al área del buffer de casette. Un lugar muy práctico para almacenar pequeños programas CM de prueba como éste, siempre con la precaución de no exceder el tamaño de este área, que comprende el rango $033c-$03fb (ó en decimal 828-1019), y de que no se va a usar el casette mientras se ejecute nuestra rutina, pues éste usa este buffer de forma temporal mientras carga los datos en la memoria.

Nuestra primera instrucción deberá ser ensamblada en esa dirección de memoria y comenzaremos a escribir el código objeto (todavia en la libreta) asi:



Lo que se hace aquí, es simplemente cargar el registro acumulador (a), con el contenido de la posición de memoria $fa, en decimal 250 (por cierto, una dirección de la famosa pág. cero, no usada por el sistema). Notad que no se va a cargar el acumulador con el valor 250, si no con lo que la dirección de memoria 250 contenga. Concretamente, se dice que se ha usado el Modo de Direccionamiento de Página Cero.


Cargar el acumulador con un número en lugar de una dirección de memoria, se indicaria con el prefijo # delante del número, y estariamos entonces usando el Modo de Direccionamiento Inmediato.

Este modo de direccionamiento recibe el nombre del hecho que el operando a usar es el número que le sigue inmediatamente al propio código de operacion. Usando este modo, se escribiria asi: lda #$fa.


Como basic no entiende el formato hexadecimal, vamos a ir convirtiendo todos los números en decimal, pero sabed que es más preferible usar el hexadecimal, ya que todo queda mucho más compacto, entre otros motivos.

Es importantísimo que comprendáis como funciona el sistema hexadecimal asi como el binario, para poder seguir el curso correctamente sin problemas. Doy por hecho que los comprendéis perfectamente, de no ser asi os recomiendo que leáis la siguiente entrada en Wikipedia:
http://es.wikipedia.org/wiki/Hexadecimal

En el mundo del C64 (y en otros también) se usa el prefijo $ para indicar que un número está en formato hexadecimal, el prefijo %, para indicar que está en binario y si a un número no le antecede ningún prefijo, se entiende que es un número decimal.


Antes de seguir con la siguiente linea, debemos saber cual será su dirección exacta... para ello deberemos mirar en la lista que mencioné lineas atrás, y observar si el operando del comando contiene uno ($FF), dos bytes ($FFFF), o ninguno. Si no contiene ninguno, la instrucción completa ocupa 1 byte en la memoria, y la siguiente dirección seria la anterior + 1 (el propio comando), si tiene un byte de operando ($FF) seria +2 (+1 del propio comando, +1 del operando) , y +3 si tiene dos bytes de operando. Tened en cuenta que en el 6510 (y en toda la familia 65xx) todos los comandos de operación ocupan sólo 1 byte. ¿No es tán complicado, verdad?

la linea anterior nos indica que la siguiente instrucción comenzará pues en 828+2, o sea 830, ya que en la lista vemos que lda $FF (que es el modo de direccionamiento que usamos en nuestro programa para esa linea) sólo tiene un operando ($FF en la lista). Por tanto la siguiente linea para nuestro borrador del código objeto, en la libreta, la escribiremos asi:




clc, borra la bandera de acarreo, y es necesario para sumar correctamente un número, ya que la instrucción de suma lo hace teniendo en cuenta el estado de esta bandera del procesador (bandera C, de Carry)

Es una instrucción que no requiere ningún operando, (se dice que está implícito, o mejor dicho que usa el Modo de Direccionamiento Implícito) y por tanto sólo ocupará el byte que ocupa el propio comando. Por ello la siguiente dirección del programa es la anterior +1, o sea 831:




adc sirve para realizar una suma entre el acumulador y una posición de memoria, añadiendo el acarreo (bandera C), es por esto último que previamante limpiamos esa bandera, con clc, para que la suma sea correcta.

Ésta y las siguientes dos lineas, si nos fijamos en la lista como antes, vemos que tambien ocupan 2 bytes (el "opcode" y su operando). El programa iria quedando asi:




sta 252 almacenará el contenido que tenga en ese momento el registro acumulador en la posición de memoria 252, y cmp #7, comparará el contenido del acumulador con el número 7, poniendo la bandera de cero (Z) = 1 si el numero comparado es igual al contenido en el acumulador, o Z = 0 en caso contrario. Ya veremos con más detalle todo esto, cuando hablemos más a fondo sobre cada instrucción y acerca de las banderas de estado.

La siguiente instrucción, beq es un poco más peliaguda de ensamblar (a mano). Pertenece al tipo de instrucciones de salto condicional y usa el modo de direccionamiento relativo, beq significa "saltar si igual" (Branch on EQual), y la igualdad a la que se refiere exactamente es si el contenido de la bandera de cero está activada (1) o no (0). En caso afirmativo, el siguiente byte del comando, indica el número de bytes a saltar (desde el PC, ó puntero de programa, del fin de la instrucción). Este byte que indica el número de bytes a saltar, es entendido como un número con signo, o lo que es lo mismo, un número binario en complemento a dos. Asi pues, si queremos compilar a mano instrucciones que usen el direccinamiento relativo, tendremos que conocer como se convierte un número en su complemento a dos. La verdad es que es muy sencillo. Para convertir un número positivo en negativo (esto se llama obtener su complemento a dos) tan sólo debemos coger el número en su versión positiva, invertir el valor de sus bits (con lo que tendriamos su complemento a uno) y le sumamos 1. Debido a que un byte con signo sólo puede alcanzar valores entre -127 y +128, esa será la distancia máxima a la que podremos "saltar".

El problema en nuestro caso actual, es que tras el beq no hemos indicado una dirección concreta, sino una etiqueta (por comodidad, a la hora de programar en papel) y todavia nos es imposible de saber el valor que tendrá la dirección a la que queremos saltar... asi que la dejamos asi, con el nombre de la etiqueta, y ya calcularemos el byte de salto relativo más adelante, cuando sepamos su valor concreto. En todo caso, si miramos la tabla, beq es una instrucción que sólo requiere un operando, asi que la siguiente instrucción empezará en PC+2.

Seguimos con la siguiente linea: sta $d020. Ya hemos visto que hace sta, almacena el contenido del acumulador en memoria. En este caso la dirección de memoria indicada en su operando ($d020), fijaos que es un número mayor de 255 (no cabe en un sólo byte), por lo cual no usa direccionamiento de pág. cero como si sucedia en el caso anterior.

En este caso se está usando el Direccionamiento Absoluto, por que usa una dirección absoluta (no relativa), concretamente la dirección $d020, ó 53280 en decimal, ¿a muchos os suena verdad?. Es la dirección de memoria que usa el chip gráfico (VICII) para mantener el color del fondo de la pantalla.

Si miramos en la tabla vemos que hay un STA $FFFF, y si recordáis, cuando el operando era $FFFF la siguiente dirección era la actual +3. Asi va quedando, entonces:




La siguiente instrucción, rts, en la linea 8, no tiene operandos por lo que la siguiente dirección es la última + 1. Esta instrucción significa "ReTurn from Soubroutine" (volver de subrutina), y basicamente funciona como RETURN en Basic, volviendo al punto del programa donde fue invocada la rutina. En las siguientes lineas, vienen otras instrucciones que tambien usan direccionamiento absoluto por lo que ya conocéis como proceder y daré el resultado directamente. El programa ya ha pasado la fase de "obtener direcciones de ensamblado":




Fijaos que ya conocemos el valor de la etiqueta BUC, es justo la dirección que ocupaba donde decia BUC: inc $d020, que es 843. La última instrucción del listado es un salto incondicional, que usa siempre direccionamiento absoluto, y los dos siguientes bytes que le siguen son efectivamente la dirección exacta a la que tendrá que saltar, algo muy distinto a expresar un número de bytes a saltar, como veiamos antes, con el direccionamiento relativo.

Esta dirección de memoria se almacenará usando el método de almacenamiento invertido, asi que en el paso final siguiente de la compilación, deberemos almacenar el número de 16 bits que compone esa dirección en el orden que dicta ese método: primero el byte bajo y luego su byte alto. Para ello resulta útil tener el número de 16 bits en hexadecimal y traducir cada byte al decimal, a la hora de compilarlo (para ponerlo en lineas DATA del BASIC). Por ejemplo $d020, se debe almacenar en memoria como $20 (32 en decimal) y luego $d0 (208 en decimal).

Pero regresemos de momento en el punto donde estabamos. Conocemos la etiqueta BUC (podemos apuntar en una esquina de la libreta, o en una página aparte BUC = 843), y ahora como decia, ya podemos calcular aquel byte del salto relativo que habiamos dejado pendiente. Si fuera direccionamiento absoluto hariamos 837 beq 843, pero las instrucciones de salto condicional no tienen un código de operación que use el dir. absoluto, asi que deberemos obtener que byte poner a continuación del beq. Para ello miramos, ¿cuantos bytes hay que saltar hasta la etiqueta BUC? La respuesta es 4 (843-839), que como es un número positivo (el salto es hacia adelante) no será necesario que calculemos su complemento a dos. Si hubiera sido un salto para atrás de 4 bytes, si que tendriamos que haber expresado -4, que en formato de byte con signo seria 252.

Ahora todo lo que queda es sustituir los mnemónicos por su código de operación (consultando la lista de códigos) y tendremos por fin, el código objeto del programa que habiamos escrito en papel, al que se denomina código fuente. La fuente de la que sale el código objeto...




Quiero recordar que se ha ido compilando en decimal, para facilitar el cargador en BASIC, que es de lo que trata el capitulo. Sin embargo fijaos que compacto y clarito queda todo en hexadecimal:




Ahora tan solo queda realizar la carga con un bucle FOR NEXT en BASIC, y en las lineas DATA pondriamos los numeros del código objeto que hemos ido obteniendo. Este seria el resultado:




para salir del bucle infinito, pulsaremos la tecla RUN/STOP + RESTORE (ESC + RE.PAG en Vice), tras lo cual podremos probar de nuevo con otros valores (el programa sigue en memoria), p.ej.) poke 250, 1: poke 251, 0: sys 828, que al no ser un resultado = 7, dejaria la pantalla del color del resultado de la suma, que es 1 (el codigo de color para el blanco), sin entar en el bucle infinito con el borde incrementando su color:




Para los que quieran ahorrarse teclear el programa cargador, aqui tienen el disco con el programa ya tecleado:

ensamblalo-cap3.d64


Podéis asimismo comprobar, que tras hacer un SYS 828, la posición de memoria 252 contiene el resultado de la suma. ¡Probadlo vosotros mismos!

Sólo necesitais ejecutar desde el mismo Basic PRINT PEEK(252) Tambien podéis cambiar el valor de 7 en la linea 5 haciendo un POKE 836, 16 por ejemplo, y veréis que ahora el bucle multicolor se ejecutará cuando la suma de las dos variables sea 16. ¡Probadlo!

A todo esto, si por casualidad habéis puesto números en las variables de los sumandos, cuyo resultado fuera mayor de 255 (el "ancho" de un byte), veréis que el resultado obtenido es erróneo. Probadlo ahora: introducid varios valores cuya suma sea mayor que 256...

Para solucionar este problemilla, obviamente deberemos trabajar con números de precisión de 16 bits (o más), lo cual se logra combinando pares de 8 bits. Lo veremos, llegado el momento...


En el próximo capítulo nos olvidaremos de este método y compilaremos un nuevo programa, esta vez con la ayuda de un monitor de código máquina, desde el cual podremos pensar ya tranquilamente en hexadecimal, además ya no tendremos que realizar tantos cálculos extra (con las etiquetas), el monitor se ocupará de ello por nosotros.

Os recomiendo que practiquéis pequeños ensamblados a mano. Para ello os propongo el siguiente ejercicio. Es muy importante que cojáis soltura y os vayáis acostumbrando a operar a este nivel. No dejéis almenos de realizar el ejercicio propuesto: ¡haciendo es como más se aprende!

Ejercicio 1. Compilar a mano el siguiente programa, realizar un cargador en basic y ejecutarlo:




Si lo hacéis bien, comprobaréis con vuestros propios ojos (nuevamente) la velocidad y potencia que puede llegar a desarrollar incluso un sencillo programa escrito en lenguaje máquina.

Además, podéis trastear con el programa todo lo que queráis (y es muy aconsejable que lo hagáis).

Por ejemplo, ¿que pasaria si cambiaramos el lugar de la etiqueta FILL poniendola en la linea 3? o ¿y si en vez de txa pongo lda #1? ¿o que pasa si cambio ldx #0, por ldx #65...? Obviamente contra más lenguaje sepáis, más cosas interesantes se os ocurrirán de cara a trastear, pero con lo que ya habéis ido aprendiendo, más la propia experimentación descubriréis que es un divertido método con el que seguir aprendiendo y consolidando conceptos.

Sin la curiosidad y la experimentación tan propias del ser humano, no habria existido nunca ciencia alguna, y sin ella hoy en dia, no existirian si quiera los ordenadores...

9 comentarios:

Bieno64 dijo...

Otro articulazo. Tendré que mirar de imprimirlo, para poder estudiarlo con tranquilidad.

Lobogris dijo...

La verdad es que impreso todo esto ganaria más. Como mínimo es recomendable por ejemplo imprimir el programa base inicial almenos para seguir la lectura de lo que se va haciendo sin tener que tirar la barra de scroll para atrás... En todo caso, va a haber un libro pdf de este curso, cuando lo termine ("encuadernado" por Josepzin) ;)

Bieno64 dijo...

Lo del libro si que es una muy buena idea !!!!

NicoGalis dijo...

Que gran noticia lo del libro, a mi tambien me gustaria tener uno cuando termine el curso.

Wood dijo...

Muy bueno Lobo, fuerza!

Lobogris dijo...

Gracias Wood! :D

Esteban dijo...

Que tal Jesus! Muy currado el IDE. No lo había descargado hasta hoy porque la última vez lo hacía todo desde Mac. Muy buenos artículos tambien. Gracias :)

Lobogris dijo...

Hola Esteban!! Aqui estamos, tirandillo como podemos, pero bien. Genial que te hayan gustado los articulos y el IDE, dentro de unos dias tengo pensado continuar el curso, que luego será editado (al acabarlo) en formato de libro electrónico. Lástima que no tenga idea de Mac, por que muchos usuarios echan a faltar el IDE para Mac...

Anónimo dijo...

Hola! Muchas gracias por este curso! Todavía no he terminado de leerlo, pero parece que se acaba en la lección 5? Está muy bien hecho, me ha inspirado para volver a usar el c64 después de 25 anios! :D

Publicar un comentario en la entrada