Skip to content

L9: Practica 4

javierMaciasGMAIL edited this page Nov 17, 2024 · 132 revisions

Sesión Laboratorio 9: Práctica 4-1

  • Tiempo: 2h
  • Objetivos de la sesión:
    • Comprender la división entre programa principal y subrutina (un nivel)
    • Entender el mecanismos jal/ret que permite hacer subrutinas
    • Saber cómo escribir el programa principal y las subrutinas en ficheros separados
    • Aprender a pasar parámetros en el programa principal y la subrutina
    • Conocer el convenio de uso de registros
    • Practicar, practicar, practicar

Vídeos

  • Fecha: 2019/Nov/30

Vídeo 1/6: Subrutinas: Instrucciones jal y ret

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

Click to see the youtube video

Vídeo 2/6: Registro ra: Dirección de retorno

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

Click to see the youtube video

Vídeo 3/6: Instrucciones jal y jalr

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

Click to see the youtube video

Vídeo 4/6: Paso de parámetros

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

Click to see the youtube video

Vídeo 5/6: Separación de las subrutinas en ficheros independientes

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

Click to see the youtube video

Vídeo 6/6: Convenio de uso de los registros

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

Click to see the youtube video

Contenido

Introducción

Ya sabemos realizar saltos (condicionales o no) para lograr que el microprocesador ejecute instrucciones situadas en otras posiciones de memoria. En esta sesión aprenderemos a hacer subrutinas, que son trozos de código que realizan una función específica, y que al terminar devuelven el control al punto siguiente en el que fueron invocadas. Esto es importantísimo, porque nos permite reutilizar código fácilmente, y organizar nuestros programas. Para resolver un problema complejo, dividimos el código en trozos más pequeños: las subrutinas

Las subrutinas es lo que permite que en los lenguajes de alto nivel (python, C, Ada, C++...) se implementen funciones, procedimientos o métodos. Por eso usaremos indistintamente los términos subrutina o función para referirnos al mismo concepto

Ideas claves: Organización y reutilización

Las funciones tienen dos misiones fundamentales:

  • Organizar y estructurar nuestro código: Aplicamos la idea de "divide y vencerás". Para resolver un problema complejo lo dividimos en pequeños trozos (funciones). Cada función realiza una operación muy concreta

  • Reutilizar el código: El mismo código lo podemos invocar desde diferentes partes de nuestro programa, ahorrando memoria, y haciendo más fácil el mantenimiento y depuración de los programas

Esto es válido en cualquier lenguaje de programación. Cuando trabajamos en ensamblador, las funciones se llaman subrutinas

Programa principal y una subrutina

Para comprender el mecanismo de funcionamiento de las subrutinas partiremos de lo que ya conocemos e iremos introduciendo el resto de conceptos poco a poco. Partiremos de un ejemplo en el que hay que realizar una tarea genérica y terminar. Esta tarea puede ser cualquiera, pero la representaremos mediante la impresión en la consola del mensaje "TAREA 1"

Vamos a ir viendo las diferentes formas de organizarlo

Programa principal

Definimos programa principal como el código que está comprendido entre el PUNTO DE ENTRADA y EL PUNTO DE SALIDA. El sistema operativo pasa el control a nuestro programa, y se empieza a ejecutar su primera instrucción (que en el caso del simulador RARs sabemos que la primera del segmento de código: 0x00400000). Luego realiza la Tarea 1 y finalmente termina

Esto lo esquematizamos con el siguiente diagrama

El programa de ejemplo, que realiza la tarea 1, es el siguiente:

##-- Esquema de un programa que realiza la tarea 1, 
#--  entre otras cosas
#--  Está estructura en UN UNICO PROGRAMA PRINCIPAL

	.include "servicios.asm"
	
	.data
	
