L5: Practica 2 - myTeachingURJC/2019-20-LAB-AO GitHub Wiki

Sesión Laboratorio 5: Práctica 2-2

  • Tiempo: 2h
  • Objetivos de la sesión:
    • Aprender cómo realizar llamadas al sistema
    • Conocer los nombres ABI de los registros
    • Saber usar los registros temporales tx y los de argumento ax
    • Manejo de las llamadas al sistema PrintInt, ReadInt y PrintChar

Vídeos

  • Fecha: 2019/Nov/07

Vídeo 1/3: Registros de la ABI RISC-V

Haz click en la imagen para ver el vídeo en Youtube

Click to see the youtube video

Vídeo 2/3: Instrucción ECALL

Haz click en la imagen para ver el vídeo en Youtube

Click to see the youtube video

Vídeo 3/3: Llamadas al sistema PrintInt, ReadInt y PrintChar

Haz click en la imagen para ver el vídeo en Youtube

Click to see the youtube video

Contenido

Introducción

La Entrada/salida se realiza mediante el mecanismo de dispositivos mapeados en memoria. Sin embargo los sistemas operativos incorporan servicios para gestionar la entrada/salidas y facilitar su acceso desde nuestros programas. Aprenderemos cómo invocar estos servicios. Es lo que denominamos llamadas al sistema

La Interfaz binaria de aplicaciones (ABI) de RISC-V

Nuestros programas no se ejecutan de forma aislada, sino que interactúan con el sistema operativo y las bibliotecas del sistema, que son programas diseñados por otras personas

Para que dos programas (módulos) puedan interactuar entre sí, se definen una ABI: Interfaz binaria de aplicaciones. En esta interfaz se determina cómo deben intercambiar información estas aplicaciones para que sean compatibles

Nombre ABI de los registros

Una de las cosas que se especifica en la ABI es el nombre de los registros, y el uso que se les da. En el RISC-V tenemos 32 registros, denotados como x0-x31. A nivel de procesador, todos los registros (salvo el x0), son iguales: los podemos utilizar en cualquier instrucción. No hay diferencia a nivel hardware entre ellos

Sin embargo, a nivel de programación, a cada registro se le assigna un uso diferente, y por tanto, un nombre distinto. Esta nueva denominación la llamamos nombres ABI. En esta tabla se resumen los nombres ABI asignados a los registros

Nombre Nombre ABI Uso Descripción
x0 zero Zero (Inmutable). Siempre está a 0
x1 ra Dirección de retorno
x2 sp Puntero de Pila (stack pointer) Los accesos a la pila se hacen a través de este registro
x3 gp Puntero global (Global pointer) NO USAR. Se reserva para la gestión de excepciones
x4 tp Puntero de hilo (Thread pointer) NO USAR. Se reserva para la gestión de excepciones
x5-x7 t0-t2 Registros temporales Se usan en cualquier momento, por cualquiera, para cualquier uso
x8-x9 s0-s1 Registro estáticos Se garantiza que mantienen su valor al llamar a cualquier módulo
x10-x17 a0-a7 Registro de argumentos Son registros también temporales, usados para pasar datos entre módulos
x18-x27 s2-s11 Registro estáticos Se garantiza que mantienen su valor al llamar a cualquier módulo
x28-x31 t3-t6 Registros temporales Se usan en cualquier momento, por cualquiera, para cualquier uso

A lo largo del curso iremos viendo para qué sirve cada registro. Empezaremos usando estos registros:

  • t0-t6: Registros temporales. Los usaremos para cualquier cálculo intermedio o temporal
  • a0-a7: Registros de argumentos. Se usan para almacenar los argumentos en las llamadas a otros módulos, y pasar así información entre ambos módulos. Son registro temporales. Los podemos usar para cualquier cálculo intermedio

A partir de ahora, SIEMPRE USAREMOS LOS NOMBRES ABI

Registros en el simulador RARs

En la columna de la izquierda, dentro de la ventana de registros del simulador RARs, están puestos los nombres ABI de los registros. En la columna central se encuentra el número de registro real: x0,x2,x3... omitiendo la x.

Ejemplo: Cálculo de una expresión

Como ejemplo vamos a calcular la expresión (a - b) + (c + 5) para los valores iniciales de a=1, b=2 y c=3, pero usando los registros con sus nombres ABI

