Instrucciones de manejo de la pila

La pila ya se describió en una de las entradas dedicadas a los registros del microprocesador (porque hay un registro llamado el “puntero de la pila”). No obstante, a modo de resumen, podemos decir que la pila es una estructura de datos de tipo LIFO (last in – first out) que ocupa la página uno de la memoria, desde la $01ff hasta la $0100 (porque se va llenando de arriba abajo). Permite guardar datos y recuperarlos en el orden contrario, es decir, primero se recuperan los últimos datos que se han guardado. Y se utiliza básicamente para tres cosas:

  • Para llevar la pista de las llamadas a subrutinas y las direcciones de retorno para continuar la ejecución una vez han terminado.
  • Para salvaguardar y recuperar los registros del microprocesador cuando ocurre una interrupción. Las interrupciones se verán más adelante.
  • Como estructura de datos para que el programador guarde y recupere datos.

Los dos primeros usos son automáticos, el programador no necesita hacer nada para que ocurran. Ya se encargan el microprocesador, en el caso de las llamadas a rutinas, y el sistema operativo, en el caso de las interrupciones.

Para el último uso, es decir, para utilizar la pila como almacén de datos, el programador tiene que utilizar las siguientes instrucciones:

  • “pha”.
  • “pla”.
  • “php”.
  • “plp”.

La primera instrucción guarda el valor del acumulador en la pila, y la segunda recupera el último valor de la pila y lo mete en el acumulador.

Estas instrucciones, usadas conjuntamente con las ya presentadas “tax”, “txa”, “tay” y “tya”, permiten meter y sacar de la pila el acumulador, el registro X y el registro Y, en los dos últimos casos pasando antes por el acumulador.

Análogamente, la tercera instrucción guarda el registro de estado en la pila, y la última recupera el último valor de la pila y lo mete en el registro de estado.

Todo lo anterior puede ser útil, por ejemplo, para evitar que una rutina modifique estos registros. Esto se conseguiría guardando todos los registros (acumulador, X, Y y estado) en la pila al comienzo de la rutina, y recuperándolos al final. Precisamente esto es lo que ocurre cuando tiene lugar una interrupción, como se verá más adelante.

Todas estas instrucciones utilizan el modo de direccionamiento implícito, y no modifican los flags del registro de estado, salvo “plp” que, al cargar el último valor de pila en el registro de estado, lógicamente sí modifica sus flags.

Los detalles de estas instrucciones se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#PHA.

Adicionalmente, también guardan relación con la pila las instrucciones:

  • “tsx”.
  • “txs”.

La primera de ellas carga el puntero de la pila en el registro X. Es decir, carga la dirección de la primera posición libre de la pila, en el registro X. Y más concretamente su byte menos significativo (LSB), ya que el más significativo (MSB) siempre es $01 en el caso de la pila (página uno).

La segunda hace lo contrario: carga el valor del registro X en el puntero de la pila.

Estas dos instrucciones también utilizan el modo de direccionamiento implícito y tampoco modifican los flags del registro de estado. Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#TXS.


Programas de ejemplo: Prog23 y Prog24

Subrutinas vs macros

Para terminar de entender mejor las similitudes y diferencias entre subrutinas y macros, lo mejor es hacer una comparación de diferentes aspectos:

Subrutina Macro
Objetivo Reutilizar código y crear niveles de abstracción. Reutilizar código y crear niveles de abstracción.
Código ensamblador Empieza con una etiqueta (opcional, pero cómodo) y termina con “rts”. Empieza con “defm” y termina con “endm”.
Llamada Se hace con “jsr etiqueta” o “jsr dirección”. Se hace con el nombre de la macro y los parámetros separados por comas.
Parámetros de entrada Se pasan mediante los registros o mediante posiciones de memoria. Se especifican en cada llamada, tras el nombre de la macro. Se usan con /1, /2, …
Parámetros de salida Se pasan mediante los registros o mediante posiciones de memoria. El mecanismo de macros como tal no tiene parámetros de salida.
Código máquina Empieza en una posición de memoria y termina con “rts”. El código sólo aparece una vez en memoria. En cada “llamada” a la macro, se copia el código de la macro y los parámetros de entrada se sustituyen por sus valores.

Supongamos que tenemos un programa que tiene que imprimir por pantalla varios caracteres. Si la función para imprimir un carácter X la hacemos con una subrutina, el código ensamblador quedaría así:

Subrutina

Y el código máquina quedaría así:

Subrutina_CM