msg_tarea1:	.string "\nTAREA 1\n"
	
	.text 
	
	
	#------------------------ PROGRAMA PRINCIPAL (MAIN)-------------------------
	#-- PUNTO DE ENTRADA
	
	
	#-- Se realizan las inicializaciones u otras acciones necesarias
	#-- ....
	
	#-- Acción de la Tarea 1: La simulamos imprimiendo un 
	#-- mensaje en la consola, pero la tarea podría ser cualquiera
	#-- que necesitasemos en nuestro proyecto
	la a0, msg_tarea1
	li a7, PRINT_STRING
	ecall
	
	#-- ....
	#-- Por aqui habria más código
	#-- ...
	
	#-- PUNTO DE SALIDA
	li a7, EXIT
	ecall
	#-----------------------------------------------------------------------
	

Lo ensamblamos y lo ejecutamos. En la consola vemos el mensaje de que la TAREA 1 se ha ejecutado

Repetición de la tarea 1

Supongamos que ahora nuestro proyecto ha sufrido modificaciones y es necesario ejecutar la Tarea 1 dos veces: una al comenzar, y otra al terminar

Con lo que sabemos hasta ahora, nuestra única opción es hacer "copy" y "paste" del código de la tarea 1. El esquema quedaría ahora así:

Y este es el código:

##-- Esquema de un programa que realiza la tarea 1, 
#--  La tarea 1 se realiza al comienzo y al final
#--  Está estructura en UN UNICO PROGRAMA PRINCIPAL

	.include "servicios.asm"
	
	.data
	
msg_tarea1:	.string "\nTAREA 1\n"
	
	.text 
	
	
	#------------------------ PROGRAMA PRINCIPAL (MAIN)-------------------------
	#-- PUNTO DE ENTRADA
	
	#-- Comienzo
	#-- Ejecutar la Tarea 1
	la a0, msg_tarea1
	li a7, PRINT_STRING
	ecall
	
	#-- ....
	#-- Otros cálculos y operaciones de mi programa
	#-- ...
	
	#-- Final
	#-- Ejecutar la Tarea 1
	la a0, msg_tarea1
	li a7, PRINT_STRING
	ecall
	
	#-- PUNTO DE SALIDA
	li a7, EXIT
	ecall
	#-----------------------------------------------------------------------

Al ensamblarlo y ejecutarlo, vemos dos veces el mensaje que saca la tarea 1, como era de esperar

Análisis del programa

Como se trata de un ejemplo muy sencillo, y la tarea 1 tiene muy pocas líneas de código, nos puede parecer una solución correcta. Sin embargo NO LO ES. Tiene los siguientes problemas:

  • Duplicación de código: En dos partes del programa estamos duplicando el código, por lo que nuestro programa va a aumentar de tamaño. Si la tarea 1 fuese más compleja y tuviese muchísimas instrucciones, crecería mucho en tamaño

  • No escalable: ¿Y si ahora fuese necesario ejecutar la tarea 3 veces?. Habría que triplicar su tamaño... Esta solución, no escala. No es general

  • Difícil de mantener. Si hay que modificar la tarea 1 para que añadir más funcionalidad... hay que hacer en dos sitios. Esto es propenso a errores y es una mala práctica de programación

Solución: La tarea 1 es una SUBRUTINA

El diseño correcto es convertir la tarea 1 en una función o subrutina: Es un fragmento de código que realiza una tarea específica, y que el programa principal debe invocar cuando considere necesario. El nuevo diseño de mi programa está dividido en dos partes:

  • El programa principal: Es el que se comienza a ejecutar, llama a la función tarea1 al comienzo, realiza los cálculos pertinentes, vuelve a invocar la tarea1 y termina (punto de salida)
  • Una subrutina para realizar la tarea 1

El esquema es el siguiente:

Ahora el programa principal realiza una llamada a la subrutina. Se ejecuta la tarea 1. Y esta devuelve el control al punto del programa principal desde donde se invocó

El programa principal realiza el resto de operaciones. Y por último vuelve a invocar a la subrutina tarea 1. Cuando termina, le devuelve el control al mismo punto desde donde fue invocada

Estas son las ideas importantes:

  • La subrutina tiene un ÚNICO PUNTO DE ENTRADA, y un ÚNICO PUNTO DE SALIDA. Por supuesto, igual que ocurre con el programa principal, podría haber varios puntos de salida. Sin embargo, es una buena práctica de programación el tener sólo un único punto de salida

  • La subrutina devuelve el control al punto desde donde fue invocada. Por tanto, existe un mecanismo que debe recordar a qué dirección retornar. ¿Cuál es este mecanismo?

