Skip to content

L14: Practica 6‐2

Juan Gonzalez-Gomez edited this page Jan 15, 2026 · 173 revisions

L14: Macros

  • Objetivos de la sesión:
    • Entender el concepto de macro
    • Aprender a hacer macros con el RARs
    • Realizar llamadas al sistema mediante macros

Contenido

Introducción

La programación en ensamblador es tediosa. Para hacer cosas simples, como por ejemplo llamar a una función a la que se le pasan parámetros, es necesario utilizar varias instrucciones. Lo mismo ocurre cuando invocamos los servicios del sistema operativo: hay que utilizar múltiples instrucciones. Además, la mayoría de las veces estos mecanismos son repetitivos, y siempre se utilizan de la misma manera

Para simplificar estas tareas repetitivas, y hacer más fácil la programación en ensamblador, aparece el concepto de macros, que también se utiliza en los lenguajes de alto nivel, como C o Rust por ejemplo

Las macros NO están definidas en la ISA del RISCV, y por tanto es algo que dependen del ensamblador utilizado. Cada uno tiene su propia sintáxis, aunque las ideas son comunes para todos ellos. En esta sesión aprenderemos a crear y utilizar macros en el simulador RARs

Las macros son una extensión de las constantes. Funcionan de manera similar, pero son más potentes. Cuando el ensamblador lee el identificador de una macro previamente difinida, la sustituye por el código en ella indicada. Es como si hiciese un "copy & paste". Con esto crea un nuevo programa en ensamblador, que es el que se convertirá en código máquina en una segunda pasada

En esta sesión aprenderemos a utilizar mácros a partir de ejemplos, empezando por las más sencillas: las que NO tienen parámetros. Luego veremos las que tienen parámetros, y cómo con ellas podemos hacer cosas más útiles. Finalmente crearemos una biblioteca de macros en el fichero so.h para acceder a los servicios del sistema operativo

Macros sin parámetros

Comenzamos con las macros más sencillas: las que NO tienen parámetros. La vamos a introducir progresivamente, a partir de un programa hola mundo que conocemos muy bien

Ejemplo 1: hola mundo

Vamos a comenzar con una versión del programa Hola mundo, que imprime una cadena en la consola y termina. Este programa ya lo conocemos y tenemos totalmente controlado

	.include "servicios.s"

	.data
msg:	.string "Esto es una prueba"

	.text
	#-- Imprimir mensaje
	la a0, msg
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall

Esto es lo que obtenemos al ejecutarlo:

El programa tiene 6 instrucciones:

Mi primera macro: M_EXIT

Vamos a fijarnos en las dos instrucciones para TERMINAR:

li a7, EXIT
ecall

Se trata de una llamada al sistema operativo, que ya conocemos de sobra. Siempre, en cada programa que queremos finalizar, tenemos que escribir estas dos instrucciones para devolver el control al sistema operativo. Y SIEMPRE SON LAS MISMAS DOS INSTRUCCIONES

Es posible crear una especie de CONSTANTE que represente a estas dos instrucciones. Cuando el programa ensamblador lee esta "CONSTANTE", la sustituye por las dos instrucciones. Esta consante especial se denomina MACRO

Vamos a definir la macro M_EXIT que incluye esas dos instrucciones. Lo hacemos con las directivas .macro y .end_macro

.macro M_EXIT
  li a7, EXIT
  ecall
.end_macro

Una vez definida, cada vez que el ensamblador ve el identificador M_EXIT, lo sustituye por esas dos instrucciones

Así, el programa hola mundo lo podemos reescribir de esta manera:

#-- Ejemplo hola mundo
	
	.include "servicios.s"

	#-- Definicion de la macro M_EXIT
	.macro M_EXIT
	  li a7, EXIT
	  ecall
	.end_macro

	.data
msg:	.string "Esto es una prueba"

	.text
	#-- Imprimir mensaje
	la a0, msg
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT

El programa funciona igual y tiene exactamente las mismas 6 instrucciones. Al ensamblarlo vemos esto:

Las dos últimas instrucciones son las que el ensamblador ha añadido a partir de la macro M_EXIT. Es lo que se denomina la expansión de la macro

Observa que aparecen los números <7> y <8> delante de estas instrucciones. Eso indica que esas instrucciones están en realidad dentro de una macro, cuya definición está en las líneas 7 y 8

Nomenclatura para macros

El nombre asignado a una macro puede ser cualquiera. El mismo que podemos dar a cualquier etiqueta. Sin embargo, igual que sucede con las CONSTANTES, típicamente se usan nombres EN MAYÚSCULAS

En esta sesión, además, las macros relaciondas con el sistema operaitivo llevan el pefijo M_ seguido por el nombre del servicio (que es el que usamos con las constantes del sistema oeprativo). Así, para acceder al servicio exit, sabemos que la constante EXIT contiene el código de servicio y M_EXIT es el nombre de la macro para acceder a este servicio

Usando la macro M_EXIT

Para utilizar las macros hacemos lo mismo que con las constantes: las ponemos en un archivo separado que incluimos en el programa donde las vamos a usar. La macro M_EXIT está relacionada con el sistema operativo, por lo que lo colocamos en el mismo fichero donde tenemos las constantes del sistema operativo: servicios.s

  • Fichero: servicios.s
	#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv PRINT_INT_HEX 34
	.eqv PRINT_INT_BINARY 35
	.eqv SLEEP         32
	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro

A partir de ahora, para acceder al servicio EXIT del sistema operativo hay que incluir el fichero servicios.s y escribir M_EXIT. Así es como queda el programa principal

	.include "servicios.s"

	.data
msg:	.string "Esto es una prueba"

	.text
	#-- Imprimir mensaje
	la a0, msg
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT

Expansión de la macro M_EXIT

El proceso de sustituir el nombre de la macro por sus instrucciones equivalentes se llama expansión de la macro. Esta es una operación que realiza el ensamblador. A partir del programa original el ensamblador genera otro programa donde ya NO hay macros, sino código como si lo hubiesemos tecleado nosotros

La forma de crear macros NO es parte de la ISA DEL RISCV. Esto significa que no está estandarizado y por tanto cada ensamblador define su propia sintáxis para implementar macros

No todos los ensambladores tienen la capacidad de incluir macros. Los que sí la incluyen se denominan macro-ensambladores

Ejemplo 2: Pulsa ENTER

En este ejemplo se pide al usuario que apriete la tecla ENTER dos veces, y se imprimen dos mensajes diferentes. Para ello se llama al servicio READ_CHAR del sistema operativo

	.include "servicios.s"

	.data
 msg1:	.string "Pulsa ENTER"
 msg2:  .string "Pulsa ENTER otra vez"

	.text
	#-- Imprimir mensaje 1
	la a0, msg1
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar a que se apriete ENTER
	li a7, READ_CHAR
	ecall
	
	#-- Imprimir mensaje 2
	la a0, msg2
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar a que se apriete ENTER
	li a7, READ_CHAR
	ecall
	
	#-- Terminar
	M_EXIT

Ensamblamos el programa. Tiene un total de 14 instrucciones

Este es el resultado de la ejecución:

Macro M_READ_CHAR

Cada vez que llamamos al servicio READ_CHAR para leer un carácter hay que utilizar siempre estas dos instrucciones:

  li a7, READ_CHAR
  ecall

Están pidiendo a gritos que las metamos en la macro M_READ_CHAR. Esta es su definición:

.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

Ahora desde el programa principal bastará con escribir M_READ_CHAR cada vez que queremos invocar el servicio READ_CHAR

Lo incluimos en el fichero servicios.s:

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv PRINT_INT_HEX 34
	.eqv PRINT_INT_BINARY 35
	.eqv SLEEP         32
	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

Usando la macro M_READ_CHAR

