domingo, 19 de enero de 2014

El ensamblador EDTASM para el μP 6809 I

Un microprocesador que fue pionero de algunas de las características que encontramos en microprocesadores más sofisticados de hoy en día fue el microprocesador 6809 fabricado por la empresa Motorola. Esta unidad de procesamiento central fue utilizada en la construcción de una computadora muy popular vendida en las tiendas de Radio Shack, la computadora TRS-80 Color Computer conocida simplemente como la CoCo:





Se trata de la primera computadora programable en una versión del lenguaje BASIC llamada Color Basic que no requería de un monitor de colores, y podía conectarse a cualquier televisor casero usando para ello una cajita que convertía la señal digital emanada de la computadora en una señal de radiofrecuencia apropiada para poder ser recibida por un televisor analógico (en ese entonces los televisores digitales que son el estándard de hoy en día aún no hacían su aparición). Esto abarató mucho las cosas, y la CoCo fue de hecho la primera computadora casera con video a colores vendida masivamente por tienda comercial alguna.

La CoCo tenía un boquete lateral que permitía inserción de cartuchos con programas de varios tipos elaborados en memoria ROM; no se requería conocimiento alguno de programación para usarlos, simplemente se insertaba el cartucho en la computadora, y el programa estaba en condiciones de ser utilizado. Uno de esos programas, sin embargo, tenía una característica muy especial: estaba elaborado para ser utilizado por personas que querían aprender a programar no en el lenguaje de alto nivel BASIC, el lenguaje nativo de la computadora, sino en lenguaje ensamblador, el lenguaje ensamblador a ser usado para producir programas en lenguaje de máquina capaces de poder ser ejecutados en la misma CoCo. El cartucho era un paquete completo que no solo incluía un programa ensamblador, incluía también un editor de texto para poder elaborar y corregir según fuese necesario el programa fuente elaborado en lenguaje assembler (de allí las iniciales EDT) así como el mismo programa ensamblador para tomar el programa escrito en assembler convirtiéndolo a lenguaje de máquina (de allí las iniciales ASM, formándose entonces el nombre EDTASM que significa “editor-ensamblador”).

Usar el editor-ensamblador EDTASM en una computadora CoCo conectada a un televisor analógico es algo sumamente sencillo. Simplemente se apaga la computadora, se inserta el cartucho ROM en la entrada lateral provista para tal efecto, se vuelve a encender la computadora, y ya está. Al encenderse la computadora, aparece lo siguiente:

   EDTASM+ 1.0
   COPYRIGHT 1981 BY MICROSOFT
   *

El asterisco (*) nos confirma que ya estamos dentro del editor, tras lo cual podemos empezar a escribir programas en el lenguaje ensamblador que es propio al microprocesador 6809 de Motorola. Escribiendo la letra “I” (usado para pedir la Inserción de caracteres de texto) y oprimiendo la tecla de entrada “Enter”:

   * I [Enter]

el editor EDTASM+ responderá imprimiendo por su cuenta el primer número de línea del programa, por omisión (default) el número asignado será 00100:

   * 00100

Este número de línea es para conveniencia del programado, y no afecta en nada el programa en lenguaje de máquina que se quiere obtener. Prácticamente todos los ensambladores que existen en la actualidad incorporan números de línea para dar mayor legibilidad a los programas y permitir una detección más rápida de los errores cometidos.

Para escribir un programa fuente en lenguaje ensamblador usando EDTASM, se recurre a cuatro campos:

  1) El campo del símbolo
  2) El campo del comando
  3) El operando (en caso de ser requerido por el comando)
  4) El comentario (opcional)

En esa computadora usamos la tecla [→] (flecha hacia la derecha) para pasar de un campo al siguiente. Una vez que hemos terminado de escribir una línea completa, oprimimos la tecla [Enter] para pasar a la siguiente línea. De este modo, podemos tener elaborado un programa como el siguiente:




Obsérvese cómo tenemos cada línea repartida en cuatro campos (lo cual hace mucho más legible el programa):




Una vez elaborado el programa, tenemos dos opciones: dándole un nombre de archivo lo podemos guardar en algún medio de almacenamiento permanente (la computadora CoCo permitía almacenar los programas en grabadores de cinta magnetofónica, hoy obsoleta), o bien lo podemos ensamblar cuando aún lo tenemos en la memoria de la computadora. Para ensamblar el programa anterior, estando aún dentro del editor, usamos el “switch” para Ensamble-en-la-Memoria (AIM, Assemble In Memory):

   * A/IM[Enter]

Esto hace que se produzca en la pantalla del televisor algo como lo siguiente:




La salida de la computadora CoCo al televisor analógico sólo tiene capacidad para imprimir en la pantalla 16 líneas de 32 caracteres por línea (una disponibilidad de 512 “posiciones de caracteres”), razón por la cual si una línea del programa (¡inclusive sin incluír comentarios!) no cabe en una línea de texto en la pantalla, el resto es colocado en una segunda línea en la pantalla. De este modo, lo anterior va siendo desplazado “hacia arriba” (corrimiento conocido en inglés como scrolling) hasta que se llega al final del texto en donde podemos tener el resto del programa ya ensamblado (el editor EDTASM proporciona opciones de operación para ir deteniendo el corrimiento dinámico ascendente del texto cada vez que se llena la pantalla con una “página” de texto):





Como puede apreciarse en lo que ilustran ambas pantallas, el código ensamblado (en lenguaje de máquina, pero mostrado por el editor-ensamblador EDTASM en hexadecimal) ha sido agregado a la izquierda de cada línea de texto. Obsérvese que en el resultado final del proceso de ensamblaje todos los comentarios son eliminados. A continuación se da una explicación más detallada sobre lo que vemos en la primera página:




Y en lo que toca al final del programa ensamblado, la explicación es:




El programa fuente escrito en lenguaje ensamblador que estamos usando para el microprocesador 6809 es el que fue dado arriba al principio:




Una vez que dicho programa ha sido ensamblado y convertido a lenguaje de máquina, el programa en lenguaje de máquina (en notación hexadecimal) entregado por el editor-ensamblador es el siguiente:


Aunque lo que el editor-ensamblador nos está proporcionando para conveniencia nuestra son instrucciones puestas en el sistema hexadecimal, en realidad estas son instrucciones que se ejecutan directamente en sistema binario en la computadora. Como puede verse, a cada línea escrita en el lenguaje ensamblador para el 6809 le corresponde una instrucción en lenguaje de máquina:




 El hecho de que a cada instrucción escrita en lenguaje ensamblador le corresponda directamente una instrucción en lenguaje de máquina abre la posibilidad de llevar a cabo el procedimiento inverso, esto es, tomar un programa ejecutable (en lenguaje de máquina) y aplicarle un procedimiento en reversa convirtiendo cada instrucción en lenguaje de máquina a una instrucción en lenguaje ensamblador:




Hay programas construídos específicamente para tal propósito, los cuales son conocidos como desensambladores. Naturalmente, al llevarse a cabo el desensamble de lo que está escrito en lenguaje de máquina, el desensamblador no usará las etiquetas START, SCREEN y DONE, ya que tal información se pierde irremisiblemente una vez que el código está en lenguaje de máquina (el desensamblador posiblemente tratará de poner etiquetas tales como TAG1, TAG2, o LABEL1, LABEL2, etc.) que sean necesarias para resaltar los saltos que esté dando un programa de un lugar a otro de la memoria.

Habido el hecho de que en las computadoras de hoy en día construídas con microprocesadores sofisticados hay instrucciones que solo requieren de un byte (como la instrucción de ALTO) mientras que hay otras instrucciones que al llevar a cabo un domiciliamiento referenciando alguna localidad de la memoria se tienen que formar con dos bytes o inclusive tres o más bytes, un problema obvio que tiene que ser solventado en un proceso de desensamble es que, si a partir de cierta localidad de la memoria RAM se tiene almacenada en la memoria en cada localidad sucesiva algo como lo siguiente (en el lenguaje de la máquina, en el lenguaje binario de unos y ceros):

   01001010
   10011001
   10101111
   00010110
   01001011
   00110100
   10101010
   00010001
   10101010
   00100111
   11011001
   01001101
   10001010
   01010111

entonces, ¿cómo sabemos el punto a partir del cual en lo anterior podemos llevar a cabo el proceso de desensamble? Considérese la segunda línea, 10011001. Podemos llevar a cabo el desensamble de esta línea siempre y cuando la línea anterior (la primera línea,01001010) sea una instrucción de un solo byte, porque si se trata de una instrucción de dos bytes, entonces todo el proceso de desensamble del resto del código en lenguaje de máquina puede terminar produciéndonos pura basura. Sin embargo, sobreponerse a este obstáculo no es tan difícil como parece. Si iniciamos el proceso de desensamble a partir de la segunda línea produciéndose una secuencia de cosas sin sentido, entonces volvemos a iniciar el proceso de desensamble a partir de la tercera línea. Si el desensamble tiene éxito, entonces podemos suponer que la segunda línea es una instrucción en lenguaje de máquina y no una extensión de la línea anterior (la primera línea). Pero si se produce de nueva cuenta otra retahila de cosas sin sentido, entonces podemos empezar el proceso de desensamble a partir de la tercera línea, y así sucesivamente. Si sabemos de antemano la máquina con la que estamos trabajando (esto es, si conocemos el microprocesador utilizado), entonces del conjunto de instrucciones de la unidad de procesamiento central podemos saber de inmediato cuál es la extensión máxima (en bytes) de cualquier instrucción, lo cual pone un tope límite al número de intentos de desensamble que tenemos que llevar a cabo. Esta misma tarea se la podemos dejar al desensamblador, construyéndolo de modo tal que lleve a cabo estas repeticiones en caso de encontrarse con material que no puede ser desensamblado.