Con este nuevo esquema se solucionan los problemas anteriores. Ahora sólo hay un único código que ejecuta la tarea 1 (NO está duplicado). El programa principal simplemente lo invoca dos veces, pero no está duplicado en memoria. Si es necesario, el programa principal lo puede invocar tantas veces como quiera, sin incrementar apenas el tamaño del programa

Y por supuesto, esta subrutina es más fácil de mantener. Normalmente las subrutinas las implementan personas diferentes. El jefe de proyecto hace la división de las tareas, y cada ingeniero se encarga de asegurar que su subrutina funciona correctamente

Las instrucciones jal/ret

El punto de entrada de una subrutina lo definimos mediante una etiqueta, como hacemos siempre. Es la dirección donde está la primera instrucción de la subrutina

La llamada se realiza con la instrucción jal. Esta instrucción realiza un salto incondicional a la subrutina, pero almacena la dirección de retorno: es la dirección de la siguiente instrucción a jal

El punto de salida de la subrutina se define con la instrucción ret. Cuando se ejecuta este instrucción, se realiza un salto incondicional a la dirección de retorno, que había sido almacenada previamente por la instrucción jal

Estas ideas se esquematizan en esta figura

El ejemplo anterior lo vamos a separar en el programa principal y la subrutina tarea1. Este es el código:

##-- Esquema de un programa que realiza la tarea 1
##-- Está dividido en el PROGRAMA PRINCIPAL y la
##-- Subrutina tarea1 

	.include "servicios.asm"
	
	.data
	
msg_tarea1:	.string "\nTAREA 1\n"
	
	.text 
	
	
	#------------------------ PROGRAMA PRINCIPAL (MAIN)-------------------------
	#-- PUNTO DE ENTRADA
	
	#-- Comienzo
	#-- Ejecutar la Tarea 1
	jal tarea1
	
	#-- ....
	#-- Otros cálculos y operaciones de mi programa
	#-- ...
	
	#-- Final
	#-- Ejecutar la Tarea 1
	jal tarea1
	
	#-- PUNTO DE SALIDA
	li a7, EXIT
	ecall
	#-----------------------------------------------------------------------
	
	
	 #-------- SUBRUTINA TAREA 1 ----------------
tarea1:  #-- PUNTO DE ENTRADA

	 #-- Ejecutar la Tarea 1
	la a0, msg_tarea1
	li a7, PRINT_STRING
	ecall
	
	#-- PUNTO DE SALIDA
	ret
	#----------------------------------------------

Si lo ensamblamos y lo ejecutamos, el resultado es exactamente igual que antes: en la consola nos aparece dos veces el mensaje TAREA1

La llamadas a la subrutina se realizan con esta instrucción:

jal tarea1

El punto de entrada de tarea1 es este:

tarea1:

Se define mediante una etiqueta, igual que cualquier otra etiqueta normal

El punto de salida de la subrutina lo define la instrucción ret

ret

La magia de la instrucción ret

Ejecutamos el programa anterior paso a paso. La magia de las subrutinas está en que al ejecutar la instrucción ret, siempre se retorna a la instrucción siguiente al jal usado para su invocación. Así, el primer jal está en la línea 19 (dirección 0x00400000). Se invoca tarea1 y cuando termina devuelve el control a la siguiente instrucción, que está en la línea 27 (dirección 0x00400004)

La siguiente instrucción es otro jal, que vuelve a llamar a la subrutina tarea1. Al ejecutar el ret, ahora se devuelve el control a la instrucción de la línea 30 (dirección 0x00400008)

El punto de entrada de la subrutina está dado por la etiqueta tarea1, cuyo valor lo podemos ver en la tabla de símbolos: 0x00400010

En esta animación vemos el proceso de ejecución. Al ejecutar ret, en cada caso salta hacia atrás a un lugar diferente. Al ejecutar las instrucciones jal se salta siempre a la dirección 0x00400010, donde comienza la subrutina, pero el salto al retornar es a una dirección diferente, dependiendo de desde dónde se haya llamado