Así es como queda el programa principal, llamando a la nueva macro M_READ_CHAR:

	.include "servicios.s"

	.data
 msg1:	.string "Pulsa ENTER"
 msg2:  .string "Pulsa ENTER otra vez"

	.text
	#-- Imprimir mensaje 1
	la a0, msg1
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar a que se apriete ENTER
	M_READ_CHAR
	
	#-- Imprimir mensaje 2
	la a0, msg2
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar a que se apriete ENTER
	M_READ_CHAR
	
	#-- Terminar
	M_EXIT

El programa es exactamente el mismo que antes. Tiene las mismas 14 instrucciones

Expansión de las macros

En el ejemplo 2 hay 3 macros. Lo primero que hace el ensamblador es expandir estas macros, para obtener el programa completo a ensamblar como se muestra en esta figura

Macros con parámetros

Las macros también pueden tener parámetros. Esto las hace mucho más útiles. Por ejemplo, las podemos utilizar para los servicios del sistema operativo PRINT_INT y PRINT_CHAR. En ellas hay que colocar en a0 el número o carácter a imprimir. La macro lo hace por nosotros

Ejemplo 3: Imprimir una fecha

En este programa se imprime en la consola una fecha, en formado año/mes/dia. Para ello llamamos a los servicios PRINT_INT y PRINT_CHAR

#-- Imprimir una fecha

	.include "servicios.s"

	.text
	#-- Imprimir año
	li a0, 2025
	li a7, PRINT_INT
	ecall
	
	#-- Imprimir '/'
	li a0, '/'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir mes
	li a0, 12
	li a7, PRINT_INT
	ecall
	
	#-- Imprimir '/'
	li a0, '/'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir dia
	li a0, 21
	li a7, PRINT_INT
	ecall
	
	#-- Imprimir salto de linea
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Terminar
	M_EXIT

La fecha que se imprime es 2025/12/21. Esta es la salida en la consola:

El programa ensamblado tiene 20 instrucciones:

Para la impresión de cada uno de los números de la fecha utilizamos 3 instrucciones. Todas son iguales salvo la primera, que tiene asigna al resigtro a0 el número a imprimir. Tiene esta sintáxis: li a0, <num>, donde <num> es el número a guardar en a0, que es diferente en cada caso. Primero 2025, luego 12 y por último 21

Ese número a guardar en a0 es nuestro parámetro

Macro con 1 parámetro: M_PRINT_INT

Para denotar parámetros en una macro utilizamos el prefijo %. En esta macro tenemos un parámetro que representa a un número constante, que llamaremos %num. La macro la definimos así:

.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

Si ahora, por ejemplo, queremos imprimir el número 2025, basta con escribir M_PRINT_INT(2025) y el ensamblador automáticamente lo expande a este código:

  li a0, 2025
  li a7, PRINT_INT
  ecall

¡Nos ahorramos escribir esas 3 líneas!. La impresión de un número entero es ahora mucho más fácil, y se parece más a cómo lo hacemos en un lenguaje de alto nivel

Igual que hicimos con las macros anteriores, la metemos en el fichero servicios.s para usarla con facilidad en nuestros programas

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv PRINT_INT_HEX 34
	.eqv PRINT_INT_BINARY 35
	.eqv SLEEP         32
	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

#-- Servicio para imprimir un numero entero
#-- Parametro: %num: Numero a imprimir
#-- NOTA: %num tiene que ser un NUMERO CONSTANTE
.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

Usando la macro M_PRINT_INT

Con esta nueva macro el ejemplo 3 es más corto, y también más legible

#-- Imprimir una fecha

	.include "servicios.s"

	.text
	#-- Imprimir año
	M_PRINT_INT(2025)
	
	#-- Imprimir '/'
	li a0, '/'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir mes
	M_PRINT_INT(12)
	
	#-- Imprimir '/'
	li a0, '/'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir dia
	M_PRINT_INT(21)
	
	#-- Imprimir salto de linea
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Terminar
	M_EXIT

La salida en consola es exactamente la misma, y el programa sigue teniendo las mismas 20 instrucciones:

Sin embargo hemos ganado en legibilidad. El código es más sencillo de escribir y de entender, sin incrementar su tamaño. ¡Todo son ganancias!

Error en el parámetro pasado

Hay que tener cuidado al usar las macros, especialmente si las ha creado otra perosna y no las tenemos controladas. La macro M_PRINT_INT() sirve para imprimir un número entero, pero al verla como una macro podemos estar tentados a pasar cualquier argumento

Por ejemplo, nos podría parecer intuitivo utilizarla para imprimir el número enterior que está almacenado en el registro t0:

	.include "servicios.s"
	
	.text
	
	#-- En t0 tenemos el entero a imprimir
	li t0, 2025
	
	#-- Imprimir el entero de t0!
	M_PRINT_INT(t0)
	
	#-- Terminar
	M_EXIT