El uso de desensambladores para descifrar programas ejecutables y ver la manera en la cual trabajan  no es algo muy del agrado de las casas productoras de software, ya que esto permite “ver” la manera en la cual trabaja el código ejecutable de algún programa, y permite incluso mejorarlo pudiéndose poner en venta una versión más sofisticada del mismo programa pero sin haber tenido que empezar de cero, ahorrando la cantidad usualmente considerable de dinero que se requiere para escribir desde el principio un programa comercial (de hecho, en algunos programas se estipula en los manuales del paquete que se prohibe al usuario intentar llevar a cabo el proceso de desensamble.

Además de los obvios propósitos de piratería cuando se lleva a cabo el desensamble de algún programa, otros que usan en forma intensiva los programas desensambladores son los “hackers”, los cuales con este tipo de herramienta pueden saber exactamente qué hace cada línea de código y pueden alterar las instrucciones en lenguaje de máquina para producir una versión modificada del programa que incorpora código malicioso.

En el sencillo programa introductorio que se ha dado, cada instrucción está almacenada a partir de una localidad de la memoria, a partir de un domicilio. Para un programador de sistemas, frecuentemente es de interés (sobre todo cuando un programa está siendo purgado de errores) el tener conocimiento no sólo del listado de instrucciones que serán llevadas a cabo, sino también del domicilio en el que está puesta cada instrucción, tomando en cuenta que en muchos diseños reales la instrucción completa puede estar formada por dos o más palabras binarias. En el microprocesador 6809, al igual que en muchos otros procesadores, la unidad básica del tamaño de cada palabra binaria es de 8 bits, o sea un byte. Una instrucción como ALTO (halt, stop, etc.) ciertamente no ocupará más de un byte, porque hablando en términos generales no hace referencia a ninguna localidad de la memoria. Pero hay otras instrucciones que son formadas usando dos o tres bytes. Y esto lo podemos apreciar tabulando las instrucciones junto con la localidad de la memoria a partir de la cual está especificada cada instrucción usada en un programa. En el código ensamblado,  el editor-ensamblador nos proporciona dicha información, con lo cual se tiene la siguiente tabla:




La primera instrucción está puesta en el domicilio 0000 de la memoria. La segunda instrucción está puesta en el domicilio 0002 de la memoria. ¿Y qué hay del domicilio 0001 de la memoria? Ciertamente, no está vacío, y la única explicación posible es que la primera instrucción completa no es de un solo byte, sino que requiere de dos bytes para formarse. Y en efecto, el número hexadecimal “86” (o mejor dicho, su equivalente binario que viene siendo 01010110) está almacenado en el domicilio (hexadecimal) 0000 (en realidad, el domicilio binario 00000000), mientras que el número hexadecimal “F9” (o mejor dicho, su equivalente binario) está almacenado en el domicilio 0001, y la instrucción completa (ensamblada, escrita en hexadecimal) es:

86 F9

En lo que toca a la segunda instrucción que empieza en el domicilio 0002 de la memoria, podemos ver que la siguiente instrucción, la tercera instrucción, empieza en el domicilio 0005 de la memoria, lo cual implica que la segunda instrucción requiere de tres bytes para formarse consumiendo por lo tanto tres localidades de la memoria, siendo dicha instrucción:

8E 50 00

En pocas palabras, el comando de la segunda instrucción consume un byte, mientras que el operando de la segunda instrucción consume dos bytes. De este modo, la tercera instrucción tiene que empezar en el domicilio 005 de la memoria.

Procediendo de la misma manera, podemos ver que la tercera instrucción consume dos localidades de la memoria, mientras que la cuarta instrucción que empieza en el domicilio 0007 de la memoria consume tres localidades de la memoria; lo cual a su vez implica que la quinta instrucción tiene que empezar a partir del domicilio 000A de la memoria (que en nuestro sistema decimal de conteo vendría siendo el domicilio 0010). La única instrucción que sólo consume un byte es la última, la que no hace referencia a operando alguno, por ser una instrucción de frenado en la ejecución del programa.

De este modo, el reparto del programa actual en la memoria es el siguiente:




En realidad, toda la información que aparece arriba debería estar puesta en el lenguaje que entiende la máquina, en “unos” y “ceros”, en notación binaria, porque es lo único que “entiende” la máquina, el lenguaje de “encendido” y “apagado”.

En el campo de símbolos de nuestro primer programa en lenguaje assembler para el microprocesador 6809, tenemos los siguientes tres símbolos a los cuales frecuentemente se les dá el nombre de etiquetas (labels):

    START
    SCREEN
    DONE

Cada una de estas etiquetas representa un domicilio de la memoria. El domicilio de cada etiqueta corresponde precisamente al número (hexadecimal) puesto en el extremo izquierdo de la línea ensamblada. Juntando esta información en una tabla, tenemos entonces la siguiente tabla:




Este tipo de tabla es precisamente de lo que se habla en la entrada “Ensambladores”. Obsérvese en la segunda “pantalla” del televisor analógico que el editor-ensamblador EDTASM proporciona la tabla de símbolos al final del programa ensamblado.

La ejecución del progama ya ensamblado convertido al lenguaje de la máquina basada en el microprocesador 6809 empieza tomando la primera instrucción almacenada en la localidad de la memoria 0000 que marca el incio del programa. Ayudados por los comentarios, podemos ver que hay unas inicializaciones con las instrucciones LDA y LDX, tras lo cual con la instrucción STA se imprime un caracter en la pantalla del televisor, el caracter gráfico F9 (F9 es un número hexadecimal, desde luego). No entraremos en detalles sobre la letra o el número representado bajo F9, porque ello no es trascendental para nuestro análisis. La siguiente instrucción CMPX checa si hemos llegado al final de la memoria asignada a los caracteres de video que están siendo enviado al televisor analógico. Si no hemos llegado aún al final de la memoria, saltamos con una instrucción BNE (Branch if Not Equal) a la localidad de la memoria 0005 para buscar allí la siguiente instrucción STA, que pone un carácter en la pantalla en la siguiente posición inmediata, checándose a continuación de nueva cuenta con CMPX si hemos llegado al final de la memoria asignada a los caracteres de video, repitiéndose el proceso hasta que eventualmente al obtenerse una igualdad la instrucción BNE no efectúa el salto y se pasa a la siguiente instrucción puesta en la localidad 000C de la memoria, la instrucción SWI (Software Interrupt) que marca la terminación del programa. En breve síntesis, el programa lo que hará será llenar con el mismo caracter (que puede ser una letra como “M” o algún símbolo como “&”) varias de las posiciones disponibles en la pantalla del televisor, desde la posición “$500” ó “500 hexadecimal” (el símbolo “$” puesto antes del número es usado por el editor-ensamblador EDTASM para indicar que se trata de un número hexadecimal) hasta la posición “$5FF” ó “5FF hexadecimal”, habiendo espacio en toda la pantalla para 16 renglones de 32 caracteres en cada renglón.

Podríamos, en principio, ir cargando el programa poco a poco en la memoria RAM de la computadora, byte por byte, bit por bit, empezando por cargar el primer byte (86 hexadecimal, o bien 10000110, una conversión directa e inmediata si hacemos el reagrupamiento 1000-0110) en el domicilio 0000 de la memoria, cargando a continuación el siguiente byte (F9 hexadecimal, o bien 11111001, también una conversión directa e inmediata si hacemos el reagrupamiento 1111-1001), y así sucesivamente; y ya cargado el programa completo podríamos ejecutar el programa indicándole a la computadora comenzar en el domicilio de la memoria señalado de inicio. Así lo hacían muchos aficionados a la electrónica cuando los primeros “kits” de precio módico basados en los primeros microprocesadores empezaron a hacer su aparición en el mercado. Pero para programas que para alguna aplicación incluso modesta que requieren de cientos o miles de instrucciones, este ritual pronto se convierte en una pena, y quienes no desean cargar con esta cruz tienen que habilitar sus sistemas con cargadores automáticos, teniendo que recurrir eventualmente al uso de los ensambladores para que hagan la conversión y que ellos mismos se hagan cargo de la ejecución del programa colocando cada palabra binaria en cada localidad de la memoria según se requiera.

Se agregará en este punto que en nuestro programa fuente introductorio las tres palabras simbólicas START, SCREEN y DONE no son palabras especiales reservadas por el editor EDTASM (de hecho las únicas palabras reservadas por la mayoría de los ensambladores son las que corresponden a las mnemónicas como BNE, JMP, MOV, etc. que forman parte del conjunto de instrucciones del procesador). Igual podríamos haber usado en su lugar las palabras simbólicas INICIO, PANTA (por PANTALLA) y HECHO, y el programa habría ensamblado igual. Puesto que cada palabra simbólica o etiqueta representa una localidad de la memoria, no podemos darle la misma etiqueta a dos domicilios diferentes, tenemos que usar una etiqueta distinta para cada domicilio.

PROBLEMA: Al serle entregado el siguiente programa al editor-ensamblador EDTASM para su ensamblaje:




una vez que se ha llevado a cabo el proceso de ensamblaje se obtiene lo siguiente:




1) Escríbase la tabla de símbolos creada para su uso interno por el ensamblador.