El registro ra

¿Cuál es el mecanismo que permite al procesador recordar la dirección de retorno al ejecutar ret? Cada vez que se ejecuta la instrucción jal, se guarda la dirección de la siguiente instrucción en el registro ra (return address). Al ejecutarse la instrucción ret, el procesador salta a la dirección almacenada en ra

El registro ra es en realidad el registro x1. Es un registro de propósito general, que se puede usar para cualquier otra cosa. Sin embargo, en la ABI del RISCV se ha establecido que SOLO LO PODEMOS USAR con este propósito de almacenar la dirección de retorno. Por ello, NO DEBEMOS USARLO NUNCA CON OTROS FINES

Para entender el funcionamiento, usaremos este código, que simplemente llama a una subrutina vacía, que sólo contiene la instrucción ret

##-- Entendiendo las instrucciones jal/ret

	.include "servicios.asm"

	.text
	
	#-- Salto a la subrutina
	jal subrutina
	
retorno:  #-- Aquí se retorna. Ponemos una etiqueta
          #-- para ver la direccion en la tabla de simbolos
	
	#-- Terminar
	li a7, EXIT
	ecall
	
#-- Punto de entrada de la subrutina	
subrutina:

	#-- Punto de salida de la subrutina
	ret

Lo ensamblamos y nos fijamos en la tabla de símbolos:

Etiqueta Dirección Descripción
retorno 0x00400004 Dirección de retorno (siguiente a jal)
subrutina 0x0040000c Punto de entrada de la subrutina

Al ejecutarlo paso a paso vemos lo que ocurre: con la instrucción jal subrutina, se almacena en ra la dirección de retorno, que es la 0x00400004, la siguiente al jal

El procesador salta a la dirección 0x0040000c, que es el punto de entrada de la subrutina. Ahí se encuentra la instrucción ret. Al ejecutarla, se realiza un salto a la dirección indicada por ra, con lo cual se retorna

En esta animación lo vemos en funcionamiento

En realidad jal / ret son pseudoinstrucciones. En el ejemplo anterior, el ensamblador las ha convertido a las instrucciones reales:

El salto a la subrutina se hace mediante la instrucción:

jal x1,6

Esta instrucción significa: saltar hacia adelante 6 medias palabras (3 instrucciones) y almacenar en el registro x1 (que es ra) la dirección siguiente a la propia instrucción jal

La instrucción ret se ha sustituido por:

jalr x0, x1, 0

Su significado es: salta a la dirección dada por el registro x1 + 0, y la dirección siguiente a jalr guardarla en x0 (es decir, ignorarla)

En realidad, en "humano" la escribimos así:

jalr x0, 0(x1)

Paso de parámetros

Tanto el programa principal como la subrutina pueden intercambiar información. El programa principal envía información mediante los parámetros de entrada y la subrutina devuelve información mediante los valores de retorno

El convenio establecido para el paso de parámetros y de valores de retorno es el siguiente:

  • El programa principal envía datos a la subrutina mediante los registros a0-a7 (¡¡¡Y SÓLO ESOS!!!). Además se hace en orden: si sólo tiene un argumento, se pasa por a0. Si tiene dos, se pasa el primero por a0 y el segundo por a1. Y así sucesivamente
  • La subrutina devuelve los valores de retorno en los registros a0 y a1, también en orden. Si sólo devuelve un valor, tiene que ser por a0

Ejemplo: función de incremento

Vamos a programar una subrutina (función) a la que se le pasa como entrada un número y devuelve su valor incrementado en una unidad. El nombre de la función es incrementar. Si la implementásemos en lenguaje C, sería así:

/* Función para incrementar un número */
int incrementar(int num)
{
  return ++num;
}

Si la implementásemos en python, sería así:

def incrementar(num): 
    return num+1 

La implementación de la subrutina en ensamblador sería así:

#--------------------------------------------
#-- Funcion de incremento:
#--   Entradas: a0: numero a incrementar
#-- Salidas:
#--   a0: Numero incrementado
#--------------------------------------------
incrementar:

	#-- Incrementar a0
	addi a0, a0, 1
	
	#-- Retornar
	ret