Usamos los registros temporales: t0 - t6. Podemos usar cualquier de ellos para almacenar los valores de a, b y c. Por ejemplo: t0=a, t1=b y t2=c. El resultado final lo almacenamos en t6

El programa para evaluar la expresión es:

#-- Calculo de la expresión (a - b) + (c + 5)
#-- Usamos los nombres ABI de los registros
#-- Asignación: t0 = a,  t1 = b,  t2 = c
#-- Resultado en t6
#-- Evaluar para a=1, b=2, c=3

	.text
	
	#-- Inicialización de los registros
	#-- t0 = a = 1
	li t0, 1
	
	#-- t1 = b = 2
	li t1, 2
	
	#-- t2 = c = 3
	li t2, 3
	
	#-------- Realizar el calculo de la expresión
	#-- t3 = (a - b)
	sub t3, t0, t1  
	
	#-- t4 = c + 5
	addi t4, t2, 5
	
	#-- t6 = t3 + t4 = (a - b) + (c + 5)
	add t6, t3, t4
	
	#-- Terminar
	li a7, 10
	ecall

Lo ensamblamos y lo ejecutamos. Nos fijamos en la primera instrucción

li t0, 1

Sabemos que li es una pseudo-instrucción, y que el ensamblador la transforma en addi. Pero además, el registro t0 se convierte a su equivalente x5, por lo que la instrucción real en ensamblador es:

addi x5, x0, 1

También observamos que en los registro t0, t1 y t2 contienen los valores iniciales: 1, 2 y 3. Y que efectivamente en t6 se encuentra el resultado final: t6 = 7

Llamadas al sistema

Hemos visto cómo nuestros programas se comunican con los periféricos mediante entrada/salida mapeada. Esto es típico de los sistema empotrados. Sin embargo, la mayoría de las veces existe un sistema operativo debajo que se encarga de la gestión de toda la entrada/salida, no teniendo que implementarla nosotros

El mecanismo que tienen nuestros programas para utilizar servicios proporcionados por el sistema operativo se denomina llamadas al sistema

Esta idea está esquematizada en la siguiente figura. Una vez que nuestro programa se empieza a ejecutar, en cualquier momento se puede realizar una llamada al sistema para acceder a uno de los servicios que ofrece, como por ejemplo imprimir un número en la consola, o leerlo de ella

En ese momento, el control lo toma el sistema operativo, que realiza la acción pedida. Una vez finalizada, devuelve el control a nuestro programa, que se sigue ejecutando hasta que se termina

El sistema operativo, en nuestro simulador, se encuentra en las direcciones bajas, mientras que nuestro programa está en el segmento de código. El esquema anterior lo podemos representar también de esta forma:

Arranca nuestro programa(1), que se encuentra en el segmento de código (empieza en 0x00400000). En un momento determinado se realiza una llamada al sistema(2), por lo que el control se pasa al sistema operativo(3) (cuyo código está en la región debajo del segmento de código). Al finalizar devuelve el control otra vez a nuestro programa (4). Nuestro programa puede realizar tantas llamadas al sistema como sea conveniente, aunque en la figura, por simplicidad, sólo se ha esquematizado una. Por último nuestro programa termina (5)

Llamando al sistema: ecall

Para realizar una llamada al sistema utilizamos la instrucción ecall. Desde el menú de ayuda (Tecla F1) tenemos acceso a todas las llamadas al sistema disponibles en el simulador RARs. Están en la pestaña syscalls

Cada servicio proporcionado por el sistema operativo tiene asignado un número, que debemos introducir en el registro a7. Además, si el servicio necesita de unos parámetros de entrada, se los pasaremos a través de los registros a0, a1 y a2. Los valores devueltos por el servicio estarán en a0 y a1. Todo esta información está disponible en la ayuda. La primera columna indica el Nombre del servicio y la segunda su código

Así, por ejemplo, el primer servicio mostrado en la ayuda es la llamada al sistema prinInt, cuyo código es 1. Nos permite imprimir un número entero en la consola. Como parámetro de entrada hay que introducir el número a imprimir en el registro a0. El servicio no devuelve ningún valor. En las siguientes secciones veremos ejemplos de uso

Puntos de entrada y salida del programa principal

Vamos a entender qué es lo que ocurre en un sistema RISC-V desde que lo encendemos. Al darle alimentación, el contador de programa se inicializa con la dirección de la primera instrucción del sistema operativo. Es decir, que es el sistema operativo el que toma el control