Sin embargo, al utilizarla de esta manera aparece un mensaje de error: `"t0": operand is of incorrect type´

El RARs nos lleva al fichero donde está definida la macro y nos señala la línea del error. Pero lo que vemos ahí en principio no es incorrecto: li a0, %num. De hecho la macro es correcto. El problema está causado en cómo hemos usado la macro

El ensamblador al expandir la macro NO COMPRUEBA EL TIPO DE PARÁMETRO. Lo que introduzcamos como parámetro lo mete directamente en la instrucción li a0, %num. Como hemos introducido t0, la expansión produce la instrucción li a0, t0, que es INCORRECTA

El menesaje de error lo genera el ensamblador al intentar ensamblar la instrucción li a0, t0

Error: Ningún parámetro pasado

De igual forma, si usamos la macro M_PRINT_INT sin pasarle ningún parámetro, se produce el mensaje de error: forward reference or invalid parameters for macro "M_PRINT_INT"

Por ejemplo, tenemos este programa incorrecto:

	.include "servicios.s"
	
	.text
	
	#-- llamar a la macro sin parametros
	M_PRINT_INT()
	
	#-- Terminar
	M_EXIT

Al ensamblarlo obtenemos este mensaje de error:

La macro M_PRINT_CHAR

El ejemplo de la impresión de la fecha todavía lo podemos simplificar más. Estamos llamando 3 veces al servicio PRINT_CHAR para imprimir dos caracteres / y un salto de línea \n. El servicio de impresión de un carácter también lo podemos convertir a una macro. En este caso el parámetro es ahora un carácter, por lo que lo llamamos %car. Así queda la macro definida:

.macro M_PRINT_CHAR(%car)
  li a0, %car
  li a7, PRINT_CHAR
  ecall
.end_macro

La metemos en el archivo servicios.s, que ahora queda así:

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv PRINT_INT_HEX 34
	.eqv PRINT_INT_BINARY 35
	.eqv SLEEP         32
	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

#-- Servicio para imprimir un numero entero
#-- Parametro: %num: Numero a imprimir
#-- NOTA: %num tiene que ser un NUMERO CONSTANTE
.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

#-- Servicio para imprimir un caracter
#-- Parametro: %car: Un carácter constante
.macro M_PRINT_CHAR(%car)
  li a0, %car
  li a7, PRINT_CHAR
  ecall
.end_macro

Usando la macro M_PRINT_CHAR

Así obtenemos la versión final del programa para imprimir la fecha. Queda mucho más reducido y compacto

#-- Imprimir una fecha

	.include "servicios.s"

	.text
	#-- Imprimir año
	M_PRINT_INT(2025)
	M_PRINT_CHAR('/')
	
	#-- Imprimir mes
	M_PRINT_INT(12)
	M_PRINT_CHAR('/')
	
	#-- Imprimir dia
	M_PRINT_INT(21)
	M_PRINT_CHAR('\n')
	
	#-- Terminar
	M_EXIT

¡Este programa es casi casi de alto nivel!

Sigue teniendo exactamente las mismas instrucciones que antes: 20

Expansión de las macros

En esta figura se muestra el programa fuente original y en la derecha el programa generado por el ensamblador al expandir las macros

Se comprueba gráficamente cómo el programa original es más corto y compacto que el final. Pero generan ambos generan el mismo código máquina. Por el hecho de meter las macros el programa no aumenta en tamaño. También tarda los mismos ciclos en ejecutarse, porque tiene las mismas instrucciones

Macros y constantes

El programa de la fecha todavía se puede hacer aún más legible y más fácil de mantener. Para imprimir los números enteros podemos usar constantes. Estas constantes se las pasamos a la macro como parámetros. Es decir, que podemos usar CONSTANTES como parámetros de las macros

En el ejemplo definimos las constantes ANNO, MES y DIA, y se las pasamos a la macro M_PRINT_INT

	.include "servicios.s"
	
	#-- Constantes con los datos de la fecha
	.eqv ANNO 2025
	.eqv MES  12
	.eqv DIA  21

	.text
	#-- Imprimir la fecha. Formato AÑO/MES/DIA
	M_PRINT_INT(ANNO)
	M_PRINT_CHAR('/')
	
	M_PRINT_INT(MES)
	M_PRINT_CHAR('/')
	
	M_PRINT_INT(DIA)
	M_PRINT_CHAR('\n')
	
	#-- Terminar
	M_EXIT

El resultado es el mismo, y por supuesto el programa ocupa las mismas instrucciones: 20

Ejemplo 4: Barra de progreso

En este ejemplo se simula una barra de progreso de 5 caracteres, que se actualiza cada 400ms:

#-- Barra de progreso

	.include "servicios.s"
	
	.text
	
	#-- Contador del progreso
	li t0, 5
	
next:
	#-- Comprobar si ha terminado el progreso
	beq t0, zero, fin
	
	#-- Progreso no terminado
	#-- Imprimir siguiente caracter
	M_PRINT_CHAR('*')
	
	#-- Decrementar progreso
	addi t0, t0, -1
	
	#-- Pausa
	li a0, 400
	li a7, SLEEP
	ecall
	
	#-- repetir
	j next
	
fin:
	#-- Terminar
	M_EXIT

Este es el resultado:

La pausa se realiza llamando al servicio SLEEP del sistema operativo, y se le pasa como parámetro el tiempo en milisegundos. Esta llamada es la que vamos a convertir en una macro, pasándole como parámetro la espera en milisegundos

La macro M_SLEEP

Para realizar la pausa en milisegundos se utiliza el servicio SLEEP del sistema operativo. Este servicio tiene un único parámetro: el tiempo en milisegundo. Por ello la macro es similar a las dos anteriores: M_PRINT_INT y M_PRINT_CHAR, pero ahora se usa el parámetro %ms

.macro M_SLEEP(%ms)
  li a0, %ms
  li a7, SLEEP
  ecall
.end_macro

Esta macro la incorporamos en el fichero servicios.s:

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv PRINT_INT_HEX 34
	.eqv PRINT_INT_BINARY 35
	.eqv SLEEP         32
	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

#-- Servicio para imprimir un numero entero
#-- Parametro: %num: Numero a imprimir
#-- NOTA: %num tiene que ser un NUMERO CONSTANTE
.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

#-- Servicio para imprimir un caracter
#-- Parametro: %car: Un carácter constante
.macro M_PRINT_CHAR(%car)
  li a0, %car
  li a7, PRINT_CHAR
  ecall
.end_macro

#-- Servicio para realizar una espera
#-- Parametro: %ms: Tiempo en milisegundos
.macro M_SLEEP(%ms)
  li a0, %ms
  li a7, SLEEP
  ecall
.end_macro

Usando la macro M_SLEEP

Así queda el ejemplo de la barra de progreso usando esta nueva macro:

#-- Barra de progreso

	.include "servicios.s"
	
	.text
	
	#-- Contador del progreso
	li t0, 5
	
next:
	#-- Comprobar si ha terminado el progreso
	beq t0, zero, fin
	
	#-- Progreso no terminado
	#-- Imprimir siguiente caracter
	M_PRINT_CHAR('*')
	
	#-- Decrementar progreso
	addi t0, t0, -1
	
	#-- Pausa
	M_SLEEP(400)
	
	#-- repetir
	j next
	
fin:
	#-- Terminar
	M_EXIT

Ejemplo 5: Impresión de mensajes

La impresión de mensajes en la consola se utiliza muchísimo. Por ello tiene todo el sentido usar también macros. En este ejemplo se imprimen 3 mensajes cada segundo:

	.include "servicios.s"
	
	.data
msg1:	.string "Mensaje 1...\n"
msg2:   .string "Mensaje 2...\n"
msg3:   .string "Fin"
	
	.text
	
	#-- Imprimir primer mensaje
	la a0, msg1
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar un segundo
	M_SLEEP(1000)
	
	#-- Imprimir segundo mensaje
	la a0, msg2
	li a7, PRINT_STRING
	ecall
	
	#-- Esperar un segundo
	M_SLEEP(1000)
	
	#-- Imprimir tercer mensaje
	la a0, msg3
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT

El funcionamiento del programa es el siguiente:

El programa tiene 20 instrucciones, y en el segmento de datos se han definido 3 etiquetas

La macro M_PRINT_STRING

Definimos la siguiente macro para el acceso al servicio PRINT_STRING:

.macro M_PRINT_STRING(%cad)
  la a0, %cad
  li a7, PRINT_STRING
  ecall
.end_macro

Y lo metemos en el fichero servicios.s:

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv SLEEP            32
	.eqv PRINT_INT_HEX    34
	.eqv PRINT_INT_BINARY 35

	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

#-- Servicio para imprimir un numero entero
#-- Parametro: %num: Numero a imprimir
#-- NOTA: %num tiene que ser un NUMERO CONSTANTE
.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

#-- Servicio para imprimir un caracter
#-- Parametro: %car: Un carácter constante
.macro M_PRINT_CHAR(%car)
  li a0, %car
  li a7, PRINT_CHAR
  ecall
.end_macro

#-- Servicio para realizar una espera
#-- Parametro: %ms: Tiempo en milisegundos
.macro M_SLEEP(%ms)
  li a0, %ms
  li a7, SLEEP
  ecall
.end_macro

#-- Servicio para imprimir una cadena
#-- Parametro: %cad: Puntero a la cadena
.macro M_PRINT_STRING(%cad)
  la a0, %cad
  li a7, PRINT_STRING
  ecall
.end_macro

Usando la macro M_PRINT_STRING

El ejemplo lo reescribimos ahora de esta manera

	.include "servicios.s"
	
	.data
msg1:	.string "Mensaje 1...\n"
msg2:   .string "Mensaje 2...\n"
msg3:   .string "Fin"

	
	.text
	
	#-- Imprimir primer mensaje
	M_PRINT_STRING(msg1)
	
	#-- Esperar un segundo
	M_SLEEP(1000)
	
	#-- Imprimir segundo mensaje
	M_PRINT_STRING(msg2)
	
	#-- Esperar un segundo
	M_SLEEP(1000)
	
	#-- Imprimir tercer mensaje
	M_PRINT_STRING(msg3)
	
	#-- Terminar
	M_EXIT

Con esta macro es más fácil realizar las impresiones de cadenas. Sin embargo, todavía se puede hacer aún más fácil, como veremos en las siguientes secciones

Macros y etiquetas

Dentro de las macros también podemos definir etiquetas. Sabemos que las etiquetas son ÚNICAS. No se pueden definir dos etiquetas con el mismo nombre. Esto generaría un error de ensamblado

Por ello, las etiquetas definidas dentro de la macro son especiales. Se definen de la misma manera: con un texto terminado con el carćter :. PERO cuando se expande la macro se añade el sufijo _M<i> a la etiqueta, donde <i> es el número de la macro, comenzando por 0.

Cada vez que se expande una macro, se le asigna un nombre empezando en 0. Así, la primera macro es la M0, la segunda la M1, la tercer M2, etc...

Etiquetas en el segmento de código

En este programa de ejemplo se define la macro TEST_LABEL que contiene la etiqueta label delante de la instrucción nop. Se colocan 3 macros para ver lo que ocurre

	.include "servicios.s"

.macro TEST_LABEL
label:
	nop
.end_macro

	.text
	
	TEST_LABEL  #-- Macro 0 (M0)
	TEST_LABEL  #-- Macro 1 (M1)
	TEST_LABEL  #-- Macro 2 (M2)

	#-- Terminar
	M_EXIT	

Si ahora ensamblamos y miramos la tabla de símbolos, comprobamos que se han definido 3 etiquetas diferentes, cuyos nombres son label_M0, label_M1 y label_M3. Se definen en ese orden. Las tres están en el segmento de código

Esas etiquetas definidas en la macro, son etiquetas normales. Es decir, que desde fuera de la macro las podríamos usar normalmente. Esto es algo que normalmente no se hace, pero es totalmente viable.

Este es el programa modificado en el que se hace un salto a la etiqueta label_M0 para comenzar a ejecutar la primera instrucción nop

	.include "servicios.s"

.macro TEST_LABEL
label:
	nop
.end_macro

	.text
	
	#-- Saltar a la etiqueta label de la
	#-- primera macro
	j label_M0
	
	TEST_LABEL  #-- Macro 0 (M0)
	TEST_LABEL  #-- Macro 1 (M1)
	TEST_LABEL  #-- Macro 2 (M2)
	
	#-- Terminar
	M_EXIT

Este programa ensambla correctamente y se ejecuta sin problemas

En esta figura se muestra el programa fuente original y el resultado de la expasión de las macros:

Etiquetas en el segmento de datos

Las macros también sirven para colocar información en el segmento de datos. Partimos de este programa de ejemplo en el que se definen 2 vectores en el segmento de datos. Cada vector está compuesto por 3 palabras, correspondientes a sus coordenadas x,y,z. Desde el programa principal simplemente leemos estas componentes y las metemos en registros:

	
	#-- Constantes para acceder a las coordenadas
	#-- x,y,z de los vectores
	.eqv X 0
	.eqv Y 4
	.eqv Z 8
	
	.data
vec0:	.word 1,2,3   #-- Primer vector
vec1:	.word 4,5,6   #-- Segundo vector
	
	.text
	
	#-- Leer componentes x,y,z del primer vector
	la s0, vec0
	lw t0, X(s0)
	lw t1, Y(s0)
	lw t2, Z(s0)
	
	#-- Leer componentes del segundo vector
	la s0, vec1
	lw t3, X(s0)
	lw t4, Y(s0)
	lw t5, Z(s0)
	
	#-- Terminar
	M_EXIT

Lo ensamblamos y lo ejecutamos. Observamos que los vectores están en el segmento de datos y que al ejecutarse sus componentes se han leido en los vectores t0, t1, t2, y t3, t4 y t5 respectivamente

Los vectores los podemos incluir en el segmento de datos de dos formas diferntes, que veremos en los siguientes apartados

Macros en el segmento de datos

Las macros se pueden colocar también en el segmento de datos, y puede utilizar las directivas del ensamblador. Para colocar un vector en el segmento de datos definimos una macro como esta:

.macro VEC(%x,%y,%z)
  vec:  .word %x, %y, %z
.end_macro

Tiene 3 parámetros: %x, %y y %z. Esta macro sitúa estos 3 números enteros en el segmento de datos, usando la directiva .word. Además, le asigna la etiqueta base vec, que luego en la expansión de la macro se convierte en vec_M0 y vec_M1

Así queda nuestro programa. Observa que la macro VEC() se ha puesto DENTRO DEL SEGMENTO DE DATOS

	.include "servicios.s"	
	
	#-- Constantes para acceder a las coordenadas
	#-- x,y,z de los vectores
	.eqv X 0
	.eqv Y 4
	.eqv Z 8
	
#-- Insertar un vector en el segmento de datos
.macro VEC(%x,%y,%z)
  vec:  .word %x, %y, %z
.end_macro
	
	.data
	VEC(1,2,3)    #-- Primer vector
	VEC(4,5,6)   #-- Segundo vector
	
	.text
	
	#-- Leer componentes x,y,z del primer vector
	la s0, vec_M0
	lw t0, X(s0)
	lw t1, Y(s0)
	lw t2, Z(s0)
	
	#-- Leer componentes del segundo vector
	la s0, vec_M1
	lw t3, X(s0)
	lw t4, Y(s0)
	lw t5, Z(s0)
	
	#-- Terminar
	M_EXIT

El resultado al ensamblarlo y ejecutarlo es el mismo que el programa anterior, PERO las etiquetas que se generan en la expansión de las macros son vec_M0 y vec_M1. En el programa original usábamos vec0 y vec1

Macros en el segmento de código

Sin embargo, el programa se puede simplificar aún más si incluimos las macros directamente en el segmento de código. Dentro de la macro ahora especificamos que lo que viene a continuación se debe situar en el segmento de datos, por lo que se coloca la directiva .data. Tras la colocación del vector se coloca la directiva .text para que las instrucciones que están tras la macro se coloquen en el segmento de código

Así queda la nueva macro:

.macro VEC(%x,%y,%z)
        .data
  vec:  .word %x, %y, %z
  	.text
.end_macro

Y este es el nuevo programa:

	.include "servicios.s"	
	
	#-- Constantes para acceder a las coordenadas
	#-- x,y,z de los vectores
	.eqv X 0
	.eqv Y 4
	.eqv Z 8
	
#-- Insertar un vector en el segmento de datos
.macro VEC(%x,%y,%z)
        .data
  vec:  .word %x, %y, %z
  	.text
.end_macro
	
	.text
	
	#-- Crear los vectores (en el segmento de datos)
	VEC(1,2,3)    #-- Primer vector
	VEC(4,5,6)    #-- Segundo vector
	
	#-- Leer componentes x,y,z del primer vector
	la s0, vec_M0
	lw t0, X(s0)
	lw t1, Y(s0)
	lw t2, Z(s0)
	
	#-- Leer componentes del segundo vector
	la s0, vec_M1
	lw t3, X(s0)
	lw t4, Y(s0)
	lw t5, Z(s0)
	
	#-- Terminar
	M_EXIT

Este programa es exactamente igual al anterior. Tiene las mismas instrucciones y se almacenan en el segmento de datos los mismos vectores. La ejecución produce los mismos resultados

La diferencia está en que este programa es más parecido a los de alto nivel. Dentro del propio código se definen dos vectores con las macros, y el ensamblador los sitúa directamente en el segmento de datos

En la siguiente sección veremos un ejemplo extremadamente útil de esta idea de colocar datos desde el segmento de código: una macro para imprimir cadenas constantes

La macro PRINT

La gran ventaja de las macros es que nos permite simplificar mucho la programación en ensamblador. Y en muchos casos logramos que se parezca bastante a la programación en alto nivel

¿Recuerdas el programa hola mundo que vimos en la primera sesión del laboratorio? En su momento era un programa críptico y complicado de entender. Este programa lo fuimos desgranando poco a poco, en las sucesivas sesiones, para comprender todos los conceptos implicados

Ahora, gracias a las macros, lo podemos simplificar muchísimo más. Este es el resultado:

#-- Programa "Hola mundo"
#-- Super simplificado gracias a las MACROS

	.include "servicios.s"
			
	.text
	
	PRINT("Hola mundo!")
	
	#-- Terminar
	M_EXIT

Al ensamblarlo y ejecutarlo, esto es lo que obtenemos

La magia de la nueva macro PRINT está en que sitúa automáticamente la cadena "Hola Mundo!" en el segmento de datos y llama al servicio PRINT_STRING pasando la dirección de esa cadena. Todo esto se hace de forma transparente. Así es como está definida la macro:

.macro PRINT(%str)
        .data
   msg: .string %str
   
   	.text
   	la a0, msg
   	li a7, PRINT_STRING
   	ecall
.end_macro	

La incluimos en el fichero servicios.s, que queda así:

#-- Código de los servicios del sistema operativo

	.eqv PRINT_INT    1
	.eqv READ_INT     5
	.eqv PRINT_STRING 4
	.eqv READ_STRING  8
	.eqv PRINT_CHAR   11
	.eqv READ_CHAR    12
	.eqv EXIT         10
	.eqv SLEEP            32
	.eqv PRINT_INT_HEX    34
	.eqv PRINT_INT_BINARY 35

	
#--- Macros para acceder a los servicios
#--- del sistema operativo

#-- Servicio para terminar
.macro M_EXIT
    li a7, EXIT
    ecall
.end_macro
	
#-- Servicio para leer caracter
.macro M_READ_CHAR
  li a7, READ_CHAR
  ecall
.end_macro	

#-- Servicio para imprimir un numero entero
#-- Parametro: %num: Numero a imprimir
#-- NOTA: %num tiene que ser un NUMERO CONSTANTE
.macro M_PRINT_INT(%num)
  li a0, %num
  li a7, PRINT_INT
  ecall
.end_macro

#-- Servicio para imprimir un caracter
#-- Parametro: %car: Un carácter constante
.macro M_PRINT_CHAR(%car)
  li a0, %car
  li a7, PRINT_CHAR
  ecall
.end_macro

#-- Servicio para realizar una espera
#-- Parametro: %ms: Tiempo en milisegundos
.macro M_SLEEP(%ms)
  li a0, %ms
  li a7, SLEEP
  ecall
.end_macro

#-- Servicio para imprimir una cadena
#-- Parametro: %cad: Puntero a la cadena
.macro M_PRINT_STRING(%cad)
  la a0, %cad
  li a7, PRINT_STRING
  ecall
.end_macro

#-- Servicio para imprimir una cadena
#-- Parametro: %str: Cadena constante (Ej. "Hola")
.macro PRINT(%str)
        .data
   msg: .string %str
   
   	.text
   	la a0, msg
   	li a7, PRINT_STRING
   	ecall
.end_macro	

La macro mete la cadena constante pasada como parámetro (%str) en el segmento de datos, y le asocia la etiqueta msg. Esta etiqueta sabemos que se expande como msg_M0. Pero dentro de la macro nos da igual. Referenciamos la cadena como msg

A continuación, en el segmento de datos, se generan las instrucciones necesarias para invocar el servicio PRINT_STRING del sistema operativo, pasando a través de a0 la dirección de la cadena, que está en la etiqueta msg

En esta figura se muestra la expansión de las macros y cómo este hola mundo super-simplificado se convierte en el hola mundo real

Macros dentro de macros

Cuando se define una macro se puede invocar a cualquiera de las macros que hayan sido creadas anteriormente. Esto nos permite reutilizar código

En este ejemplo se hace el cálculo de 5*4 y se guarda el resultado en a0. Para realizarlo se usa la macro MULTX4() que suma cuatro veces el registro a0 y el resultado lo devuelve en a0. PERO esta macro está, a su vez, construida mediante llamadas a la macro MULTX2() que multiplica por 2 el registro a0

Así es como queda el programa. Es importante el orden en el que se definen las macros. Primero hay que crear MULTX2() y luego MULTX4() que es la que usa `MULTX2():

	.include "servicios.s"
	