Como sólo tiene un parámetro de entrada, es obligatorio pasarlo por a0. Como sólo tiene un parámetro de salida, es obligatorio devolverlo por a0

El programa principal imprime el valor original, llama a la función incrementar, imprime un salto de línea, luego imprime el resultado y termina. Lo implementamos de esta manera:

##-- Ejemplo de subrutina
##-- Funcion para devolver el incremento de su parámetro

	.include "servicios.asm"

	#-- Valor a incrementar
	.eqv VALOR 2

	.text
	
	#-- Imprimir el valor original
	li a0, VALOR
	li a7, PRINT_INT
	ecall
	
	#-- Llamar a la subrutina de incrementar
	li a0, VALOR
	jal incrementar
	
	#-- Guardar a0 en t0
	mv t0, a0
	
	#-- Imprimir '\n'
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir el valor incrementado
	mv a0, t0
	li a7, PRINT_INT
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall

Es importante hacer notar que se trata de dos partes independientes del programa. Una parte es el programa principal, y la otra la función incrementar. Sólo se comunican entre ellas a través del registro a0, y nada más

A la hora de implementar el programa completo tenemos varias opciones. Una es meterlo todo en un único fichero. Típicamente ponemos el programa principal al comienzo, y tras su punto de salida la(s) subrutina(s):

##-- Ejemplo de subrutina
##-- Funcion para devolver el incremento de su parámetro

	.include "servicios.asm"

	#-- Valor a incrementar
	.eqv VALOR 2

	.text
	
	#----------------------------------------------
	#-- PROGRAMA PRINCIPAL
	#----------------------------------------------
	
	
	#-- Imprimir el valor original
	li a0, VALOR
	li a7, PRINT_INT
	ecall
	
	#-- Llamar a la subrutina de incrementar
	li a0, 2
	jal incrementar
	
	#-- Guardar a0 en t0
	mv t0, a0
	
	#-- Imprimir '\n'
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir el valor incrementado
	mv a0, t0
	li a7, PRINT_INT
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall
	
#--------------------------------------------
#-- Funcion de incremento:
#--   Entradas: a0: numero a incrementar
#-- Salidas:
#--   a0: Numero incrementado
#--------------------------------------------
incrementar:

	#-- Incrementar a0
	addi a0, a0, 1
	
	#-- Retornar
	ret

Ensamblamos el programa y lo ejecutamos. En la consola vemos primero el número 2 y debajo el 3: es el 2 incrementado en una unidad por nuestra función

Separación por ficheros

Sin embargo, tanto el programa principal como las subrutinas las podemos colocar en ficheros separados. El ensamblado se hace de forma independiente, cada fichero por separado y un programa llamado enlazador (linker) se encarga de unirlos y colocarlos correctamente en memoria

Para realizar la separación en ficheros, hay que usar la directiva .globl en el fichero de la subrutina para que la etiqueta con su punto de entrada sea accesible desde el otro fichero. Así, el ejemplo anterior lo dividimos en dos ficheros:

  • Fichero incrementar.s: contiene la subrutina
#-- Fichero con la subrutina e incremento
		
	#-- Hacer accesible el punto de entrada
	#-- incrementar desde otro ficheros	
	.globl incrementar	
	
	#-- La funcion se encuentra en el segmento de texto
	.text
#--------------------------------------------
#-- Funcion de incremento:
#--   Entradas: a0: numero a incrementar
#-- Salidas:
#--   a0: Numero incrementado
#--------------------------------------------
incrementar:

	#-- Incrementar a0
	addi a0, a0, 1
	
	#-- Retornar
	ret
  • Fichero incrementar_main.s: Contiene el programa principal