2) Para el programa ensamblado elabórese en forma detallada, empleando notación hexadecimal, la repartición de las instrucciones depositadas en cada una de las localidades de la memoria, poniendo en un renglón distinto cada instrucción completa.

Por inspección directa, tomando en cuenta que a cada línea del programa “fuente” le corresponde en forma unívoca una instrucción en lenguaje de máquina que empieza a partir de cierta localidad de la memoria, siendo ésta información que aparece dada en el extremo izquierdo del programa ya ensamblado (resaltada aquí en color azul), podemos ver que la tabla de símbolos debe ser la siguiente:




Como ya se había señalado arriba, se hace hincapié nuevamente que esta misma información es dada (sin necesidad de tener que pedírsela) por el EDTASM al final en la parte inferior del programa ya ensamblado:




En efecto, el EDTASM nos proporciona la tabla de símbolos. Esta es una característica deseable (inclusive indispensable) en cualquier ensamblador, razón por la cual podemos encontrar algo equivalente en la mayoría de los ensambladores en uso actual.

En el programa hay instrucciones de dos bytes, tres bytes, y cuatro bytes. Considerando que el domicilio dado en el extremo izquierdo del programa ensamblado para cada instrucción es la localidad en la cual va puesto el primer byte de la instrucción, entonces (usando el sistema de conteo hexadecimal) podemos ver que en cada localidad de la memoria desde la que está puesta bajo el domicilio 0A6A hasta la que está puesta en el domicilio 0A96 los contenidos (de byte en byte) son los siguientes (las primeras dos instrucciones requieren dos bytes cada una, la tercera instrucción requiere de cuatro bytes, la cuarta instrucción requiere de dos bytes, y así sucesivamente):




 Examinando en mayor detalle la línea identificada con el número de línea 00210 en la cual se ejecuta una instrucción de comparación CMCPX, encontramos algo que puede llamarnos la atención:

#$400 + 511

El primer símbolo (numeral) “#” es usado para pedirle al EDTASM que trate todo lo que está puesto a su derecha no como una instrucción o una etiqueta sino como un dato, y el símbolo “$” es usado para pedirle al ensamblador que trate el número que aparece antes del operador de suma “+” como un número hexadecimal. De este modo, se tiene la suma del número hexadecimal $400 al número decimal. ¿Pero exactamente qué significa ésto? Con la ayuda de una calculadora para poder llevar a cabo conversiones entre distintas bases numéricas (véase el Apéndice “conversiones entre bases numéricas distintas”) podemos convertir el número hexadecimal $400 al número decimal 1024, el cual sumado al número 511 nos produce el número decimal 1535, el cual convertido a hexadecimal resulta ser $5FF, lo cual podemos resumir del modo siguiente:
$400 + 511 = 1024 + 511
           = 1535
           = $5FF
Si en vez de haber puesto “#$400+511” en el programa fuente hubiéramos puesto “$5FF”, el programa habría sido ensamblado de idéntica manera. ¿Qué objeto podría tener la primera opción, la cual dicho sea de paso nos demuestra una habilidad que debe tener un buen ensamblador, la capacidad para escribir sumas (o diferencias) de números en distintas bases dejando que el ensamblador efectúe la tarea de hacer las operaciones aritméticas y las conversiones necesarias al llevarse a cabo el ensamblaje? La primera opción es atractiva porque está de acuerdo con la filosofía de dejarle a la máquina la tarea de efectuar todas las cosas que nosotros podríamos efectuar manualmente, y quizá algunos programadores no quieran perder su tiempo en estar haciendo estas conversiones para sumar números en bases distintas si la máquina puede hacer tal cosa por nosotros. Pero hay otra razón igualmente importante: darle mayor claridad al programa escribiendo explícitamente algo que está siendo fijado por alguna razón. En este caso, el número hexadecimal $400 es un límite inferior, mientras que el número hexadecimal $5FF es un límite superior. Esto no lo podemos “ver” de modo tan claro si escribimos simplemente 5FF en lugar de $400+511. El límite inferior nos fija el inicio de un bucle repetitivo del programa en donde se llevarán a cabo una serie de instrucciones en lenguaje de máquina, empezando con el valor $400 seguido de otro bucle usando el valor $401 seguido de otro bucle usando el valor $4002 y así sucesivamente, mientras que el límite superior nos fija la condición con la cual debemos salir del bucle. ¿Pero de qué límites estamos hablando aquí? Para ello, tenemos que echar mano de algo que tarde o temprano tiene que estar consultando cualquier programador de sistemas en cualquier computadora digital en la que esté trabajando, el mapa de la memoria de la máquina, al cual nos referiremos simplemente como el mapa de la memoria. Para la computadora CoCo de Radio Shack, el mapa de la memoria es el siguiente (información tomada, con algunas traducciones pertinentes, del manual EDTASM Plus: Color Computer Editor Assembler with ZBug, número de catálogo Radio Shack 26-3250):