#-- Multiplicar por dos un registro
.macro MULTX2(%reg)
  add %reg, %reg, %reg
.end_macro	
	
#-- Multiplicar por 4 un registro
.macro MULTX4(%reg)
  MULTX2(%reg)
  MULTX2(%reg)
 .end_macro	
	
	.text
	
	#-- Calcular 5 x 4
	li a0, 5
	MULTX4(a0)
	
	#-- Terminar
	M_EXIT

Lo ensamblamos y comprobamos que el programa funciona. En el registro a0 tenemos el valor 20

La expansión de las macros se hace en 2 fases. En la primera se expande MULTX4() (y M_EXIT), y en la segunda MULTX2():

Macros y funciones

A primera vista las macros parecen similares a las funciones, ¡PERO NO LO SON!. De hecho, otro de los usos de las macros es simplificar la llamada a las funciones, como veremos en la siguiente sección

Llamadas a funciones con macros

Lo vemos con un ejemplos. Definimos la función strcp(dst, src) que copia la cadena fuente en la destino. El programa principal copia la cadena "HOLI" en la cadena destino, y la imprime:

	.include "servicios.s"
	
	#-- Longitud maxima de la cadena destino
	.eqv MAX 20
	
	.data
src:	.string "HOLI"
dst:	.space 20	
	
	.text
	
	#-- Copiar cadena
	#-- strcp(dst, src)
	la a0, dst
	la a1, src
	jal strcp
	
	#-- Imprimir la cadena resultante
	la a0, dst
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT
	