#-- Programa principal
#-- Usa la subrutina incrementar() que está en otro fichero

	.include "servicios.asm"

	#-- Valor a incrementar
	.eqv VALOR 2

	.text
	
	#-- Imprimir el valor original
	li a0, VALOR
	li a7, PRINT_INT
	ecall
	
	#-- Llamar a la subrutina de incrementar
	li a0, 2
	jal incrementar
	
	#-- Guardar a0 en t0
	mv t0, a0
	
	#-- Imprimir '\n'
	li a0, '\n'
	li a7, PRINT_CHAR
	ecall
	
	#-- Imprimir el valor incrementado
	mv a0, t0
	li a7, PRINT_INT
	ecall
	
	#-- Terminar
	li a7, EXIT
	ecall

Para que el simulador pueda generar correctamente el código máquina cuando hay varios ficheros, tenemos que abrir todos los ficheros implicados (y sólo esos ficheros) en las pestañas del editor y activar la opción: settings/assemble all files currently open

Ahora NOS SITUAMOS SOBRE EL PROGRAMA PRINCIPAL y lo ensamblamos. Si tenemos activada la visualización de la tabla de símbolos, veremos que la etiqueta incrementar está marcada como global

Lo ejecutamos normalmente

Datos y código

Cuando se hace la división en programa principal y subrutinas, cada parte tendrá código y opcionalmente datos. De esta manera, en cada subrutina habrá que colocar también la directiva .text para indicar la parte del código y .data si hubiese datos que son sólo de la subrutina. Esto es especialmente importante si cada parte está en un fichero diferente

La plantilla sería así:

#-- Plantilla con programa principal y Subrutinas

	.include "servicios.asm"
	
#-----------------------------------------------------------
#--- PROGRAMA PRINCIPAL
#-----------------------------------------------------------

	#-- Datos para el programa principal
	.data	
	
	#-- Código del programa principal
	.text
	
	#-- Punto de ENTRADA
	
	#---  ....
	
	#-- Punto de SALIDA
	li a7, EXIT
	ecall
	
#---------------- FIN PROGRAMA PRINCIPAL ------------------------

#---------------------------------------------------------------
#--- SUBRUTINA 1
#---------------------------------------------------------------

	#-- Hacer el punto de entrada global
	.globl subr1

	#-- Datos de la subrutina 1
	.data	
	
	#-- Código de la subrutina 1
	.text
	
	#-- PUNTO DE ENTRADA
subr1:  

	#--- .....
	
	#-- PUNTO DE SALIDA
	ret

Bien lo podemos poner todo en el mismo fichero, o bien lo podemos separar en varios ficheros

Convenio de uso de los registros

Los registros NO PODEMOS usarlos como queramos, porque hay unos convenios para que podamos reutilizar los programas. La violación de estos convenios se considera un ERROR GRAVE, y aunque el programa realice su función correctamente, sería incompatible con el código hecho por otros miembros del equipo

  • Sólo se pueden pasar parámetros a nuestras subrutinas mediante los registros a0-a7. NUNCA lo haremos usando otros registros
  • El paso de parámetros a las subrutinas se hace en orden: el primer argumento se pasa por a0, el segundo por a1, el tercero por a2, etc.
  • Sólo se pueden recibir valores de retorno usando los registros a0 y a1
  • Los valores de retorno se hacen en orden. Si la función tiene un único valor de retorno se devuelve por a0. Si tiene dos, el segundo por a1
  • Los registros s0-s11 son registros estáticos. Esto significa que NO se pueden modificar al ejecutar una subrutina: su valor, antes y después de llamar a una subrutina tiene que ser el mismo (no puede cambiar)
  • Los registros del t0-t6 son temporales. Esto significa que tras la llamada a una subrutina debemos suponer que HAN CAMBIADO. O dicho de otra manera, si después de llamar a una subrutina usamos los registros temporales, es OBLIGATORIO inicializarlos con algún valor (no podemos dar por supuesto que tienen el mismo valor que antes de llamar a la subrutina)
  • La misma regla anterior se aplica a los registros a0-a7: Se consideran temporales, aunque su uso está establecido para el paso de parámetros

Algunos consejos para no violar el convenio de uso de los registros