Esta es la forma en la cual se reparte para varios fines el espacio disponible dentro de la memoria de la computadora. La parte en la que estamos interesados es la que parte que va desde el domicilio $0400 hasta el domicilio $05FF, y podemos ver que es precisamente la parte de la memoria adjudicada para la “impresión” de caracteres en la pantalla del televisor analógico. El espacio de texto está repartido en 16 líneas de texto con una capacidad de 32 caracteres en cada línea, dando una capacidad total de 512 “posiciones de caracter”. Hay un byte disponible en la memoria para cada posición de caracter, los cuales permiten un “mapeo” de uno-a-uno (unívoco) en la pantalla. El byte para definir el caracter que va en la primera posición en la primera línea de texto (puesto en la esquina superior izquierda) es el que está almacenado en la localidad $0400, el siguiente es el que está almacenado en la localidad $0401, y así sucesivamente, hasta llegar al último byte disponible para definir el caracter que va en la última posición en la última línea de texto (puesto en la esquina inferior derecha):




Puesto que 1 byte ofrece 128 diferentes combinaciones posibles de “unos” y “ceros”, podemos representar un alfabeto de 128 caracteres (letras, dígitos numéricos, símbolos especiales como “&” y “%”), pudiendo contener todos los caracteres propios del código ASCII (esta es la razón principal por la cual el byte formado por 8 bits es la opción favorita para codificar texto en vez de una palabra binaria formada por cuatro o cinco bits).

El programa ensamblado manipula pues lo que hay en la pantalla del televisor analógico. ¿Pero qué tipo de manipulación es la que se lleva a cabo? Obsérvese que al principio en la primera línea del programa se le dá como comentario al programa (esto se logra con el asterisco puesto al principio para indicarle al ensamblador que toda la línea debe ser tomado como un comentario sin haber campos de ninguna especie) el nombre de bubble sort. Resulta que esta es una de varias técnicas posibles para el reordenamiento de datos, la cual pone en orden lexicográfico una serie de caracteres. Si los caracteres de un texto constan exclusivamente de letras y espacios en blanco, entonces a lo largo de varios “pases” el programa en su quintaesencia va reacomodando todos los caracteres individuales en orden alfabético hasta que ya no hay más reacomodo posible. Usando grupos de números a modo de ejemplo, tenemos un reacomodo tipo “burbuja” en acción :




Podemos ver el por qué se le dá a este tipo de reordenamiento el nombre de “burbuja”: los caracteres de más “livianos” (de menor “peso”, por así decirlo), van siendo “flotados” hacia arriba hasta que el más “ligero” se encuentra “en el tope” y el más “pesado” se encuentra “en el fondo”.

No podemos entrar en mayor detalle sin tomar en un conocimiento más a fondo sobre algunos detalles específicos de la arquitectura de la máquina, o sea los recursos internos con los cuales cuenta el microprocesador 6809. Empezaremos por asentar que éste microprocesador cuenta con nueve registros de almacenaje temporal que podemos usar en la elaboración de un programa fuente en assembler:




Los registros A y B son usados para la manipulación de datos y para llevar a cabo cálculos aritméticos. Cada uno puede almacenar un dato de un byte. Podemos dirigirnos a ambos como un solo registro de dos bytes, el registro D.

El registro DP es utilizado para algo que se conoce como domiciliamiento directo. Almacena el byte más significativo de un domicilio, lo cual le permite al procesador accesar directamente un domicilio usando únicamente el byte menos significativo en vez de dos bytes.

Los registros X y Y pueden almacenar cada uno un dato de dos bytes, y son usados principalmente con domiciliamiento indexado.

El registro PC de 16 bytes almacena el domicilio de la siguiente instrucción a ser ejecutada.

Los registros U y S pueden almacenar cada uno un domicilio de dos bytes que apunta hacia una “pila” entera de memoria. Este domicilio es igual a 1 sumado al tope de la pila. Por ejemplo, si el registro U contiene 0155, la pila empieza en el domicilio 0154 y continúa hacia abajo.

Por último, el registro CC es el registro de Códigos de Condición, y es usado para probar condiciones y fijar interrupciones. Está dividido en ocho “flip-flops” (generalmente, flip-flops D) independientes el uno del otro, conocidos en la literatura técnica como banderas. Estos códigos de condición mantienen un registro de los resultados de las comparaciones entre varias operaciones aritméticas y operandos. Varias de las instrucciones del 6809 pondrán en “1” (set) o pondrán en “0” (clear) una o más de estas banderas. Otras operaciones checarán las condiciones de algunas de estas banderas para determinar si están en “set” o en “clear”. Los códigos de condición pueden alterar el flujo de un programa en virtud de que pueden ser checados por instrucciones de “ramificación” (branch) que no es más que otro nombre para lo que es un “salto” llevado a cabo en la ejecución de un programa.

Esta es la relación y el significado de cada bandera:




Cada una de las banderas son usadas para fijar la condición que producirá un salto en el programa hacia otra instrucción diferente a la instrucción que sigue a la que está siendo ejecutada, lo cual ocurrirá generalmente después de una operación aritmética o una operación de comparación. En el lenguaje ensamblador para el μP 6809 hay 17 saltos condicionales llamados branch (palabra que etimológicamente significa “ramificación”, aunque el significado técnico que se le dá es el de un salto) y un salto incondicional BRA. La lista completa de estos saltos junto con los códigos de condición que determinan la condición específica bajo la cual se llevará a cabo cada salto son los siguientes:




El salto BRN (Branch Never) hace lo mismo que una instrucción “NOP” (No Operación) excepto ocupar espacio. La instrucción BRN ocupa 2 bytes y su alter ego LBRN ocupa tres bytes. La instrucción NOP usa un “código de operación” diferente y ocupa 1 byte. Estas instrucciones dejan  intactos los códigos de condición y no modifican ninguno de los registros del microprocesador ni los contenidos de la memoria. ¿Para qué pueden servir estas instrucciones? Un propósito es para “quemar tiempo” en algún bucle repetitivo. Otro propósito es para reservar espacio en anticipación de “remiendos” (patches) que se le tengan que ir agregando al programa evitando con ello el tener que escribir todo un código fuente de nuevo.

Estas son las instrucciones que deben ser utilizadas para comparar dos números enteros absolutos (sin signo positivo o negativo):