#-------------------------------
#-- strcp(dst, src)
#--   Copiar cadena src en dst
#--
#-- ENTRADAS:
#--   a0: Direccion cadena destino
#--   a1: Direccion cadena fuente
#-------------------------------
strcp:
	
next:
	#-- Leer caracter fuente
	lb t1, 0(a1)
	
	#-- ¿Fin cadena?
	beq t1, zero, fin
	
	#-- Copiar caracter en destino
	sb t1, 0(a0)
	
	#-- Incrementar los punteros de las cadenas
	addi a0, a0, 1
	addi a1, a1, 1
	
	#-- Repetir
	j next


fin: 
	#-- Copiar un zero en la cadena destino
	sb zero, 0(a0)
	ret

Este es el resultado al ejecutarlo:

Siempre que queramos copiar una cadena a partir de su etiqueta hay que realizar una llamada a la funcion strcp(dst, src), que al escribirlo en ensamblador tiene esta pinta:

  la a0, dst
  la a1, src
  jal strcp

Podemos definir la macro STRCP() para sustituir esas tres instrucciones por esto:

  STRCP(dst, src)

que es mucho más legible y cercano al alto nivel

Así es como queda la macro definida:

.macro STRCP(%dst, %src)
  la a0, %dst
  la a1, %src
  jal strcp
.end_macro

Y así es como queda ahora el programa principal. Mucho más claro:

	.text
	
	#-- Copiar cadena
	STRCP(dst, src)
	
	#-- Imprimir la cadena resultante
	la a0, dst
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT

Este es el ejemplo completo para probarlo directamente. Es exactamente igual que el original: tiene las mismas instrucciones y tarda el mismo tiempo:

	.include "servicios.s"
	
	#-- Longitud maxima de la cadena destino
	.eqv MAX 20
	
.macro STRCP(%dst, %src)
  la a0, %dst
  la a1, %src
  jal strcp
.end_macro	
	
	.data
src:	.string "HOLI"
dst:	.space 20	
	
	.text
	
	#-- Copiar cadena
	STRCP(dst, src)
	
	#-- Imprimir la cadena resultante
	la a0, dst
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar
	M_EXIT
	
#-------------------------------
#-- strcp(dst, src)
#--   Copiar cadena src en dst
#--
#-- ENTRADAS:
#--   a0: Direccion cadena destino
#--   a1: Direccion cadena fuente
#-------------------------------
strcp:
	
next:
	#-- Leer caracter fuente
	lb t1, 0(a1)
	
	#-- ¿Fin cadena?
	beq t1, zero, fin
	
	#-- Copiar caracter en destino
	sb t1, 0(a0)
	
	#-- Incrementar los punteros de las cadenas
	addi a0, a0, 1
	addi a1, a1, 1
	
	#-- Repetir
	j next


fin: 
	#-- Copiar un zero en la cadena destino
	sb zero, 0(a0)
	ret