En cambio, si la función la hacemos con una macro, el código ensamblador quedaría así:

Macro

Y el código máquina quedaría así:

Macro_CM

Nótese que cada llamada a la macro es sustituía por una copia del código de la macro (defm … endm), y que en cada copia los parámetros son sustituidos por el valor con que se hizo la “llamada” (2 en este caso). De la macro como entidad independiente no queda ni rastro; no es más que una plantilla de código que se reutiliza.

Desde una subrutina es posible llamar a otra subrutina (así creamos diferentes niveles de abstracción) y también usar macros. Desde una macro es posible llamar a una subrutina, pero, en cambio, no es posible usar una segunda macro; el ensamblador se haría lío con tanta sustitución de código. Al menos con CBM prg Studio…


Programa de ejemplo: Prog22

Macros

Las macros valen para lo mismo que las subrutinas: factorizar código y crear diferentes niveles de abstracción. Ahora bien, así como las subrutinas son una característica del código máquina del 6510 (y de muchos otros lenguajes de programación), las macros son una construcción de los ensambladores modernos.

Una macro es un trozo de código en ensamblador que tiene que tener un nombre e ir definido entre las directivas “defm” y “endm” (esto es para el caso de CBM prg Studio; en el caso de otros ensambladores la directiva será diferente). Una macro puede tener parámetros. Posteriormente, la macro será “llamada” mediante su nombre y parámetros desde diferentes puntos del código ensamblador.

Ahora bien, la “llamada” tiene poco que ver con la llamada a una subrutina. En el caso de una subrutina, al ejecutar “jsr” el contador de programa pasa a tomar el valor de la primera dirección de la subrutina, y al ejecutar “rts” el contador de programa pasa a tomar el valor de la instrucción siguiente al “jsr”, que previamente se guardó en la pila.

Pues bien, en el caso de una macro, lo que realmente ocurre es que cada “llamada” a la macro se sustituye por una copia del código de la macro, es decir, por una copia del código que hay entre “defm” y “endm”. Y el “paso de parámetros” se consigue gracias a que, en cada copia del código de la macro, los parámetros se sustituyen por el valor que se ha suministrado en la llamada.

En definitiva, subrutinas y macros valen para lo mismo, pero las primeras son un mecanismo del código máquina del 6510, y las segundas son como un “truco” del ensamblador (por ejemplo, CBM prg Studio). Lo que hace el ensamblador es sustituir cada “llamada” a la macro por una copia de su código, y en cada copia del código los parámetros son sustituidos por los valores especificados en la “llamada”.


Programa de ejemplo: Prog21

Subrutinas

Las subrutinas son una herramienta de programación muy potente. Permiten sacar “factor común” de aquellos trozos de código que se ejecutan muchas veces y reutilizarlo, además de permitir la creación de diferentes niveles de abstracción.

Las instrucciones relacionadas con subrutinas son:

  • “jsr”.
  • “rts”.

La instrucción “jsr” salta a la subrutina, es decir, la ejecuta. La instrucción “rts” termina la ejecución de la subrutina, es decir, devuelve la ejecución a la instrucción siguiente al “jsr”. Esto es posible gracias a que al ejecutar “jsr” se guarda la dirección de retorno en la pila.

Si un programa en código máquina se ejecuta desde BASIC con SYS 49152 (o la dirección que proceda), entonces “rts” también se utiliza para terminar ese programa en código máquina y volver a BASIC.

Una cuestión importante es cómo pasar información o parámetros a las subrutinas. Las dos formas principales son:

  • Mediante los registros del microprocesador.
  • Mediante posiciones de memoria.

Por ejemplo, las rutinas del Kernal utilizan con frecuencia los registros del microprocesador (acumulador, registro X o registro Y) para recibir los parámetros con los que trabajar. Por ejemplo, la rutina “chrout”, que pinta un carácter en pantalla, recibe el código de ese carácter mediante el acumulador.

Otra posibilidad es utilizar posiciones de memoria. El programa principal, que es el que llama a la subrutina, deposita la información en una o varias posiciones de memoria (que pueden ser de memoria general o de la página cero en particular), y la subrutina obtiene la información de esas mismas posiciones.

A mí particularmente me gusta más esta segunda opción (posiciones de memoria), porque los registros del microprocesador me parecen herramientas para trabajar de forma temporal o transitoria, cargando la información de una posición de memoria, procesando esa información, generando otra, devolviéndola a memoria, y así sucesivamente.