Los saltos BR tienen ciertas limitaciones. Puesto que sólo hay un byte disponible en cada instrucción BR para especificar un domicilio, el alcance del “salto” es limitado. En la aritmética de “dos complemento”, un número puede tener cualquier valor entre menos 128 y más 127. Puesto que el segundo byte es un número de “salto” en forma de “dos complemento”, esto implica que sólo podemos saltar 128 localidades hacia atrás y 127 localidades hacia adelante. No hay que olvidar que esos números enteros están referenciados al Contador del Programa, que apunta hacia la instrucción que sigue después de una instrucción BR. Referenciados entonces al BR, sólo podemos saltar 126 bytes hacia atrás y 129 bytes hacia adelante. Si tratamos de usar una instrucción BR para dar un salto “fuera de rango”, el editor-ensamblador EDTASM detectará el error cometido por nosotros y nos dará el mensaje de error “BYTE OVERFLOW”. ¿Hay algo que se pueda hacer al respecto?

Resulta que cada uno de los saltos tiene una forma alterna, el “salto largo” (Long Branch). Este salto aún es “relativo” a los contenidos del Contador del Programa, pero ahora hay 4 bytes en lugar de dos en la instrucción. La forma de especificar un “salto largo” es prefijando la instrucción de salto con una letra “L” (Long), de modo tal que si queremos especificar un salto largo BCS la instrucción dada al ensamblador será LBCS. No es necesario usar un salto largo si estamos inseguros acerca del rango de valores que serán usados, ya que el EDTASM detectará el error permitiéndonos hacer una corrección rápida en el programa fuente para reensamblarlo de nuevo con un salto largo en vez de un salto ordinario. De este modo, podemos usar ya sea un salto ordinario o un salto largo en cualquier parte del programa para saltar a cualquier parte de la memoria, basándonos en el efecto producido por una condición de comparación, suma, resta o cualquier otra instrucción que preceda al salto que será llevado a cabo en base a la instrucción precedente.

Para el microprocesador 6809, hay seis maneras diferentes de poder pasarle una localidad de la memoria a una instrucción:

   1) Inherente

   2) Inmediato

   3) Extendido

   4) Indexado

   5) Relativo

   6) Directo

Como el lector tal vez lo habrá intuído, usamos el símbolo de numeral “#” puesto inmediatamente antes de un número para indicarle al editor-ensamblador EDTASM que el número será interpretado como un dato (por ejemplo, un año o una temperatura). De este modo, la instrucción:

LDA  #$3000

en donde LDA es el operador y #$3000 es el operando indica que con una instrucción de carga el número hexadecimal $3000 deberá ser puesto dentro del acumulador A. En cambio, si omitimos el símbolo de numeral “#”, le estaremos indicando al EDTASM que el número será interpretado como un domicilio de la memoria. De este modo, tomando en cuenta que el registro D en realidad es un registro de dos bytes formado por los registros A y B, la instrucción:

LDD  $3000

pide que el registro D sea cargado con los bytes contenidos dentro de las localidades de la memoria $3000 Y $3001.

En el domiciliamiento inherente, no se usa operando alguno, ya que la instrucción no lo requiere. Por ejemplo, la instrucción:

SWI

es una interrupción programada en el software que no requiere de operando alguno. Y la instrucción:

CLRA

“limpia” los contenidos del acumulador A poniendo todos sus ocho bits en cero. El registro A es parte inherente de la instrucción.

En el domiciliamiento inmediato, el operando es un dato. Es necesario usar el símbolo “#” para especificar este modo. Por ejemplo, la instrucción:

ADDA  #$30

suma el valor hexadecimal 30 a lo que ya se encontraba almacenado en el acumulador A. Y la instrucción:

CMPX   #$3A54

compara los contenidos del registro X con el valor hexadeximal $3A54.

En el domiciliamiento extendido, el operando es un domicilio. Este es el modo “por omisión” (default) de todos los operandos. Por ejemplo, la instrucción:

JSR   $1FB7

provoca un salto incondicional al domicilio de la memoria dentro del cual será procurada la siguiente instrucción. Y el par de instrucciones:
SPOT   EQU   $12C9
             STA   SPOT
almacenan en acción combinada los contenidos del acumulador A en el domicilio $12C9 de la memoria.

Si la instrucción pide un dato, el operando contiene el domicilio de la memoria en donde el dato está almacenado. De este modo, la instrucción:

LDA   $2E5B

no carga el registro A con el número hexadecimal $2E5B. El microprocesador cargará al acumulador A con cualquier dato que esté contenido dentro del domicilio $2E5B. Si el número $06 (de un byte) está contenido en la localidad de la memoria $2E5B, el acumulador A será cargado con el número $06. Del mismo modo, la instrucción:

ADDA   $0F49

sumará el dato contenido dentro del domicilio $0F49 al número que esté contenido dentro del domicilio $0F49.

En el domiciliamiento extendido, el operando es el domicilio de un domicilio. Esta es una variante del modo de domiciliamiento extendido. Para especificarlo, se usan los símbolos “[” y “]”. Para entender este modo de domiciliamiento, piénsese en un juego de la búsqueda de un tesoro. La primera instrucción puede ser “Buscar en el reloj”. El reloj contiene dentro una segunda instrucción que dice “Buscar en el refrigerador”. Por ejemplo, la instrucción:

JSR   [$1B64]

produce un salto al domicilio que está contenido dentro de los domicilio $1B64 y $1B65 (recuérdese que se requieren dos bytes para especificar un domicilio de la memoria). Si el domicilio $1B64 contiene $06 y el domicilio $1B65 contiene 11, el domicilio efectivo será $0611. La ejecución del programa brincará al domicilio $0611. Y el par de instrucciones:

   EQU   $1234
   STA   [SPOT]

hará que se almacenen los contenidos del registro A en los domicilios $1234 y $1235. Por otro lado, la instrucción:

LDD   [$1234]

ocasionará que el registro D sea cargado con el dato (de dos bytes de extensión) almacenado en los domicilios $1234 y $1235.

Este modo de domiciliamiento es ideal para invocar rutinas contenidas en la memoria ROM de la computadora. En la computadora CoCo, el domicilio de entrada de la rutina POLCAT está contenida en el domicilio $A000. Podemos invocarla por lo tanto con estas instrucciones:

    EQU   $A000
    JSR   [POLCAT]