Macro vs funciones

Las macros y funciones parecen cosas similares, pero NO lo son. La principal diferencia es que las macros se expanden. Es como si cada vez que aparece la macro se hace un copy & paste del código. Por ello, si definimos una macro muy grande, cada vez que la usamos se incluyen muchas instrucciones. El programa puede crecer mucho en tamaño sin darnos cuenta

Las funciones son el elemento clave para abordar la complejidad de los proyectos. Nos permiten dividir algo complejo en trocitos más pequeños. Además, el código se reutiliza y es más fácil de mantener

Las macros nos permite escribir menos líneas y ahorrar tiempo de desarrollo, pero nunca hay que usarlas como sustituto de las funciones. En la sección anterior hemos visto un ejemplo muy claro de cómo una macro nos facilita la llamada a una función

Las macros típicamente se usan para compactar 2 ó 3 líneas de ensamblador. Si tienen más instrucciones entonces tal vez no sea una buena macro

Acceso al Sistema operativo mediante macros: so.h

Para aprender el uso de las macros hemos definidos macros que nos permiten llamar a los servicios del sistema operativo. Hemos partido del fichero servicios.s que contiene los códigos de los servicios en constantes y lo hemos ampliado con macros. Las diferenciamos mediante el prefijo M_

En esta sección vamos a definir el acceso al sistema operativo desde 0, utilizando constantes y macros. Estas nuevas definiciones están disponibles en el fichero so.h. Así podremos utilizar en nuestros programas el antiguo servicios.s o el nuevo so.h según nos convenga

También se cambia la extensión del fichero de .s a .h. De esta forma podemos diferenciar los ficheros que contienen ensamblador de los que sólo tienen constantes y macros. Es la misma nomenclatura utilizada que en el lenguaje C: Se utiliza .h para especificar los archivos que se pueden incluir, y que contiene definiciones

Código de los servicios del sistema operativo

El acceso al sistema operativo lo haremos principalmente con macros, por lo que utilizaremos los nombres de los servicios para referirnos a ellas. Así, EXIT se refiere ahora a la macro que invoca el servicio exit, y NO al código de ese servicio

Los códigos de los servicios los definimos en constantes que comienzan con el prefijo COD_

Así es cómo quedan definidos los servicios que hemos usado hasta ahora:

# ===============================================
# Codigo de los servicios del sistema operativo
# ===============================================
    .eqv COD_PRINT_INT    1
    .eqv COD_READ_INT     5
    .eqv COD_PRINT_STRING 4
    .eqv COD_READ_STRING  8
    .eqv COD_PRINT_CHAR   11
    .eqv COD_READ_CHAR    12
    .eqv COD_EXIT         10
    .eqv COD_SLEEP            32
    .eqv COD_PRINT_INT_HEX    34
    .eqv COD_PRINT_INT_BINARY 35

Macros de acceso directo a los servicios del sistema operativo

Para acceder a cualquier servicio del sistema operativo siempre se utilizan estas dos instrucciones:

  li a7, <codigo>
  ecall

Para cada servicio se define una macro con el nombre del servicio. El resto de parámetro se deben introducir por los registros correspondientes indicados en la documentación del sistema operativo. A estas macros las llamamos macros de acceso directo. Son las siguientes:

# -------------------------------------------
# - Macros de acceso DIRECTO a los servicios 
# -------------------------------------------
.macro EXIT
  li a7, COD_EXIT
  ecall
.end_macro

.macro PRINT_INT
  li a7, COD_PRINT_INT
  ecall
.end_macro

.macro PRINT_CHAR
  li a7, COD_PRINT_CHAR
  ecall
.end_macro

.macro PRINT_STRING
  li a7, COD_PRINT_STRING
  ecall
.end_macro

.macro PRINT_INT_HEX
  li a7, COD_PRINT_INT_HEX
  ecall
.end_macro

.macro PRINT_INT_BIN
  li a7, COD_PRINT_INT_BIN
  ecall
.end_macro

.macro READ_INT
  li a7, COD_READ_INT
  ecall
.end_macro

.macro READ_STRING
  li a7, COD_READ_STRING
  ecall
.end_macro

.macro READ_CHAR
  li a7, COD_READ_CHAR
  ecall
.end_macro

.macro SLEEP
  li a7, COD_SLEEP
  ecall
.end_macro

En este programa se usan algunos servicios como ejemplo. Lo importante es ver que con estas nuevas macros definidas podemos acceder a ellos exactamente igual que como lo hacíamos directamente con los códigos

	.include "so.h"
	
	.data
msg:	.string "Mensaje de prueba\n"
	
	.text
	
	#-- Imprimir cadena
	la a0, msg
	PRINT_STRING
	
	#-- Imprimir numero entero
	li a0, 200
	PRINT_INT
	
	#-- Imprimir caracter
	li a0, '\n'
	PRINT_CHAR
	
	#-- Imprimir numero hexa
	li a0, 0xFF
	PRINT_INT_HEX
	
	#-- Terminar
	EXIT

El resultado es el esperado:

Macros específicas

Para hacer más sencillo todavía el acceso a los servicios de sistema operativo vamos a crear nuevas macros que usan las macros de acceso directo. En determinados servicios que hay que pasar un parámetro, vamos a diferencias si lo que se pasa es un dato inmediato, un dato a través de registro o una referencia a través de una etiqueta. Utilizaremos los sufijos I, R y L respectivamente

PRINT_CHAR

Servicio de impresión de un carácter

Macro Parámetro Descripcion
PRINT_CHAR a0: Caracter a imprimir Acceso inmediato
PRINT_CHARI(car) car: Caracter inmediato Ej: PRINT_CHAR('A')
PRINT_CHARR(reg) reg: Registro Ej: PRINT_CHAR(t0)

La diferencia entre PRINT_CHARI y PRINT_CHARR es la instrucción usada para depositar el valor en el registro a0. En la primera macro es li y en la segunda mv

Estas son las definiciones de las macros:

.macro PRINT_CHARI(%car)
   li a0, %car
   PRINT_CHAR
.end_macro

.macro PRINT_CHARR(%reg)
   mv a0, %reg
   PRINT_CHAR
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	li a0, 'H'
	PRINT_CHAR
	
	#-- Imprimir caracter inmediato
	PRINT_CHARI('\n')
	
	#-- Imprimir Caracter en registro
	li t0, 'I'
	PRINT_CHARR(t0)
	
	#-- Terminar
	EXIT
  • Resultado:

PRINT_INT

Estas son todas las macros para acceder al servicio de impresión de números enteros

Macro Parámetro Descripcion
PRINT_INT a0: Numero a imprimir Acceso inmediato
PRINT_INTI(num) num: Numero inmediato Ej: PRINT_INTI(20)
PRINT_INTR(reg) reg: Registro Ej: PRINT_INTR(t0)

La diferencia entre PRINT_INTI y PRINT_INTR es la instrucción usada para depositar el valor en el registro a0. En la primera macro es li y en la segunda mv

Estas son las definiciones de las macros:

#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INTI(%cte)
  li a0, %cte
  PRINT_INT
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INTR(%reg)
  mv a0, %reg
  PRINT_INT
.end_macro

Este es un un programa de ejemplo para hacer las pruebas

	.include "so.h"
	
	.text
	
	#-- Acceso directo
	li a0, 1
	PRINT_INT
	
	#-- Imprimir entero inmediato
	PRINT_INTI(2)
	
	#-- Imprimir entero en registro
	li t0, 3
	PRINT_INTR(t0)
	
	#-- Terminar
	EXIT
  • Resultado:

PRINT_INT_HEX

Impresión de un número entero en hexadecimal

Macro Parámetro Descripcion
PRINT_INT_HEX a0: Numero a imprimir Acceso inmediato
PRINT_INT_HEXI(num) num: Numero inmediato Ej: PRINT_INT_HEX(20)
PRINT_INT_HEXR(reg) reg: Registro Ej: PRINT_INT_HEX(t0)
  • Definiciones de las macros:
#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INT_HEXI(%cte)
  li a0, %cte
  PRINT_INT_HEX
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INT_HEXR(%reg)
  mv a0, %reg
  PRINT_INT_HEX
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	li a0, 0xFF
	PRINT_INT_HEX
	PRINT_CHARI('\n')
	
	#-- Imprimir entero inmediato
	PRINT_INT_HEXI(1)
	PRINT_CHARI('\n')
	
	#-- Imprimir entero en registro
	li t1, 0x03
	PRINT_INT_HEXR(t1)
	
	#-- Terminar
	EXIT
  • Resultado:

PRINT_INT_BIN

Impresión de un número entero en binario

Macro Parámetro Descripcion
PRINT_INT_BIN a0: Numero a imprimir Acceso inmediato
PRINT_INT_BINI(num) num: Numero inmediato Ej: PRINT_INT_BIN(20)
PRINT_INT_BINR(reg) reg: Registro Ej: PRINT_INT_BIN(t0)
  • Definiciones de las macros:
#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INT_BINI(%cte)
  li a0, %cte
  PRINT_INT_BIN
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INT_BINR(%reg)
  mv a0, %reg
  PRINT_INT_BIN
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	li a0, 0xFF
	PRINT_INT_BIN
	PRINT_CHARI('\n')
	
	#-- Imprimir entero inmediato
	PRINT_INT_BINI(1)
	PRINT_CHARI('\n')
	
	#-- Imprimir entero en registro
	li t1, 0x03
	PRINT_INT_BINR(t1)
	
	#-- Terminar
	EXIT
  • Resultado:

PRINT_STRING

Impresión de una cadena

Macro Parámetro Descripcion
PRINT_STRING a0: Dirección de la cadena Acceso inmediato
PRINT_STRINGI(str) str: Cadena inmediata Ej: PRINT_STRINGI("Hola")
PRINT_STRINGL(label) label: Etiqueta con cadena Ej: PRINT_STRINGL(msg1)
PRINT_STRINGR(reg) reg: Registro con la dirección Ej: PRINT_STRINGR(t0)
  • Definiciones de las macros:
.macro PRINT_STRINGI(%str)
  .data
  msg:  .string %str
  .text
  la a0, msg
  PRINT_STRING
.end_macro

.macro PRINT_STRINGL(%label)
  la a0, %label
  PRINT_STRING
.end_macro

.macro PRINT_STRINGR(%reg)
  mv a0, %reg
  PRINT_STRING
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.data
msg1:	.string "MSG1\n"
msg3:   .string "MSG3\n"
msg4:   .string "MSG4\n"
	
	.text
	
	#-- Acceso directo
	la a0, msg1
	PRINT_STRING
	
	#-- Imprimir cadena constante
	PRINT_STRINGI("MSG2\n")
	
	#-- Imprimir cadena dada por etiqueta
	PRINT_STRINGL(msg3)
	
	#-- Imprimir cadena dada por registro
	la t2, msg4
	PRINT_STRINGR(t2)
	
	#-- Terminar
	EXIT
  • Resultado:

READ_INT

Lectura de un número entero introducido por el usuario. NO HAY MACROS específicas

Macro Parámetro Descripcion
READ_INT Acceso inmediato
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	PRINT_STRINGI("Numero? ")
	READ_INT
	PRINT_INT
	
	#-- Terminar
	EXIT
  • Resultado:

READ_CHAR

Lectura de un carácter introducido por el usuario. NO HAY MACROS específicas

Macro Parámetro Descripcion
READ_CHAR Acceso inmediato
  • Definiciones de las macros:
.macro READ_CHAR
  li a7, COD_READ_CHAR
  ecall
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	PRINT_STRINGI("Caracter? ")
	READ_CHAR
	
	#-- Imprimir caracter
	mv t0, a0
	PRINT_CHARI('\n')
	PRINT_CHARR(t0)
	
	#-- Terminar
	EXIT
  • Resultado:

READ_STRING

Leer una cadena introducida por el usuario

Macro Parámetro Descripcion
READ_STRING a0: Dirección de la cadena. a1: Tamaño máximo cadena Acceso inmediato
READ_STRINGL(label,max) label: Etiqueta para cadena. max: Tamaño máximo cadena Ej: READ_STRING(cad, 40)
READ_STRINGR(reg,max) reg: Direccion cadena. max: Tamaño máximo cadena Ej: READ_STRING(t0, 50)
  • Definiciones de las macros:
.macro READ_STRINGL(%label, %max)
  la a0, %label
  li a1, %max
  READ_STRING
.end_macro

.macro READ_STRINGR(%reg, %max)
  mv a0, %reg
  li a1, %max
  READ_STRING
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.eqv MAX 20
	
	.data
cad:	.space MAX
	
	.text
	
	#-- Acceso directo
	PRINT_STRINGI("Cadena 1? ")
	la a0, cad
	li a1, MAX
	READ_STRING
	PRINT_STRING
	
	#-- Cadena dada por etiqueta
	PRINT_STRINGI("Cadena 2? ")
	READ_STRINGL(cad, MAX)
	PRINT_STRING
	
	#-- Cadena nada en registro
	PRINT_STRINGI("Cadena 3? ")
	la t3, cad
	READ_STRINGR(t3, MAX)
	PRINT_STRING
	
	#-- Terminar
	EXIT
  • Resultado:

SLEEP

Realizar una pausa en milisegundos

Macro Parámetro Descripcion
SLEEP a0: Tiempo en milisegundos Acceso inmediato
SLEEPI(delay) delay: Tiempo en milisegundos Ej: SLEEPI(1000)
SLEEPR(reg) reg: Registro con el tiempo en milisegundos Ej: SLEEPR(t0)
  • Definiciones de las macros:
.macro SLEEPI(%cte)
  li a0, %cte
  SLEEP
.end_macro

.macro SLEEPR(%reg)
  mv a0, %reg
  SLEEP
.end_macro
  • Programa de ejemplo:
	.include "so.h"
	
	.text
	
	#-- Acceso directo
	PRINT_CHARI('*')
	li a0, 1000
	SLEEP
	
	#-- Pausa con numero inmediato
	PRINT_CHARI('*')
	SLEEPI(1000)
	
	#-- Pausa tiempo en registro
	PRINT_CHARI('*')
	li t4, 1000
	SLEEPR(t4)
	
	#-- Terminar
	EXIT
  • Resultado:

Fichero so.h completo

# ===============================================
# Codigo de los servicios del sistema operativo
# ===============================================
    .eqv COD_PRINT_INT    1
    .eqv COD_READ_INT     5
    .eqv COD_PRINT_STRING 4
    .eqv COD_READ_STRING  8
    .eqv COD_PRINT_CHAR   11
    .eqv COD_READ_CHAR    12
    .eqv COD_EXIT         10
    .eqv COD_SLEEP         32
    .eqv COD_PRINT_INT_HEX 34
    .eqv COD_PRINT_INT_BIN 35


# -------------------------------------------
# - Macros de acceso DIRECTO a los servicios 
# -------------------------------------------
.macro EXIT
  li a7, COD_EXIT
  ecall
.end_macro

.macro PRINT_INT
  li a7, COD_PRINT_INT
  ecall
.end_macro

.macro PRINT_CHAR
  li a7, COD_PRINT_CHAR
  ecall
.end_macro

.macro PRINT_STRING
  li a7, COD_PRINT_STRING
  ecall
.end_macro

.macro PRINT_INT_HEX
  li a7, COD_PRINT_INT_HEX
  ecall
.end_macro

.macro PRINT_INT_BIN
  li a7, COD_PRINT_INT_BIN
  ecall
.end_macro

.macro READ_INT
  li a7, COD_READ_INT
  ecall
.end_macro