El convenio de uso de registros es muy sencillo, pero es traicionero. A veces pensamos que NO lo estamos violando, pero en realidad sí. Aquí exponemos algunas reglas que seguiremos en los ejercicios para garantizar que se cumple el convenio:

  • En el programa principal (main) usaremos los registro estáticos s0-s11 para almacenar información que necesitamos durante en el programa principal: punteros a cadenas, punteros a variables, contadores, etc. Puedes usar cualquier de ellos sin temor
  • En el programa principal, usaremos los registro temporales t0 - t6 entre llamadas a subrutina. Una vez que llamemos a una subrutina, DEBEMOS SUPONER que los registros temporales han perdido sus valores anteriores. Si los queremos usar, habrá que inicializarlos otra vez
  • Dentro de las subrutinas sólo usaremos registros temporales
  • Los registros a0-a7 los reservamos para el paso de parámetros

Actividades NO guiadas

Los conceptos de subrutina y programa principal son muy importantes. Sólo se llega a comprenderlos correctamente haciendo ejercicios, y practicando mucho. Lo mismo con el convenio de uso de los registros. Recuerda: no los puedes usar de cualquier manera. Y es fácil violar estas reglas si no se han practicado mucho

Ejercicio 1

  1. Se tiene este programa principal que imprime en la consola dos mensajes: uno para indicar que comienza el programa y otro para saludar. Crea una subrutina llamada saludar, que se encargue de imprimir el mensaje del saludo. El programa principal debe imprimir el primer mensaje, llamar a la subrutina saludar para imprimir el segundo mensaje (el saludo) y terminar. Implementar la subrutina en el mismo fichero que el programa principal, con su punto de entrada después del punto de salida del programa principal. Las constantes con los códigos de los servicios del sistema operativo se ha colocado en el fichero servicios.s
	
	    .data
msg_main:   .string "Comienza el programa principal\n"
msg_saludo: .string "Hola!\n"	
	
        .include "servicios.s"

	.text
	
	#----- PROGRAMA PRINCIPAL ----------------
	
	#-- Imprimir mensaje desde programa principal
	la a0, msg_main
	li a7, PRINT_STRING
	ecall
	
	#-- Saludar
	la a0, msg_saludo
	li a7, PRINT_STRING
	ecall
	
	#-- Terminar (Punto de salida)
	li a7, EXIT
	ecall
	#-----------------------------------------
  1. Modificar la implementación para que el programa principal se encuentre en un fichero: Ej1-main1.s y la función saludar en otro: saludar.s. Ejecutarlo y comprobar que el funcionamiento es igual. Ambos ficheros tiene que tener las directivas .text y .data para indicar al ensamblador qué es código y qué son datos (dado que ambas partes tienen tanto código como datos)

  2. Modificar el programa principal para que llame a la función saludar dos veces y nos aparezca el saludo dos veces por tanto (no usar bucle, sino simplemente llamar a la función dos veces seguidas)

Ejercicio 2

Escribe un programa que pida al usuario un número entero, al que llamaremos x. A continuación el programa imprimirá una línea con x caracteres '*' (asteriscos). Así, si se introduce el 10, el programa imprimirá la cadena: "**********"

Este programa debe llamar a la subrutina linea(x) que recibe como entrada el parámetro x (número de asteriscos a imprimir) y no devuelve nada. El programa principal pide el número de caracteres al usuario, llama a la función linea y termina

Tanto el programa principal como la subrutina deben estar en ficheros separados

En esta animación lo puedes ver en funcionamiento. Se ha ejecutado 2 veces, con valores distintos

Ejercicio 3

Modificar el programa anterior, tanto el principal como la subrutina linea para que ahora la función linea admita dos parámetros: car,x, siendo car el carácter a repetir y x el número de repeticiones:

Este sería el prototipo de la nueva función:

  • void linea (char car, int x) El primer argumento es un carácter y el segundo un entero. La función No devuelve nada

El programa principal deberá pedir al usuario que introduzca el carácter y la cantidad, y luego invocar a la función linea con ellos

Ejercicio 4

Escribe un programa para pedir al usuario una cadena y calcule su longitud. El programa principal pide la cadena al usuario, llama a la función len, que es la encargada de calcular la longitud, y se imprime su valor en la consola

La función len tiene como primer argumento el puntero a la cadena a calcular su longitud y devuelve dicha longitud. El prototipo de la función es:

  • int len(*cad)