Típicamente realiza la inicialización y se queda esperando. En un momento determinado, le decimos que arranque nuestro programa, por lo que le pasa el control, y comienza su ejecución. Nuestro programa tiene una dirección, llamada punto de entrada, que es donde se encuentra la primera instrucción. De forma genérica, lo esquematizamos así:

Sabemos que en el RARs el punto de entrada está en la dirección 0x00400000. Una vez que el sistema operativo nos pasa el control, nuestro programa realiza los cálculos que sean y termina. Esta operación de "terminación" significa que hay que devolver el control otra vez al sistema operativo. Para ello hay que invocar la llamada al sistema EXIT. En esta figura se muestra el mismo esquema anterior, pero particularizado para el mapa de memoria del RARS

El lugar donde se invoca el servicio exit se denomina punto de salida

Lo siguiente es una regla de buenas prácticas de programación:

  • El programa principal deberá tener un único punto de entrada y un único punto de salida

Terminando un programa: llamada al sistema EXIT

Vamos a probar nuestra primera llamada al sistema, que será el servicio de EXIT. Todos nuestros programas, a partir de ahora, acabarán invocando este servicio. La única es excepción será en el sistema RISC-V real en la FPGA, ya que en él no tenemos sistema operativo. Por lo que terminaremos con un bucle infinito (aunque en el laboratorio sólo trabajaremos con el simulador)

Si miramos el servicio de EXIT en la ayuda, vemos que tiene el código 10, y que no hay que pasarle ningún argumento (ni de entrada ni de salida):

En este programa estamos invocando el servicio Exit. Es un programa que no hace nada: en cuanto recibe el control del sistema operativo, lo devuelve otra vez (termina)

#-- Probando el servicio EXIT

	.text
	
#-- Este es el PUNTO DE ENTRADA del programa principal

	#-- No se hace nada
	
	#-- Este es el punto de salida del programa principal
	#-- se invoca al servicio exit
	
	#-- Primero cargamos en el registro a7 el codigo de servicio
	li a7, 10
	
	#-- y ahora realizamos la llamada al sistema
	ecall

¿Podríamos mejorar este programa? ¿Ves algo que te llame la atención?

¡¡Si!!! ¡Hay un número mágico! El 10, que se corresponde con el código del servicio de exit. Siguiendo las buenas praćticas de programación, conviene usar constantes para estos números mágicos. Esta es la nueva versión del programa:

#-- Probando el servicio EXIT
#-- Buenas prácticas de programación: definimos las
#-- constantes al comienzo del programa

        #-- Código del servicio Exit, para terminar
	.eqv EXIT 10

	.text
	
	#-- No se hace nada
	
	#-- Terminar
	li a7, EXIT
	ecall

PrintInt: Imprimiendo un número entero en la consola

Para practicar con las llamadas al sistema, vamos a invocar el servicio de impresión de un número decimal en la consola: PrintInt. Sacaremos un número constante, por ejemplo el 200

Primero miramos los parámetros que tiene el servicio PrintInt, así como su código de servicio:

El código es el 1 (registro a7), y como entrada hay que pasarle le valor a imprimir en el registro a0. Este es el programa que muestra el número 200 en la consola. Utilizamos constantes para los dos servicios utilizados: PRINT_INT y EXIT

#-- Ejemplo de uso del servicio PrintINT para
#-- sacar un entero por la consola

	#-- Servicio Print_Int
	.eqv PRINT_INT 01
	
	#-- Servicio Exit
	.eqv EXIT 10

	.text
	
	#-- Imprimir un numero
	li a0, 200
	
	#-- Invocar el servicio print_int
	li a7, PRINT_INT
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall

Lo ensamblamos y lo ejecutamos. Vemos que aparece el numero 200 en la consola

ReadInt: Leyendo un número entero de la consola

Vamos a probar ahora el servicio ReadInt, que nos permite leer un número entero desde la consola. Primero miramos la documentación, para ver el código y los argumentos

Su código es 5. No hay que pasarle ningún argumento de entrada. El número introducido por el usuario se devuelve en el registro a0

Para probarlo vamos a hacer un programa que almacene en el registro t0 el valor introducido por el usuario. Igual que antes, definimos la constante READ_INT con el valor del código:

#-- Ejemplo de uso del servicio ReadInt,
#-- para leer un número entero de la consola
#-- El dato leido se deposita en el registro t0

	.eqv READ_INT 5
	.eqv EXIT 10
	
	.text
	
	#-- Invocar ReadInt
	li a7, READ_INT
	ecall
	
	#-- El número leido se devuelve por a0
	#-- Lo pasamos a t0
	mv t0, a0
	
	#-- Terminar
	li a7, EXIT
	ecall

Lo ensamblamos y lo ejecutamos. Al arrancar el programa se queda esperando a que introduzcamos un número en la consola. Vemos que en la consola aparece el cursor:

Introducimos el número 200 (por ejemplo) y apretamos ENTER. El programa termina. En la derecha vemos que el registro t0 tiene el valor que hemos introducido

En esta animación se muestra el programa completo en funcionamiento:

Activando el diálogo de entrada

Para hacer más sencilla la entrada de datos, se pueden activar las ventanas de diálogo para nos aparezca una ventana nueva cada vez que necesitemos que el usuario introduzca datos de entrada, en vez de hacerlo por la consola

Esta opción se activa en el menú Settings/Pop up dialog for input syscalls (5,6,7,8,12)

Ahora, al ejecutar el programa nos aparecerá la ventana de diálogo pidiéndolos el número entero:

El programa funciona exactamente igual, simplemente que la entrada de datos es más visual. En esta animación se muestra en funcionamiento

PrintChar: Imprimiendo un caracter

Podemos imprimir cualquier carácter ASCII mediante el servicio PrintChar

El código de servicio es el 11. En el registro a0 se introduce el valor ascii del carácter a imprimir. Las constantes ASCII se pueden introducir bien con su número, decimal o hexadecimal, o bien directamente como el carácter entre comillas simples: 'a', '\n', 'Z', etc...

En este ejemplo se imprime el carácter 'A', pasándole su código ASCII hexadecimal (0x41) y luego un salto de línea usando como notación las comillas simples: '\n'. Como son dos caracteres, se invoca dos veces seguidas al servicio PrintChar

#-- Ejemplo de uso del servicio PrintChar
#-- para imprimir un carácter ASCII
#-- Se imprime una 'A' seguida de un salto de línea

	.eqv PRINT_CHAR 11
	.eqv EXIT 10

	.text
	
	#-- Imprimir una A, usando su codigo ASCII
	li a0, 0x41
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir un salto de linea, usando '\n'
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall

Lo ensamblamos y lo ejecutamos. En la consola veremos que aparece la A, y dos líneas más abajo la de finalización del programa (porque hay un salto de línea)

Usando el RARs en línea de comandos

El simulador RARs también lo podemos usar para ejecutar nuestros programas RISC-V desde la consola. En el laboratorio siempre lo utilizaremos con la interfaz gráfica, pero es interesante saber que también funciona en la consola

Para ejecutar el ejemplo de prueba de la llamada al sistema print_int, hacemos los siguiente. El fichero .jar con el simulador debe estar en el mismo directorio que el fuente (o bien indicamos en qué ruta está):

java -jar rars1_5.jar print_int.s

El programa se ensambla y se ejecuta. Vemos el resultado: aparece 200 en la consola y el programa termina. No se ha abierto en ningún momento la interfaz gráfica

También podemos ejecutar el ejemplo read_int. En este caso, el programa se queda esperando a que introduzcamos el valor por la consola. Le pasamos el parámetro t0 para que al terminar imprima el valor del registro t0. Y con el parámetro dec le decimos que lo muestre en decimal:

java -jar rars1_5.jar t0 dec read_int.s

Introducimos un valor, por ejemplo 600. Termina y nos imprime el valor del registro t0, que efectivamente vale 600 en decimal:

En esta animación se muestra su funcionamiento

Recopilación de instrucciones hasta el momento

  • Instrucciones básicas: Son las que se transforman a código máquina y que ejecuta el procesador

  • Pseudo-instrucciones: No existen realmente como instrucciones. El ensamblador las transforma en instrucciones básicas. Una pseudo-instrucción puede dar lugar a 1 ó varias instrucciones básicas

  • Directivas: Dar información al programa ensamblador. No generan código máquina

Actividades NO guiadas