Los razonamientos anteriores (cómo pasar información o parámetros) aplican tanto a los parámetros de entrada, es decir, la información que el programa principal pasa a la rutina, como a los parámetros de salida, es decir, la información que la rutina podría devolver al programa principal.

Y por supuesto una subrutina podría llamar a otra, y así sucesivamente, mientras no se agote la pila. Precisamente esta es la forma de crear diferentes niveles de abstracción, desde rutinas más básicas, que hacen funciones sencillas, hasta rutinas que, a base de combinar otras, hacen funciones más complejas.

Y todo lo anterior nos lleva a otro debate importante: una rutina puede modificar los registros del microprocesador y/o posiciones de memoria. Es decir, salvo que el programador tome medidas para evitarlo (por ejemplo, salvaguardar los registros en la pila o en posiciones de memoria y recuperarlos al final de la rutina), lo más probable es que una rutina modifique los registros. Por tanto, con carácter general, el programador no puede confiar en que las rutinas que usa, que pueden ser propias o de terceros, dejen los registros del microprocesador como estaban antes de la llamada.

De hecho, cuando se documentan rutinas es muy habitual documentar el nombre, la función, la dirección de llamada (directa o a través de una “jump table”), cómo se pasan los parámetros de entrada y salida, y qué registros se modifican. Como ejemplo se puede consultar esta página que documenta las rutinas del Kernal: http://sta.c64.org/cbm64krnfunc.html.

“jsr” utiliza el modo de direccionamiento absoluto, porque debe ir acompañada de la dirección de la subrutina (primero el LSB y después en MSB). “rts”, en cambio, utiliza el modo de direccionamiento implícito ya que, al obtenerse la dirección de retorno de la pila, no necesita ningún operando.

Ninguna de estas instrucciones modifica los flags del registro de estado de forma directa. Ahora bien, igual que ocurre con el acumulador, el registro X o el registro Y, la ejecución de una rutina (digamos, el cuerpo de la misma) por supuesto que puede modificar los flags del registro de estado. De hecho, lo más habitual será que lo haga.

Los detalles de estas instrucciones pueden consultarse en http://www.6502.org/tutorials/6502opcodes.html#JSR y http://www.6502.org/tutorials/6502opcodes.html#RTS.


Programa de ejemplo: Prog20

Instrucciones de salto incondicional

La única instrucción de salto incondicional es:

  • “jmp”.

“jmp” produce un salto en el contador de programa a la dirección indicada, cuando se utiliza el modo de direccionamiento absoluto (ej. “jmp $c000”). Y produce un salto a la dirección contenida en la dirección indicada (puntero o vector), cuando se utiliza el modo de direccionamiento indirecto (ej. “jmp ($c000)).

En este último caso, puesto que una posición de memoria sólo puede contener un único byte, y una dirección del C64 requiere dos bytes, se aplica la regla de que el LSB de la dirección de destino está en la posición indicada en la instrucción, y el MSB de la dirección de destino está en la posición siguiente (ej. $c000 => LSB destino, $c001 => MSB destino). Esto es así porque el 6510 sigue la arquitectura “Little endian”.

Jmp

“jmp” no soporta más modos de direccionamiento. Sólo el absoluto y el indirecto. Y no modifica ningún flag del registro de estado.

Sus detalles pueden verse aquí: http://www.6502.org/tutorials/6502opcodes.html#JMP.

Instrucciones de rotación de bits

Las instrucciones de rotación de bits son:

  • “rol”.
  • “ror”.

Estas instrucciones son básicamente equivalentes a las ya vistas “asl” y “lsr”, con la única diferencia de que el bit que sale del acarreo se vuelve a introducir en el bit 0, en el caso de “rol”, y en el bit 7, en el caso de “ror”.

Rot bits

Estas instrucciones soportan los mismos modos de direccionamiento que “asl” y “lsr”, es decir, los modos: acumulador, absoluto, página cero, absoluto indexado y página cero indexado. Y afectan a los mismos flags: S – sign, Z – zero y C – carry.

Sus detalles se pueden consultar aquí: http://www.6502.org/tutorials/6502opcodes.html#ROL.


Programa de ejemplo: Prog19

Instrucciones de desplazamiento de bits

Las instrucciones de desplazamiento de bits ya se presentaron al hablar de multiplicaciones y divisiones:

  • “asl”.
  • “lsr”.