En el domiciliamiento indexado, el operando es un registro de índice que apunta hacia un domicilio. Podemos usar como registro de índice cualesquiera de los registros que tengan una capacidad de dos bytes (tenemos dos registros para tales propósitos, los registros X y Y), incluyendo al mismo Contador de Programa. Para especificarlo, se usa el símbolo “,” de la coma. Para un ejemplo, cargaremos primero el registro X, un registro de dos bytes, con la instrucción:

LDX   #$1234

Hecho esto, podemos empezar a accesar el domicilio $1234 de la memoria mediante el domiciliamiento indexado. Esta instrucción:

STA   ,X

almacena los contenidos de A en el domicilio $1234, mientras que la instrucción:

STA   3,X

almacena los contenidos de A en el domicilio $1237, que es igual a $1234 más $3. La constante 3 es aquí una constante offset (se pronuncia “afset”). Hecho lo anterior, el par de instrucciones:

    SYMBOL   EQU   $4
             STA   SYMBOL,X

almacena los contenidos del registro A en el domicilio $1238, que es igual a $1234+SYMBOL (aquí SYMBOL es una constante offset). Por su parte, el par de instrucciones:

    LDB   #$5
    STA   B,X

almacena los contenidos de A en el domicilio $1239 que es igual a $1234 más los contenidos de B (estamos usando aquí el registro B como un registro de offset; podemos usar cualquiera de los dos acumuladores A y B como registros de offset). Por su parte, hecho lo anterior, la instrucción:

STA   ,X+

efectúa dos tareas distintas: (1) almacena los contenidos de A en el domicilio $1234 (los contenidos del registro X), y (2) incrementa en 1 los contenidos de X de modo tal que ahora contendrá $1235. Hecho lo anterior, la instrucción:

STA   ,X++

hace también dos tareas distintas: (1) almacena los contenidos de A en el domicilio $1235 (los contenidos actuales del registro X), y (2) incrementa los contenidos de X en 2 para hacerlos iguales a $1237. Hecho lo anterior, la instrucción:

STA   ,--X

decrementa los contenidos de A en 2 para hacerlos iguales a $1235 (1237 menos 2), y (2) almacena los contenidos de A en el domicilio $1235.

Esto viene actuando en realidad un domiciliamiento relativo, y parece ser lo mismo que el domiciliamiento extendido. Sin embargo, usando domiciliamiento relativo en el Contador del Programa, el código resultante en lenguaje de máquina se vuelve completamente relocalizable. Y esta es otra de las grandes ideas en la ciencia de las computadoras. Los programas que recurren a domicilios absolutos de la memoria especificando de modo explícito tales domicilios ofrecen la dificultad de que si es necesario ubicar el principio del programa en un lugar diferente de la memoria, entonces todos los domicilios absolutos tienen que ser actualizados de uno en uno por el programador y el programa tiene que ser ensamblado de nuevo. En cambio, si se usan domicilios relativos, no es necesario hacer cambios en los domiciliamientos de la memoria. Como debe suponerse, en los sistemas operativos modernos en los cuales se van cargando en la memoria varios programas sin seguir un orden específico (se puede abrir primero un procesador de palabras, y tras esto abrir un procesador de imágenes, y tras esto una calculadora), como no es posible saber de antemano el domicilio exacto de la memoria a partir del cual se tiene que ir cargando cada programa ejecutable, el uso de domiciliamiento indexado es casi una necesidad.

En el domiciliamiento relativo del procesador 6809, el procesador interpreta al operando como un domicilio relativo. No se utiliza ningún símbolo (como la coma “,” o el par de símbolos “[” y “]”) en este modo, y el procesador lo utiliza automáticamente para todas las instrucciones de salto. Por ejemplo, si esta instrucción está almacenada en la localidad $0580:

BRA   $0585

el procesador convertirá $0600 a un salto relativo de:

$0600 - $0580 = +$5

Puesto que el procesador utiliza este modo de domiciliamiento en todas las instrucciones de salto, es completamente invisible y transparente para el programador a menos de que se obtenga un error de BYTE OVERFLOW (lo cual significa que el salto está fuera del rango de menos 128 a más 127, requiriéndose una instrucción de salto largo). Puesto que el procesador utiliza este modo en forma nativa, las instrucciones de salto son completamente relocalizables en la memoria sin necesidad de cambiar las instrucciones de salto.

Por último, en el domiciliamiento directo, el operando es la mitad de un domicilio. La otra mitad se encuentra en los contenidos del registro de Página Directa DP:




El ensamblador EDTASM y el microprocesador 6809 usan automáticamente este modo siempre que llegan a un operando cuyo primer byte es lo que suponen que es una “página directa” (los contenidos del registro DP). A menos de que el programador provoque un cambio en la página directa, ambos suponen que su valor será $00. Esto es lo que hace que las siguientes dos instrucciones se ejecuten de la misma manera:

JSR   $0015

JSR   $15

Ambas instrucciones producen un salto hacia el domicilio $0015. En ambos casos, el ensamblador utiliza únicamente $15 como el operando, y no $00. Cuando el procesador ejecuta las instrucciones, tomará la porción $00 (el byte más significativo) del registro DP, y lo combinará con $15 formando $0015 (al ser encendida la máquina, el registro DP contiene puros ceros, al igual que todos los demás registros; esto es parte de lo que se lleva a cabo durante las rutinas de inicialización al encenderse la máquina).

En virtud del domiciliamiento directo, todos los operandos que comienzan con $00, la página directa, consumirán menos memoria y se ejecutarán con mayor rapidez. Si la mayoría de los operandos empiezan con, digamos, $12, el programador querrá hacer su página directa igual a $12. Para esto, hay que decirle al editor-ensamblador EDTASM lo que queremos que se haga usando una pseudo operación SETDP como la siguiente en el programa:

SETDP $12

Esto le dice al ensamblador que elimine el 12 de todos los operandos que comiencen con 12. De este modo, el ensamblador ensamblará un operando como “$1234” simplemente como “$34”, lo cual consume menos memoria (un byte en vez de dos) y por lo tanto correrá con mayor rapidez. Si se están efectuando miles y miles de estas instrucciones por segundo, el aumento en la velocidad de ejecución puede ser considerable.