Practica. Piensa. Escribe código. Estúdialo. Modifícalo. Observa los errores. Soluciónalos. Esa es la manera en que te conviertas en un/a ingeniero/a excepcional. Recuerda: como ingeniero/a te contratarán para que resuelvas problemas. Problemas nuevos. Para eso recurren a ti

NOTA: A partir de ahora usaremos los registros con sus nombres de la ABI (Salvo que nos indiquen expresamente lo contrario)

Ejercicio 1

Escribe un programa para calcular la expresión f = (2*a - b) + c - 1. Utiliza los registros con sus nombres de la ABI de RISC-V. Evalúa la expresión para a = 10, b = 20 y c = 30, y comprueba que el resultado es correcto

Ejercicio 2

  1. Escribe un programa que realice la suma de dos números enteros introducidos por el teclado. Se introducen los dos números y se muestra el resultado de la suma en la consola. Intenta usar buenas prácticas de programación. Activa los diálogos emergentes para la introducción de datos

  2. Prueba el programa ejecutándolo desde la línea de comandos directamente

Ejercicio 3

Modifica el programa anterior para que los valores leídos se almacenen primero en las variables a y b. Después se debe realizar la suma leyendo estas variables, depositar el resultado en la variable f y finalmente imprimirlo por la consola. Usa como único puntero para acceder a las tres variables el registro s0. Ejecutar el programa y comprobar que las variables contienen los valores correctos, y que la suma es la que aparece en la consola

Ejercicio 4

Escribe un programa para pedir al usuario las 3 variables a,b y c, y calcular la expresión f = (2*a - b) + c - 1 (es la misma expresión del ejercicio 1). El resultado se imprime en la consola. No hace falta almacenar a,b, y f en memoria. Usar registros temporales

Ejercicio 5

Escribe un programa para pedir un número entero (0 - 255) y escribirlo en el puerto de salida del display de 7 segmentos derecho. Comprueba que funciona bien introduciendo diferentes valores. Por ejemplo, con el 255 (0xFF) se deben encender todos los segmentos (NOTA: No se está pidiendo que se visualice el número que introduce el usuario, sino que este número se envíe al Display de 7 segmentos directamente)

Ejercicio 6

Escribe un programa que pida al usuario cuatro valores enteros y los deposite consecutivamente en el segmento de datos. Luego los debe leer de memoria e imprimirlos en la consola. Tras cada número se debe sacar una coma "," por la consola. Todavía no sabemos hacer bucles, así que las escrituras y las lecturas se hacen para las 4 posiciones de memoria, una detrás de otra

Para probarlo, bajar la velocidad a 29 instrucciones por segundo. De esta forma podrás ver de manera interactiva cómo se van situando los valores en sus posiciones de memoria correspondientes

Ejercicio 7

Escribe un programa que recorra el segmento de datos (con bucle infinito) leyendo cada palabra e imprimiéndola en la consola. Inicializar el segmento de datos con los valores 1,2,3,4,5,6,7,8,9 y 10. Se debe usar el carácter de salto de línea para separar los valores impresos en la consola. Para probar el programa baja la velocidad a 29 instrucciones/segundo o bien utiliza un Breakpoint

Ejercicio 8

Escribe el programa del cálculo de la secuencia de Fibonacci , que ya has hecho en otros ejercicios, pero usando los registros con nombres ABI e imprimiendo cada término por la consola, separados por el carácter ','. No hay que imprimir los dos términos iniciales de Fibonacci

Ejercicio 9

Escribe un programa para leer un carácter usando el servicio ReadChar. El programa se ejecuta en un bucle infinito. Lee el carácter, lo incrementa en una unidad , lo imprime en la consola (es un cifrado muy simple) e imprime un salto de línea

Ejercicio 10

Modifica el programa del ejercicio 7 para que se impriman los valores del segmento de datos en hexadecimal usando el servicio PrintHex. Inicializa el segmento de datos con las palabras: 10,11,12,13,14,15,127,255,0x00FABADA, 0xCAFEBACA

Notas para el profesor

  • Título informal de la clase: "No estamos solos..."
  • A partir de ahora ya no podemos hacer lo que queramos... hay que seguir unas REGLAS, para convivir con otros programas
  • El primer "programa" ajeno con el que convivimos es el Sistema Operativo
  • Cuando estamos solos en casa, hacemos lo que queremos... es la anarquía... Si compartimos piso la cosa cambiar...

Autores

Licencia

Enlaces