.macro READ_STRING
  li a7, COD_READ_STRING
  ecall
.end_macro

.macro READ_CHAR
  li a7, COD_READ_CHAR
  ecall
.end_macro

.macro SLEEP
  li a7, COD_SLEEP
  ecall
.end_macro

# -------------------------------------------
# -- Macros ESPECIFICAS de los servicios
# -------------------------------------------

#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INTI(%cte)
  li a0, %cte
  PRINT_INT
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INTR(%reg)
  mv a0, %reg
  PRINT_INT
.end_macro

#-------------- PRINT_STRING

.macro PRINT_STRINGI(%str)
  .data
  msg:  .string %str
  .text
  la a0, msg
  PRINT_STRING
.end_macro

.macro PRINT_STRINGL(%label)
  la a0, %label
  PRINT_STRING
.end_macro

.macro PRINT_STRINGR(%reg)
  mv a0, %reg
  PRINT_STRING
.end_macro

#---------------- READ_STRING
.macro READ_STRINGL(%label, %max)
  la a0, %label
  li a1, %max
  READ_STRING
.end_macro

.macro READ_STRINGR(%reg, %max)
  mv a0, %reg
  li a1, %max
  READ_STRING
.end_macro

#------------------ PRINT_CHAR

.macro PRINT_CHARI(%car)
   li a0, %car
   PRINT_CHAR
.end_macro

.macro PRINT_CHARR(%reg)
   mv a0, %reg
   PRINT_CHAR
.end_macro

.macro SLEEPI(%cte)
  li a0, %cte
  SLEEP
.end_macro

.macro SLEEPR(%reg)
  mv a0, %reg
  SLEEP
.end_macro
#------------------- PRINT_INT_HEX

#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INT_HEXI(%cte)
  li a0, %cte
  PRINT_INT_HEX
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INT_HEXR(%reg)
  mv a0, %reg
  PRINT_INT_HEX
.end_macro


#------------------- PRINT_INT_BINARY

#-- Impresión de un entero INMEDIATO (constante)
.macro PRINT_INT_BINI(%cte)
  li a0, %cte
  PRINT_INT_BIN
.end_macro

#-- Impresion de un entero en un registro
.macro PRINT_INT_BINR(%reg)
  mv a0, %reg
  PRINT_INT_BIN
.end_macro

Conclusiones

Gracias a las macros, nuestros programas en ensamblador son ahora un poco más claros y concisos. Y también más legibles. Cada vez que haya patrones de código que se repiten, utilizamos una macro, como por ejemplo las llamadas a funciones a las que hay que pasar parámetros o la invocación de servicios del sistema operativo

A partir de ahora, en las sessiones siguientes, siempre utilizaremos el fichero so.h para acceder al sistema operativo, donde se definen macros directas y específicas para utilizar los servicios más frecuentes

Ejercicios para practicar

🚧 TODO 🚧

Profesorado

Licencia

Enlaces

Página principal


Sesiones de Prácticas

P1: Simulador RARs

L1: El simulador RARs
L2: Calculando expresiones
L3: Práctica 1-3. Variables

P2: E/S mapeada. Llamadas al sistema

L4: Pract 2-1. E/S mapeada
L5: Práctica 2-2: Inst. ecall
L6: Prác 2-3: Cadenas

P3: Bucles y Saltos condicionales

L7: Práct 3-1: Bucles y saltos
L8: Práct 3-2: Cadenas II

P4: Subrutinas

L9: Pract 4-1: Subrut. Nivel-1
L10: Pract 4-2: La pila
L11: Pract 4-3: Recursividad

P5: Memoria Dinámica

L12: Pract 5-1. Heap. Listas

VÍDEO DE DESPEDIDA

SESIONES ADICIONALES

L13: RV32I: Resto de instrucciones
L14: Macros
L15: Entrada/Salida a bajo nivel
L16: Multiplicaciones/divisiones. Unit Tests 🚧
L17: Biblioteca stdio: salida a alto nivel 🚧
Los registros de estado y control (CSR)
Interrupciones y excepciones
Borrador

SOLUCIONES

Soluciones a los ejercicios planteados en cada sesión para practicar:
Sesión L1
Sesión L2
Sesión L3
Sesión L4
Sesión L5
Sesión L6
Sesión L7
Sesión L8
Sesión L9
Sesión L10
Sesión L11
Sesión L12

Ejercicios de examen

Simulacro examen 1
GISAM. Ordinario. 2019-Dic-11
GISAM. Extra. 2020-Jul-03
GISAM. Ordinario. 2021-Ene-21
GISAM. Ordinario. 2022-Ene-10
GISAM. Extra. 2022-Jun-29
GISAM. Parcial 1. 2022-Oct-26
GISAM. Parcial 2. 2022-Nov-30
GISAM. Parcial 3. 2022-Dic-21
GISAM. Parcial 1. 2023-Oct-09
GISAM. Parcial 2. 2023-Nov-11
GISAM. Parcial 3. 2023-Dic-20
GISAM. Extra. 2024-Jun-17
GISAM. Parcial 1. 2024-Oct-14
GISAM. Parcial 2. 2024-Nov-13
GISAM. Parcial 3. 2024-Dic-16
GISAM. Extra. 2025-Jun-17
GISAM. Parcial 1. 2025-Oct-20
GISAM. Parcial 2. 2025-Nov-19
GISAM. Parcial 3. 2026-Enero-9
TELECO. Ordinario. 2019-Dic-13
TELECO. Extra. 2020-Jul-07
TELECO. Ordinario. 2021-Ene-21
TELECO. Extra. 2021-Jul-02
TELECO. Ordinario. 2022-Ene-10
TELECO. Extra. 2022-Jun-29
TELECO. Ordinario. 2023-Ene-10
TELECO. Extra. 2023-Jun-29
TELECO. Parcial 1. 2023-Oct-20
TELECO. Parcial 2. 2023-Nov-17
TELECO. Parcial 3. 2023-Dic-22
TELECO. Extra. 2024-Jun-17
TELECO. Parcial 1. 2024-Oct-10
TELECO. Parcial 2. 2024-Nov-21
TELECO. Parcial 3. 2024-Dic-19
TELECO. Extra. 2025-Jun-17
TELECO. Parcial 1. 2025-Oct-10
TELECO. Parcial 2. 2025-Nov-13
TELECO. Parcial 3. 2026-Ene-21
TELECO. Extra. 2026-Jun-25
Robótica. Ordinario. 2020-Jun-1
Robótica. Extra. 2020-Jul-13
Robótica. Ordinario. 2021-Mayo-20
Robótica. Extra. 2021-Junio-16
Robótica. Parcial 1. 2022-Feb-25
Robótica. Parcial 2. 2022-Abril-1
Robótica. Parcial 3. 2022-Mayo-6
Robótica. Parcial 1. 2023-Feb-27
Robótica. Parcial 2. 2023-Mar-27
Robótica. Parcial 3. 2023-May-08
Robótica. Parcial 1. 2024-Feb-26
Robótica. Parcial 2. 2024-Mar-20
Robótica. Parcial 3. 2024-May-06
Robótica. Extra. 2024-Junio-24
Robótica. Parcial 1. 2025-Feb-25
Robótica. Parcial 2. 2025-Mar-31
Robótica. Parcial 3. 2025-May-19
Robótica. Parcial 1. 2026-Feb-23
Robótica. Parcial 2. 2026-Abr-13
Robótica. Parcial 3. 2026-May-11
Datos. Parcial 1. 2023-Oct-09
Datos. Parcial 2. 2023-Nov-15
Datos. Parcial 3. 2023-Dic-20
Datos. Parcial 1. 2024-Oct-09
Datos. Parcial 2. 2024-Nov-13
Datos. Parcial 3. 2025-Ene-17
Datos. Parcial 1. 2025-Oct-15
Datos. Parcial 2. 2025-Nov-19

Clone this wiki locally