Las pseudo operaciones como SETDP no forman parte del conjunto de instrucciones en lenguaje de máquina del procesador. (esto lo puede comprobar el lector echando un vistazo al conjunto completo de instrucciones para el microprocesador 6809 que están dadas en la sección de apéndices al final de esta obra). Son directivas que se le dan al ensamblador para que se lleven a cabo ciertas tareas implementadas no con el hardware electrónico del procesador sino mediante programación adicional.

En el ejemplo que se acaba de dar, no basta con decirle al ensamblador que la página directa va a ser prefijada con $12. El número $12 tiene que ser cargado dentro del registro DP con un par de instrucciones como el siguiente;

    LDB   #$12
    TFR   B,DP

Aquí se carga el registro B con el dato $12 y después se hace una transferencia del registro B al registro DP con una instrucción de transferencia entre registros TFR. La complejidad adicional se debe a que no es posible cargar directamente el registro DP ya que la instrucción de carga LD solo puede ser usada con los acumuladores A y B.

En este punto, posiblemente el lector quiera echarle un vistazo a la lista completa de instrucciones para el microprocesador 6809 es proporcionada en los Apéndices en la entrada titulada “Conjunto de Instrucciones del μP 6809”

Lo que hemos visto posibilita darle una interpretación a los programas en assembler que han sido dados arriba como ejemplo de programación assembler. Obsérvese que todos ellos terminan con una última línea titulada END. Todos los programas elaborados en lenguaje assembler para el microprocesador 6809 tienen que ser terminados con una línea que diga END marcando el fin del programa. Si esta línea no aparece, el ensamblador EDTASM lo detectará como un error y así se lo hará saber al programador. Esta característica es algo común a todos los lenguajes ensambladores, aunque es probable que se utilice otra palabra que no sea END dependiendo de la máquina de la que se trate.

Con lo que hemos visto arriba, estamos en mejores condiciones de poder darle una interpretación a lo que fue nuestro primer programa en lenguaje ensamblador escrito para la computadora CoCo basada en el microprocesador 6809. Pero ante, tenemos que aclarar una cosa. Dicha computadora, al igual que muchas otras basadas en una multitud de diversas arquitecturas y lenguajes de programación, puede imprimir ya sea caracteres alfanuméricos tales como “M”, “5” y “&” (la CoCo no imprimía en letras minúsculas, habido el hecho de que su lenguaje nativo COLOR BASIC solo permitía el uso de mayúsculas) y caracteres gráficos. Esto es algo que comparten muchas computadoras y muchos programas de aplicación de hoy en día. En el programa:




lo que se imprime es un caracter identificado con el símbolo hexadecimal $F9. Y resulta que el código interno usado por la electrónica de la máquina distingue entre caracteres de texto y caracteres gráficos dependiendo del valor que tenga el bit más a la izquierda del byte usado para imprimir caracteres en la pantalla, de acuerdo con la siguiente convención:




Si el primer bit (leyendo de izquierda a derecha) es “0”, entonces se imprimirá un caracter de texto (alfanumérico). Pero si el bit es “1”, entonces se imprimirá un caracter gráfico. Como ya se indicó arriba, una computadora CoCo conectada por la antena a un televisor analógico sólo tiene capacidad para imprimir en la pantalla 16 líneas de 32 caracteres por línea, habiendo una disponibilidad de 512 espacios. Como puede verse, en modo gráfico el elemento de imagen consta de cuatro elementos de imagen o pixeles (del inglés, picture element). Si en vez de usar un byte usaramos una palabra formada por dos bytes, con una extensión de 16 bits, habría una disponibilidad mucho mayor de colores en cada elemento de imagen así como de elementos de imagen. De hecho, los avances en la tecnología que permitieron usar espacios más amplios poniendo una mayor cantidad de elementos gráficos en una pantalla con una variedad mucho mayor de colores fue lo que posibilitó la aparición de videojuegos realistas al inicio del tercer milenio.

En el caso del elemento gráfico $F9, de acuerdo a la convención anterior se tiene algo como lo siguiente:




Entonces, interprentando paso a paso el programa, podemos ver que en la primera línea 100 se carga en el acumulador A el byte 11111001 (que en hexadecimal es $F9) con una instrucción LDA, tras lo cual con la segunda instrucción LDX puesta en la línea 110 se carga el registro X con el hexadecimal $500 que de acuerdo al mapa de la memoria representa una posición en la pantalla del televisor. Con la tercera instrucción STA puesta en la línea120 se “guarda” el caracter en la localidad de la memoria $500, lo cual tiene como efecto consecuente que se imprima automáticamente el caracter gráfico en la pantalla, y se incrementa el valor del registro X en 1 para tomar el valor $501. Se efectúa una comparación con la instrucción CMPX puesta en la línea 130, y puesto que $501 está por debajo de $5FF entonces con una instrucción de salto BNE puesta en la línea 140 se repite el bucle para imprimir el caracter gráfico $F9 en la siguiente posición en la pantalla. El proceso repetitivo continúa hasta que se ha llegado al final del espacio disponible en la memoria asignada en la pantalla que corresponde a la esquina inferior derecha ($5FF), tras lo cual la ejecución del programa es detenida con una interrupción programada con la instrucción SWI que está puesta en la línea 150.

El efecto final es que tendremos en la pantalla una hilera de caracteres gráficos $F9 que le darán un aspecto como el que se muestra a continuación:




Aunque hoy en día no se usan televisores analógicos para obtener una respuesta visual en la pantalla, en los monitores de las computadoras y en los televisores digitales se usan los mismos principios basados en lo que se ha cubierto en este simple ejercicio elaborado en lenguaje ensamblador. Obviamente, si lo que se quiere poner en la pantalla es la imagen de un soldado imaginario en un campo de batalla sorteando minas y achechanzas enemigas, y si se quiere que la imagen sea realista, se requerirá una cantidad mucho mayor de elementos de imagen y por lo tanto de memoria disponible para tales efectos, y la elaboración de los programas será mucho más laboriosa si lo hacemos a mano sin usar a la computadora como ayudante en la construcción de gráficos de alta definición animados.