La instrucción “asl” mueve los bits del acumulador (o de una posición de memoria) un bit a la izquierda, metiendo el bit más significativo (bit 7) en el flag C – carry, y un bit a 0 en el bit menos significativo (bit 0). Esta operación equivale a multiplicar por dos.

Contrariamente, la instrucción “lsr” mueve los bits del acumulador (o de una posición de memoria) un bit a la derecha, metiendo un bit a 0 en el bit más significativo (bit 7), y el bit menos significativo (bit 0) en el flag C – carry. Esta operación equivale a dividir por dos.

Desp bits

Estas instrucciones son útiles, con carácter general, para manipular bits. Por ejemplo, moviendo cuatro bits a la derecha, se puede pasar de un byte a su nibble más significativo.

Estas instrucciones soportan los modos de direccionamiento acumulador, absoluto, página cero, absoluto indexado y página cero indexado. Y afectan a los flags S – sign, Z – zero y C – carry.

Sus detalles se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#ASL y http://www.6502.org/tutorials/6502opcodes.html#LSR.


Programa de ejemplo: Prog18

Bucles

Juntando contadores, comparaciones y saltos condicionales es muy fácil hacer bucles, que son construcciones muy típicas en todo tipo de programación consistentes en repetir la ejecución del mismo segmento de programa N veces, hasta que se cumple una condición de fin.

En ensamblador del 6510 hay muchas formas de hacer bucles, pero la forma más típica consiste en utilizar el registro X o el registro Y como contador (creciente o decreciente), comparar X o Y contra un valor de fin y, mientras no se alcance ese valor de fin, incrementar / decrementar el contador y repetir la ejecución.

También es posible hacer bucles con contadores basados en posiciones de memoria (instrucciones “inc” y “dec”). Una ventaja de hacer esto es que, mediante el uso de varios bytes, se pueden superar fácilmente las 256 iteraciones, que es el tope cuando se utiliza un único registro índice (8 bits).


Ejemplo de bucle basado en el registro X: Prog08
Ejemplo de bucle basado en posiciones de memoria: Prog10

Instrucciones de comparación

Las instrucciones de comparación son:

  • “cmp”.
  • “cpx”.
  • “cpy”.

La primera compara el acumulador con el operando. La segunda compara el registro X con el operando. Y la última compara el registro Y con el operando.

En los tres casos, la comparación se articula como la resta registro – operando, y los flags del registro de estado se activan o desactivan en función de esa resta. Por ello, si la comparación/resta arroja la igualdad, se activa el flag Z – zero; en caso contrario, se desactiva el flag Z – zero. Igualmente, si la comparación/resta arroja que el registro es mayor que el operando (resta positiva), se activa el flag C – carry; en caso contrario (resta negativa), se desactiva el flag C – carry.

“cmp” soporta los modos de direccionamiento inmediato, absoluto, página cero, absoluto indexado, página cero indexado, indirecto – indexado e indexado – indirecto. Por su parte, “cpx” y “cpy” sólo soportan los modos de direccionamiento inmediato, absoluto y página cero.

Todas estas instrucciones modifican los flags S – sign, Z – zero y C – carry. Sus detalles se pueden consultar aquí: http://www.6502.org/tutorials/6502opcodes.html#CMP.

Instrucciones de incremento/decremento

Las instrucciones de incremento/decremento son:

  • “inx”.
  • “dex”.
  • “iny”.
  • “dey”.
  • “inc”.
  • “dec”.

Las dos primeras valen incrementar/decrementar el índice X. Las dos segundas valen para incrementar/decrementar el índice Y. Y las dos últimas valen para incrementar/decrementar una posición de memoria.

Incrementar/decrementar el índice X o el índice Y es útil para recorrer zonas de memoria (modos de direccionamiento indexados) y para implementar contadores. Incrementar/decrementar posiciones de memoria también es útil para recorrer zonas de memoria (modos de direccionamiento indirectos) y para implementar contadores. Y una aplicación obvia de los contadores son los bucles.

Las instrucciones “inx”, “dex”, “iny” y “dey” utilizan el modo de direccionamiento implícito y afectan a los flags S – sign y Z – zero.

Las instrucciones “inc” y “dec” soportan los modos de direccionamiento absoluto, página cero, absoluto indexado y página cero indexado, y también modifican los flags S – sign y Z – zero.

Los detalles de estas instrucciones se pueden consultar en http://www.6502.org/tutorials/6502opcodes.html#INX, http://www.6502.org/tutorials/6502opcodes.html#INC y http://www.6502.org/tutorials/6502opcodes.html#DEC.