Por todo esto, aparece el concepto de subrutina en código máquina. Se trata de un salto a un fragmento de código donde se va a realizar una tarea para, posteriormente, regresar a la siguiente instrucción desde donde se llamó. Veámoslo modificando el código de nuestro viejo amigo que imprime el contenido de un registro por la pantalla en hexadecimal:
.area PROG (ABS) ; definimos un par de constantes fin .equ 0xFF01 pantalla .equ 0xFF00 .org 0x100 .globl programa prefijo:.asciz "0x" ; prefijo programa: lds #0xF000 ; usaremos la pila. Le damos un valor seguro lda #28 ; pongamos este nUmero como prueba ; imprimamos 0x ldx #prefijo bsr imprime_cadena ; primero imprimamos la primera cifra hexadecimal tsta beq segunda_cifra tfr a,b lsrb lsrb lsrb lsrb ; en B estA la primera cifra, de 0 a 15 bsr imprime_cifra_hexa ; ahora imprimimos la segunda cifra hexadecimal segunda_cifra: tfr a,b andb #0b1111 ; en B estA la segunda cifra, de 0 a 15 bsr imprime_cifra_hexa ; imprimamos un salto de lInea al final ldb #'\n stb pantalla ; el programa acaba clra sta fin imprime_cadena: pshs a sgte: lda ,x+ beq ret_imprime_cadena sta pantalla bra sgte ret_imprime_cadena: puls a rts imprime_cifra_hexa: cmpb #10 blo decimal addb #'A-'9-1 decimal:addb #'0 stb pantalla rts .org 0xFFFE ; vector de RESET .word programa
Lo primero que se observa es que el programa es mucho más legible. Las subrutinas le dotan de un significado más próximo al programador. Es recomendable, por el mismo motivo, que las etiquetas también tengan un significado que propicie la lectura del programa.
Observad cómo, en la subrutina imprime_cadena
,
se mete en la pila el valor de A para recuperarlo al final. Si no
se hiciera así, el programa no funcionaría porque
la subrutina machacaría el contenido de A, donde a esas alturas
ya está el número que se quiere imprimir.
Por las razones expuestas, es muy conveniente documentar bien las subrutinas con una cabecera que debe, al menos, especificar: su nombre, su función, registros o variables de entrada, registros o variables de salida y registros que modifica la subrutina. Hagámoslo para nuestras dos subrutinas:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; imprime_cadena ; ; saca por la pantalla la cadena acabada en '\0 apuntada por X ; ; ; ; Entrada: X-direcciOn de comienzo de la cadena ; ; Salida: ninguna ; ; Registros afectados: X, CC. ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; imprime_cadena: pshs a sgte: lda ,x+ beq ret_imprime_cadena sta pantalla bra sgte ret_imprime_cadena: puls a rts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; imprime_cifra_hexa ; ; saca por la pantalla la cifra hexadecimal contenida en B ; ; ; ; Entrada: B-cifra hexadecimal, del 0 al 15 ; ; Salida: ninguna ; ; Registros afectados: B, CC. ; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; imprime_cifra_hexa: cmpb #10 blo decimal addb #'A-'9-1 decimal:addb #'0 stb pantalla rts
El funcionamiento, a bajo nivel, de las llamadas a subrutinas es ingenioso. Dispone el 6809 de tres instrucciones para realizarlas: JSR, BSR y LBSR. Su funcionamiento en cuanto al salto que hay que hacer es equivalente a las ya vistas: JMP, BRA y LBRA. En esencia, consiste en cargar el PC con la dirección de inicio de la subrutina, pero, justo antes de eso, aunque una vez leída la instrucción y con el PC ya apuntando a la instrucción siguiente, el procesador guarda el PC en la pila S:
La jugada es clara. Cuando acabe de ejecutar la subrutina y se encuentre con la instrucción RTS (ReTurn from Subroutine), solamente tiene que sacar el valor de la pila y cargar el PC. Por arte de magia, la ejecución continúa en la instrucción siguiente desde donde se realizó el salto:
Con lo que ya sabemos, podemos decir que la instrucción RTS es
completamente equivalente a PULS PC, solo que, como RTS ocupa un byte y
PULS PC dos, es más rápida y casi siempre preferible.
Las tornas cambian cuando, a la vez que volver de una subrutina, queremos
sacar de la pila algún registro, cosa que ocurría al
final de imprime_cadena
, más arriba:
imprime_cadena: pshs a [...] puls a rtsEn esa circunstancia, es más rápido ejecutar una sola orden PULS, como es obvio:
imprime_cadena: pshs a [...] puls a,pcNo hay que preocuparse por el orden en que se realiza la extracción. Es el correcto: el PC siempre es el último registro que se saca, como vimos en la sesión anterior. Y es que los ingenieros que diseñaron el 6809 no daban puntada sin hilo...
imprime_cadena
: saca por la pantalla la cadena apuntada
por X, que ha de acabar en \0
.
Registros afectados: X y CCimprime_decimal
: saca por la pantalla, en decimal,
el número contenido en D, interpretado como sin signo.
Registros afectados: D y CC1 .area PROG (ABS) 2 3 ; definimos una constante FF01 4 fin .equ 0xFF01 FF00 5 pantalla .equ 0xFF00 6 0100 7 .org 0x100 8 .globl programa 9 0100 10 programa: 0100 10 CE F0 00 [ 4] 11 lds #0xF000 ; usaremos la pila. Le damos un valor seguro 0104 86 1C [ 2] 12 lda #28 ; pongamos este nUmero como prueba 13 0106 8D 1A [ 7] 14 bsr imprime_byte_hexa 15 16 ; imprimamos un salto de lInea al final 0108 C6 0A [ 2] 17 ldb #'\n 010A F7 FF 00 [ 5] 18 stb pantalla 19 20 ; el programa acaba 010D 4F [ 2] 21 clra 010E B7 FF 01 [ 5] 22 sta fin 23 24 25 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 26 ; imprime_cadena ; 27 ; saca por la pantalla la cadena acabada en '\0 apuntada por X ; 28 ; ; 29 ; Entrada: X-direcciOn de comienzo de la cadena ; 30 ; Salida: ninguna ; 31 ; Registros afectados: X, CC. ; 32 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 33 0111 34 imprime_cadena: 0111 34 02 [ 6] 35 pshs a 0113 A6 80 [ 6] 36 sgte: lda ,x+ 0115 27 05 [ 3] 37 beq ret_imprime_cadena 0117 B7 FF 00 [ 5] 38 sta pantalla 011A 20 F7 [ 3] 39 bra sgte 011C 40 ret_imprime_cadena: 011C 35 02 [ 6] 41 puls a 011E 39 [ 5] 42 rts 43 44 45 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 46 ; imprime_byte_hexa ; 47 ; saca por la pantalla, en hexadecimal, el byte contenido en A ; 48 ; ; 49 ; Entrada: A-byte para imprimir ; 50 ; Salida: ninguna ; 51 ; Registros afectados: CC. ; 52 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 53 011F 30 78 00 54 prefijo:.asciz "0x" ; prefijo 0122 55 imprime_byte_hexa: 0122 34 16 [ 8] 56 pshs x,d 57 ; imprimamos 0x 0124 8E 01 1F [ 3] 58 ldx #prefijo 0127 8D E8 [ 7] 59 bsr imprime_cadena 60 61 ; primero imprimamos la primera cifra hexadecimal 0129 4D [ 2] 62 tsta 012A 27 08 [ 3] 63 beq segunda_cifra 012C 1F 89 [ 6] 64 tfr a,b 012E 54 [ 2] 65 lsrb 012F 54 [ 2] 66 lsrb 0130 54 [ 2] 67 lsrb 0131 54 [ 2] 68 lsrb ; en B estA la primera cifra, de 0 a 15 0132 8D 08 [ 7] 69 bsr imprime_cifra_hexa 70 71 ; ahora imprimimos la segunda cifra hexadecimal 0134 72 segunda_cifra: 0134 1F 89 [ 6] 73 tfr a,b 0136 C4 0F [ 2] 74 andb #0b1111 0138 8D 02 [ 7] 75 bsr imprime_cifra_hexa 013A 35 96 [ 9] 76 puls x,d,pc 77 78 79 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 80 ; imprime_cifra_hexa ; 81 ; saca por la pantalla la cifra hexadecimal contenida en B ; 82 ; ; 83 ; Entrada: B-cifra hexadecimal, del 0 al 15 ; 84 ; Salida: ninguna ; 85 ; Registros afectados: B, CC. ; 86 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 87 013C 88 imprime_cifra_hexa: 013C C1 0A [ 2] 89 cmpb #10 013E 25 02 [ 3] 90 blo decimal 0140 CB 07 [ 2] 91 addb #'A-'9-1 0142 CB 30 [ 2] 92 decimal:addb #'0 0144 F7 FF 00 [ 5] 93 stb pantalla 0147 39 [ 5] 94 rts 95 FFFE 96 .org 0xFFFE ; vector de RESET FFFE 01 00 97 .word programa 98
Observad cómo ha aparecido una nueva subrutina
imprime_byte_hexa
que, a su vez, realiza dos llamadas
a la vieja subrutina imprime_cifra_hexa
para imprimir
cada una de las cifras del número hexadecimal.
imprime_cifra_hexa
promete en su contrato que solamente
resultará afectado el registro CC, por lo que hace una cosa
muy habitual en las subrutinas, guardar en la pila el valor de los
registros que modifica en el cuerpo pero que quiere devolver intactos.
Fijaos también que, en la línea 76 vuelve de la
subrutina sacando el PC directamente de la pila, en lugar de usar
RTS.
Observad cómo se va a producir la cadena de llamadas y cómo entran y salen las direcciones de retorno de la pila en el siguiente gráfico. Como se puede ver, para hacer llamadas anidadas, es necesario utilizar una pila, pues la dirección a la que se retorna (la que se saca de la pila) es la última que entró:
lee_cadena
: lee del teclado y escribe, a partir de la
posición marcada por X, caracteres hasta que el usuario
pulse intro (\n
). El carácter \n
no se almacena en la memoria y la cadena se acaba en \0
en la memoria. A la salida X apunta a la posición donde se
almacenó el carácter \0
.
Registros afectados: X y CClee_cadena_n
: igual que el caso anterior, pero admite
una entrada más en el registro A que será el número
máximo de caracteres que se almacenarán en la memoria
contando el \0
del final, que siempre se escribe.
En A devuelve el número de caracteres leídos.
Registros afectados: X y CClee_decimal
: igual que el segundo caso, pero lo que se
lee se interpreta como un número decimal. El número se
devuelve en D. En caso de error, por haber caracteres que no sean
números ni espacios o por pasarse de la capacidad del registro
D, se devuelve en D cero y se activa el
bit de acarreo. Si hay espacios al principio, se ignoran. Si aparece
un espacio después de haber leído una varias cifras,
se acaba la lectura, pero no es un error. Si no hay error,
el bit de acarreo vuelve desactivado.
Registros afectados: CCEl mecanismo más elemental consiste en copiar y pegar las subrutinas que necesitamos. Un mecanismo un poco más avanzado es usar una directiva del ensamblador para que incluya, cuando ensamble, el código que se encuentra en otro fichero.
Una directiva es muy similar a un nemónico salvo que la orden que
se da se le da al propio programa ensamblador. Las directivas, aunque
siguen un estilo similar, son propias de cada ensamblador y pueden cambiar
de uno a otro. Los nemónicos son propios del procesador y no
cambian. Las directivas comienzan por el carácter punto
(.
). Ya hemos usado una (.ORG). En esta sesión
aprenderemos algunas más.
La directiva que incluye otro fichero, cuando se ensamble, en un punto de nuestro programa es .INCLUDE:
;; AquI irIa el cOdigo propio del programa ;; Incluimos el fichero print.asm ;; Cuando se ensamble sucede como si lo hubiEramos pegado aquI .include "print.asm"
Aunque interesante, la inclusión de ficheros tal cual presenta algunos inconvenientes:
A consecuencia de esto, se impone un enfoque más sistemático, que permita guardar, organizar y usar subrutinas directamente en código máquina (código objeto)
prueba.asm
:
.module prueba .area CODE1 (ABS) .org 0x100 .globl programa .globl print_a .globl print_b .globl print_c programa: lds #0xF000 jsr print_a jsr print_b jsr print_c clra sta 0xFF01 .org 0xFFFE .word programaPrestad atención a lo siguiente:
CODE1
. A las áreas se les pueden dar los
siguientes atributos entre paréntesis:
.asm
, debe tener los mismos
atributos en todos.
programa
(se define en nuestro código
y vale 0x100) y las etiquetas print_a
,
print_b
y print_c
(se usan en nuestro
código, pero no se definen)print_a
,
print_b
y print_c
, hemos hecho un
salto con JSR y no con BSR, pues este último tiene la
limitación de no poder satar más de 120 bytes
adelante o atrás del punto actual, aproximadamente.Compilemos nuestro programa, como haríamos habitualmente y obtengamos también un listado:
as6809 -o prueba.asm as6809 -l prueba.asm
Observemos el listado que se produce:
1 .module prueba 2 3 .area CODE1 (ABS) 0100 4 .org 0x100 5 .globl programa 6 .globl print_a 7 .globl print_b 8 .globl print_c 9 0100 10 programa: 0100 10 CE F0 00 [ 4] 11 lds #0xF000 0104 BD 00 00 [ 8] 12 jsr print_a 0107 BD 00 00 [ 8] 13 jsr print_b 010A BD 00 00 [ 8] 14 jsr print_c 010D 4F [ 2] 15 clra 010E B7 FF 01 [ 5] 16 sta 0xFF01 17 FFFE 18 .org 0xFFFE FFFE 01 00 19 .word programa 20 [...] Symbol Table .__.$$$. = 2710 L | .__.ABS. = 0000 G .__.CPU. = 0000 L | .__.H$L. = 0001 L print_a **** GX | print_b **** GX print_c **** GX | 2 programa 0100 GR
Vemos que el código y los dos últimos bytes se
sitúan (al ser un área absoluta) en posiciones fijas,
que son las marcadas por las directivas .ORG. Observad, además,
que en las líneas 12, 13 y 14, el ensamblador no conoce en qué
direcciones estarán las subrutinas print_a
,
print_b
y print_c
. Es por eso, que las
direcciones de salto (los dos bytes a continuación del
código de operación 0xBD) son 0 y en la tabla de
símbolos aparecen los valores de las etiquetas como asteriscos.
Escribimos ahora el primero de los módulos con subrutinas,
al que vamos a llamar printab.asm
:
.module printab .globl print_a .globl print_b print_a:lda #'A sta 0xFF00 rts print_b:lda #'B sta 0xFF00 rts
Y el segundo, cuyo nombre será printc.asm
:
.module printc .globl print_c print_c:lda #'C sta 0xFF00 rtsLos módulos no son de buenos como para tirar cohetes, pero nos sirven para el ejemplo. Notad algunas diferencias respecto del módulo principal:
print_
,
pues queremos que las puedan usar el resto de módulosPasemos, a continuación, a ensamblarlos y a generar sus listados:
as6809 -o printab.asm as6809 -l printab.asm as6809 -o printc.asm as6809 -l printc.asm
El listado del ensamblado de los dos ficheros es este:
***************** * printab.lst * ***************** 1 .module printab 2 3 .globl print_a 4 .globl print_b 0000 86 41 [ 2] 5 print_a:lda #'A 0002 B7 FF 00 [ 5] 6 sta 0xFF00 0005 39 [ 5] 7 rts 0006 86 42 [ 2] 8 print_b:lda #'B 0008 B7 FF 00 [ 5] 9 sta 0xFF00 000B 39 [ 5] 10 rts 11 [...] Symbol Table .__.$$$. = 2710 L | .__.ABS. = 0000 G .__.CPU. = 0000 L | .__.H$L. = 0001 L 0 print_a 0000 GR | 0 print_b 0006 GR ***************** * printc.lst * ***************** 1 .module printc 2 3 .globl print_c 0000 86 43 [ 2] 4 print_c:lda #'C 0002 B7 FF 00 [ 5] 5 sta 0xFF00 0005 39 [ 5] 6 rts 7 [...] Symbol Table .__.$$$. = 2710 L | .__.ABS. = 0000 G .__.CPU. = 0000 L | .__.H$L. = 0001 L 0 print_c 0000 GR
Observamos que, al ser módulos reubicables, se ensamblan todos a partir de la dirección 0.
Resumiendo, al final de la fase de ensamblado, tenemos tres módulos
de código objeto: prueba.rel
(absoluto),
printab.rel
(reubicable) y printc.rel
(reubicable). Pensad que, en un caso real, puede que alguno de
ellos fuera comprado y ni siquiera tuviéramos el código en
ensamblador.
Ahora toca componer este rompecabezas para formar un único fichero
ejecutable. Esto es tarea del enlazador aslink
, al que
antes ya hemos acudido, pero al que ahora venimos con un problema algo
más complicadillo. La tarea del enlazador será:
La sintaxis de la orden aslink
es un poco desafortunada.
Cuando se le pasa más de un fichero, el primero es donde va a
ir el ejecutable de salida y los siguientes son los módulos que
queremos que lo compongan. Demos la orden más compleja
posible de las que aprenderemos
(-s
, salida en formato Motorola;
-m -w
, queremos mapa de símbolos;
-u
, queremos listados actualizados con el valor que han
tomado las etiquetas
de cada módulo después del enlazado):
aslink -s -m -w -u prueba.s19 prueba.rel printab.rel printc.rel
Observemos parte del mapa de símbolos generado:
Area Addr Size Decimal Bytes (Attributes) -------------------- ---- ---- ------- ----- ------------ CODE1 0000 0000 = 0. bytes (ABS,OVR,CSEG) Value Global Global Defined In Module ----- -------------------------------- ------------------------ 0100 programa prueba [...] Area Addr Size Decimal Bytes (Attributes) -------------------- ---- ---- ------- ----- ------------ _CODE 0000 0012 = 18. bytes (REL,CON,CSEG) Value Global Global Defined In Module ----- -------------------------------- ------------------------ 0000 print_a printab 0006 print_b printab 000C print_c printc
Prestad atención a que el módulo printab
ha
encontrado sitio en su posición original. Por ese motivo, el
módulo printc
se ha encontrado con la posición
ocupada y el enlazador no ha tenido más remedio que reubicarlo
en la dirección 0x000C. Una prueba más de que ha ocurrido
así son los listados que
nos ha generado el enlazador con todos los símbolos ya resueltos
(extensión .rst
) por dar la orden -u
:
***************** * prueba.rst * ***************** 1 .module prueba 2 3 .area CODE1 (ABS) 0100 4 .org 0x100 5 .globl programa 6 .globl print_a 7 .globl print_b 8 .globl print_c 9 0100 10 programa: 0100 10 CE F0 00 [ 4] 11 lds #0xF000 0104 BD 00 00 [ 8] 12 jsr print_a 0107 BD 00 06 [ 8] 13 jsr print_b 010A BD 00 0C [ 8] 14 jsr print_c 010D 4F [ 2] 15 clra 010E B7 FF 01 [ 5] 16 sta 0xFF01 17 FFFE 18 .org 0xFFFE FFFE 01 00 19 .word programa 20 ***************** * printab.rst * ***************** 1 .module printab 2 3 .globl print_a 4 .globl print_b 0000 86 41 [ 2] 5 print_a:lda #'A 0002 B7 FF 00 [ 5] 6 sta 0xFF00 0005 39 [ 5] 7 rts 0006 86 42 [ 2] 8 print_b:lda #'B 0008 B7 FF 00 [ 5] 9 sta 0xFF00 000B 39 [ 5] 10 rts 11 ***************** * printc.rst * ***************** 1 .module printc 2 3 .globl print_c 000C 86 43 [ 2] 4 print_c:lda #'C 000E B7 FF 00 [ 5] 5 sta 0xFF00 0011 39 [ 5] 6 rts 7
.asm
para cada una de las funciones
pedidas en los ejercicios anteriores de esta sesión y generad sus
ficheros objeto .rel
de modo que puedan ser enlazados
en proyectos posteriores.
prueba2.asm
con estas consideraciones:
.module prueba2 .globl programa .globl print_a .globl print_b .globl print_c programa: lds #0xF000 jsr print_a jsr print_b jsr print_c clra sta 0xFF01 .area FIJA (ABS) .org 0xFFFE .word programa
Repitiendo todo el proceso anterior, sin necesidad de volver a ensamblar
ni printab.asm
ni printc.asm
, pues de eso se trata,
obtenemos el ejecutable.
Todo el código que no pertenece a ninguna área es asignado
por el ensamblador a un área por defecto que se denomina
_CODE
. Esta área, como bien supondréis, es
de tipo REL.
Puede molestar que el código final comience en la dirección 0. Podemos dar órdenes al enlazador para que coloque las áreas REL que queramos en determinados sitios de la memoria. Por ejemplo, si queremos que el código empiece en la dirección 0x100:
aslink -s -m -w -b _CODE=0x100 prueba2.s19 prueba2.rel printab.rel printc.rel
El sistema simple con el que estamos trabajando tiene un método
para proporcionar una
función similar, aunque mucho más limitada. Creemos una
biblioteca de módulos objeto con nuestros dos módulos.
Es muy sencillo. Solamente hay que crear un fichero, lo llamaremos
print.lib
con los nombres de los módulos que forman
parte de la biblioteca, uno por línea:
printab.rel printc.rel
Ahora podemos enlazar haciendo referencia a la biblioteca, en lugar de de a los módulos directamente:
aslink -s -m -w -b _CODE=0x100 prueba2.s19 prueba2.rel -l print
La opción es -l
y del nombre de la biblioteca
se puede omitir la extensión .lib
.
No es un sistema muy completo porque aún necesitamos tener presentes los módulos en sí. En los sistemas normales es el propio fichero de la biblioteca el que contiene el código objeto de sus módulos, de modo que es autosuficiente.
imprime.lib
y lee.lib
con las correspondientes funciones del primer ejercicio. Compilad
el segundo ejercicio enlazando mediante las bibliotecas.
JSR x
BSR x
LBSR
RTS
.INCLUDE "x"
.MODULE x
.AREA x (f)
.ORG x
.GLOBL x1,
x2, ...
xn
x
s son etiquetas
exportables o
compartidas entre los módulos (y definida cada una en un
módulo, o en varios si coincide la definición).LOCAL x1,
x2, ...
xn
x
s son etiquetas locales.
Solamente conoce de cada una el módulo donde se ha definido y
usado. Puede haber varias etiquetas locales en distintos módulos
con el mismo nombre y no interfieren entre sí.e .EQU x
.BYTE x1,
x2, ...
xn
.WORD x1,
x2, ...
xn
.ASCII "x"
.ASCIZ "x"
.DS x
ls
cd
rm
man
cat
echo $?