Tanto el programa principal como la subrutina deben estar en ficheros separados

En esta animación se muestra en funcionamiento

Ejercicio 5

Escribir la subrutina count que cuenta el número de veces que aparece un cierto carácter en la cadena. Su primer argumento es el puntero a la cadena introducida por el usuario y el segundo el carácter a contador. El prototipo es:

  • int count(*cad, car)

Esta subrutina debe estar en su propio fichero

Hacer un programa principal en un fichero separado que pida al usuario una cadena, y que indique la cantidad de caracteres 'a' y 'e' que hay en ella. Para hacerlo, deberá llamar dos veces a la subrutina count

En esta animación el programa se ejecuta dos veces, para ver su funcionamiento

Ejercicio 6

Usando la función count del ejercicio 5 (en su propio fichero) y la función len del ejecicio 4 (también en su propio fichero), crear un programa principal (en otro fichero) que pida al usuario una cadena, imprima su longitud, y luego imprima el número de veces que aparece el carácter 'a' y 'e'

Ejercicio 7

Escribir la función palindromo que tiene un parámetro de entrada con el puntero a la cadena a analizar y devuelve un 1 si es un palíndromo y un 0 si NO lo es. Prototipo:

  • int palindromo(*cad)

Escribir la subrutina en un fichero separado

Escribe un programa principal para probar la función palindromo, que pida al usuario una cadena e imprima un mensaje indicando si es o no un palíndromo. Esto se repite en un bucle hasta que el usuario introduzca una cadena vacía (pulsando Enter). En ese momento terminará. La cadena vacía se caracteriza porque su primer elemento es el carácter '\n'

En esta animación se muestra en funcionamiento. Se introduce las palabras "hola" y "rotor". La tercera vez se introduce la cadena vacía y el programa finaliza

Ejercicio 8

Escribir la función cifrar, cuyo primer parámetro es el puntero a una cadena y el segundo el número K a incrementar cada carácter para realizar el cifrado. Deberá estar en su propio fichero. El prototipo de la función es:

  • void cifrar(*cad, k)

Hacer un programa principal que pida al usuario una cadena, llame a la función cifrar, imprima la cadena cifrada y finalice. Deberá estar en un fichero separado

Ejercicio 9

Escribir la función upper, cuyo primer parámetro es el puntero a una cadena y no devuelve nada. Se encarga de pasar la cadena a mayúsculas. Los caracteres que no sean 'a'-'z' se dejan igual. Deberá estar en un fichero separado. El prototipo de la función es:

  • void upper(*cad)

Hacer un programa principal que pida al usuario una cadena, llame a la función upper, imprima la cadena en mayúsculas y finalice. Deberá estar en un fichero separado

Ejercicio 10

Escribir la función atoi, a la que se le pasa como parámetro el puntero a la cadena, que convierte a número y lo devuelve. Supondremos que la cadena es correcta, y que está formada sólo por los caracteres '0' - '9' (no se hará control de errores). Deberá estar en un fichero separado. El prototipo de la función es:

  • int atoi(*cad)

Hacer un programa principal que pida al usuario una cadena, llame a la función atoi, imprima el número y finalice. Deberá estar en un fichero separado

Notas para el profesor:

  • Título informal de la clase: "Divide y Vencerás"
  • En Ingeniería se usa la técnica de divide y vencerás para abordar proyectos complejos
  • Las cosas grandes se dividen en trocitos más pequeños hasta que son abordables
  • En ingeniería del software la unidad mínima para estructura el código es la función (subrutina)
  • El arte de la guerra, de Sun Tzu

Autores

Licencia

Enlaces

Página principal


Sesiones de Prácticas

P1: Simulador RARs

L1: Práctica 1-1. RARs
L2: Práctica 1-2. Ensamblador
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

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
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
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
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

SOLUCIONES

Práctica 1: Sesiones 1,2 y 3
Práctica 2: Sesiones 4, 5 y 6
Práctica 3: Sesiones 7 y 8
Práctica 4: Sesiones 9, 10 y 11
Práctica 5: Sesión 12

Clone this wiki locally