Skip to content

L3: Practica 1

javierMaciasGMAIL edited this page Sep 14, 2024 · 156 revisions

Sesión Laboratorio 3: Práctica 1-3

  • Tiempo: 2h
  • Objetivos de la sesión:
    • Aprender a manejar variables
    • Entender el segmento de datos
    • Entender las pseudoinstrucciones
    • Saber cómo leer palabras de la memoria, y almacenarlas
    • Instrucciones lw, sw
    • Pseudoinstrucciones li, mv, la

Vídeos

  • Fecha: 2019/Oct/08

Vídeo 1/4: Pseudo-instrucciones

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

Click to see the youtube video

Vídeo 2/4: El segmento de datos

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

Click to see the youtube video

Vídeo 3/4: Accediendo a una variable: Instrucciones load y store

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

Click to see the youtube video

Vídeo 4/4: Accediendo a varias variables

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

Click to see the youtube video

Contenido

Introducción

En el RISC-V todos los cálculos se hacen usando sus 32 registros internos. Sin embargo, en los programas reales tenemos muchos más datos que deben estar almacenados en la memoria. Son nuestras variables. Aprenderemos a definirlas, inicializarlas y utilizarlas

Pseudoinstrucciones li y mv

Para facilitar la labor del programador, además de las instrucciones básicas, se definen pseudoinstrucciones. Se trata de instrucciones que el programa ensamblador convierte a instrucciones reales del RISC-V. Hay dos que utilizaremos mucho: li y mv

li: Carga de un dato inmediato

Hemos aprendido que para cargar un dato en cualquier registro lo hacemos con la instrucción addi. Así por ejemplo, si queremos inicializar el registro x5 con el valor 30, hacemos:

addi x5, x0, 30   #-- x5 = 30

Esta operación de cargar datos en un registro es MUY HABITUAL. Por eso se ha creado una pseudoinstrucción para ello: li (load immediate). Tiene el siguiente formato:

El ejemplo de la asignación anterior se haría así:

li x5, 30   #-- x5 = 30

El programa ensamblador detecta que se trata de una pseudoinstrucción y la traduce a la instrucción real: addi x5, x0, 30

Introducimos el siguiente programa en el simulador. Simplemente se asigna el valor 30 al registro x5 y termina

#-- Probando la pseudoinstrucción li

	.text
	
	#-- Cargar el valor 30 en x5
	li x5, 30
	
	#-- Terminar
	li a7, 10
	ecall

Lo ensamblamos y nos fijamos en la primera instrucción. En la derecha se muestra la instrucción tal cual la hemos introducido en el editor: li x5, 30. A su izquierda tenemos la instrucción real: la que ha generado el ensamblador a partir de la pseudoinstrucción

La instrucción que se convierte a código máquina son las que se encuentran en la columna Basic: son las instrucciones reales que el RISC-V entiende

A partir de ahora usaremos siempre li para cargar valores inmediatos en los registros, pero debemos tener claro que en realidad NO es una instrucción del RISC-V, es una pseudoinstrucción

mv: Mover valores entre registros

La operación de transferir un datos de un registro a otro la realizamos con la instrucción básica add, haciendo que uno de los registros fuente sea el x0. Así, para mover el contenido del registro x5 al registro x10 haríamos:

add x10, x0, x5

Esta operación es tan frecuente, que se ha creado la pseudoinstrucción mv con ese mismo fin. Tiene este formato:

La operación anterior se realiza así:

mv x10, x5

Este programa es la continuación del anterior. Se carga el valor 30 en el registro x5, usando li, y se lleva al registro x10 usando mv:

#-- Probando la pseudoinstrucción mv

	.text
	
	#-- Cargar el valor 30 en x5
	li x5, 30
	
	#-- Llevar el valor del registro x5 al x10
	mv x10, x5
	
	#-- Terminar
	li a7, 10
	ecall

A partir de ahora, para mover contenido entre los registros, usaremos siempre la pseudoinstrucción mv

¿Por qué hay pseudo-instrucciones?

Uno de los criterios de diseño del RISC-V es el de tener el menor número posible de instrucciones. Al tener pocas instrucciones, el procesador se puede implementar usando una menor superficie de silicio, por lo que en cada oblea cabrán más procesadores, y el precio bajará

El RISC-V consigue disminuir el número de instrucciones mediante dos enfoques:

  1. Su arquitectura es modular y ampliable. El núcleo del RISC-V se denomina RV32I, y contiene lo mínimo. No hay multiplicaciones, ni divisiones, ni otras operaciones avanzadas. Pero existen extensiones, de manera que los fabricantes pueden ampliarlos

  2. Muchas instrucciones son genéricas, y agrupan varios casos particulares, dependiendo de los registros usados. Es el caso de addi y add, que nos permiten hacer transferencias entre registros, e inicializaciones, además de sumas

En el caso 2, en vez de implementar las instrucciones particulares, lo que incrementaría el tamaño del procesador, se ha optado por incluir las pseudo-instrucciones, que en realidad son para el ensamblador. Desde un punto de vista del programador, son instrucciones "normales". Pero unas se ejecutan directamente en el procesador, y otras las transforma el programa ensamblador en instrucciones reales

El segmento de datos (data)

Los programas, además del código, tienen datos. Se almacenan en una región de la memoria que se llama segmento de datos (data) que comienza a partir de la dirección 0x10010000, justo después del segmento de código. El mapa de memoria es así:

Fíjate que los datos se almacenan de forma separa al código, en zonas diferentes de la memoria. El espacio asignado para el código es de aproximadamente 252MiB (Mebibytes)

Inicializando el segmento de datos con palabras

Para colocar datos en el segmento de datos, de forma que estén almacenados para usarlos después desde nuestros programas, usamos primero la directiva .data. Con esto le decimos al ensamblador que lo que viene a continuación son datos, y que por tanto se deben almacenar en el segmento de datos

Usando la directiva .word indicamos el dato (una palabra de 32 bits) que se almacenará en esa posición

En este programa de ejemplo se inicializa la primera posición del segmento de datos con el valor hexadecimal 0xBEBECAFE

#-- Programa de ejemplo que inicializa la primera palabra del 
#-- segmento de datos a un valor

	#-- Indicar al ensamblador que lo que viene a 
	#-- continuacion esta en el segmento de datos
	.data
	
	#-- Dato a colocar en el segmento de datos
	#-- (en la primera posicion)
	.word 0xBEBECAFE

Este programa NO tiene código, por lo que NO se generará ningún código máquina. Pero tiene datos, que deberán estar en el segmento de datos. Cargamos el programa en el simulador y le damos a ensamblar

En la ventana con la información del segmento de código vemos que efectivamente NO hay nada. Está vacío. Nos fijamos en la ventana que está debajo: es la ventana del segmento de datos. La identificamos porque tiene el título Data segment

En la izquierda están las direcciones, empezando en la 0x10010000, que ya sabemos que es la dirección inicial del segmento de datos. En la derecha se encuentran las palabras (de 32 bits) almacenadas en esas posiciones de memoria. Identificamos la que hemos colocado: 0xBEBECAFE

Para introducir más palabras se pueden añadir en la misma directiva .word separas con comas o bien colocar nuevas directivas .word. Utilizaremos ambos métodos para situar cinco palabras en el segmento de datos

#-- Programa para situar 5 palabras en el segmento de datos

	.data
	
	#-- DatoS a colocar en el segmento de datos
	.word 0xBEBECAFE, 1, 2
	.word 0xFACEB00C, 0xABCDEF01

Lo ensamblamos y nos fijamos en la ventana del segmento de datos

Vemos que las palabras introducidas están situadas una a continuación de otra. Como son palabras, están en direcciones de memoria alineadas: siempre comienzan en direcciones múltiplo de 4:

En realidad la memoria se direcciona mediante bytes. ¿Qué hay almacenado en las direcciones que NO están alineadas? Se usa el mismo criterio que ya conocemos para el almacenamiento del código máquina: Little Endian: los bytes de menor peso siempre en la dirección menor (la alineada). Así, las dos primeras palabras del segmento de datos del ejemplo anterior están almacenadas de la siguiente forma:

Usando etiquetas con los datos

Para acceder a los datos necesitamos conocer la dirección en la que se encuentran. Pero en vez de utilizar números, usamos etiquetas. El ensamblador se encarga de asignar a cada etiqueta su dirección correspondiente

En el siguiente programa se declaran 5 variables, inicializadas con los valores 1,2,3,4 y 5. Para referirnos a ellas usaremos las etiquetas a, b, c, d y e respectivamente

#-- Ejemplo de uso de etiquetas en el
#-- segmento de datos

	.data
	
	#-- Almacenamos 5 variables en memoria
	#-- inicializadas con los valores 1,2,3,4 y 5
	#-- Cada variable es una palabra, almacenada en una 
	#-- direccion de memoria (alineada)
a:	.word 1
b:	.word 2
c:	.word 3
d:	.word 4
e:	.word 5

Activando la tabla de símbolos, comprobamos qué direcciones le corresponde a cada una

Dependiendo del segmento en el que se encuentren las etiquetas, el ensamblador les asigna una dirección u otra. En este programa tenemos definidas dos etiquetas, una al comienzo del segmento de datos y otra al comienzo del segmento de código

#-- Mezclando etiquetas del segmento de datos
#-- y del segmento de codigo
#-- El ensamblador asigna las direcciones a las etiquetas
#-- en función del segmento en el que están
	
	#-- Segmento de datos
	.data
	
datos:
	.word 0xCAFEBACA
	.word 0xFACEB00C
	
	#-- Segmento de Codigo
	.text
codigo:

Al ensamblar el programa vemos en la tabla de símbolos que efectivamente la etiqueta datos contiene la dirección del segmento de datos y la etiqueta código la del segmento de código

Volcado del segmento de datos a fichero

El segmento de datos también se puede volcar en un fichero, de la misma forma en la que obteníamos el código máquina. Sólo hay que seleccionar en el menú desplegable que se quiere volcar el segmento de datos. Por defecto se vuelcan los primeros 4KiB

Usando el ejemplo anterior vamos a generar dos ficheros, uno en formato hexadecimal, y otro en binario

Si editamos el fichero hexadecimal, nos aparecerán las dos primeras palabras: cafebaca y faceb00c, y el resto todo ceros. Por defecto todas las palabras del segmento de datos están a 0

Si observamos el fichero binario, desde el terminal con la utilidad hd, veremos la información byte a byte. El programa hd muestra sólo unos pocos bytes a 0, colocando un asterisco (*) para indicar que el resto son ceros

Instrucciones de acceso a memoria: load y store

Nuestros programas usan el segmento de datos para leer datos y almacenar resultados. Sólo se puede acceder a memoria usando dos tipos de instrucciones, denominadas load y store. Los valores transferidos pueden ser palabras (32 bits), medias palabras (16 bits) o bytes

  • Instrucciones de load: Se usan para leer de la memoria un valor y guardarlo en un registro
    • lw (load word): Leer una palabra
    • lh (load half-word): Leer una media palabra
    • lb (load byte): Leer un byte
  • Instrucciones store: Almacenar un registro en una posición de memoria
    • sw (store word): Almacenar una palabra
    • sh (store half-world): Almacenar una media palabra
    • sb (store byte): Almacenar un byte

La dirección a la que se accede tiene que estar situada en un registro, al que se le suma un desplazamiento (offset), que puede ser 0. Sólo se puede acceder a la memoria de esta forma (no hay más modos de direccionamiento)

Cargando la dirección en un registro: la

Tanto para leer datos de memoria (load) como para escribirlos (store) necesitamos tener en un registro la dirección de memoria a la que queremos acceder. Por ello, la primera acción será siempre cargar esta dirección en un registro. Esto lo hacemos con la pseudo-instrucción la (load address)

En este programa de ejemplo hay un dato situado en la memoria, en la dirección definida por la etiqueta dato. Esta dirección se carga en el registro x5 y se termina

#-- Ejemplo de uso de la pseudo-instruccion la
#-- Se usa para cargar una direccion en un registros
	
	#-- Segmento de datos
	.data

	#-- Dato almacenado		
dato:	.word 0xCAFEBACA
	
	#-- Segmento de Codigo
	.text

	#-- Leer la direccion del dato en el registro x5
	#-- Esta direccion está en la etiqueta dato
	la x5, dato
	
	#-- Terminar
	li a7, 10
	ecall

Al ensamblarlo y ejecutarlo obtenemos lo siguiente

Efectivamente en el registro x5 tenemos la dirección de la memoria donde se encuentra el dato: 0x10010000, que es además la dirección de comienzo del segmento de datos

¡la es una pseudo-instrucción!

Vamos a fijarnos con más detenimiento en el programa anterior. Vemos que la pseudo-instrucción la se ha traducido a ¡dos instrucciones básicas del RISC-V!

Así, si bien en el programa fuente tenemos 3 instrucciones en ensamblador, en realidad hay 4 instrucciones en código máquina. Y nuestro programa por tanto ocupa 16 bytes (el código)

La instrucción lw

Con esta instrucción leemos una palabra de la memoria y se almacena en el registro destino indicado. El formato es el siguiente:

Se accede a la dirección de memoria indicada en el registro fuente (RS) a la que se le suma un número con signo, denominado desplazamiento (offset)

Ejemplos de uso:

  ##-- Cargar en el registro x3 la palabra situada en la dirección x2 + 0
  lw x3, 0(x2)

  ##-- Cargar en el registro x4 la palabra situada en la dirección x2 + 4
  lw x4, 4(x2)

Lectura de una variable

Mientras no nos indiquen lo contrario, siempre supondremos que las variables numéricas son palabras (32 bits). Como nuestro RISC-V es de 32-bits, este es el tamaño natural de los datos. Por tanto, para leer una variable usaremos la instrucción lw

En este ejemplo leemos la variable var1 de la memoria y la situamos en el registro x6. Previamente cargamos su dirección en x5 para poder leerla

#-- Ejemplo de lectura de una variable
#-- almacenada en memoria
#-- Por defecto, si no nos dicen lo contrario, siempre
#-- usaremos variables de 32 bits (palabras)

	.data

	#-- Variable, inicializada a un valor		
var1:	.word 0xCAFEBACA
	
	.text

	#-- Leer la direccion de la variable en x5
	#-- x5 = Direccion de var1
	la x5, var1
	
	#-- Leer la variable en el registro x6
	lw x6, 0(x5)   #-- Accede a la direccion x5 + 0
	
	#-- Terminar
	li a7, 10
	ecall

Lo ensamblamos. Nos fijamos en los valores iniciales de los registros x5 y x6: ambos están a 0. También vemos que la variable var1 está en memoria y tiene el valor 0xCAFEBACA

Ahora ejecutamos el programa, bien a paso a paso o bien del tirón (porque no hay bucle infinito). Y nos volvemos a fijar en los registros x5 y x6

Efectivamente en el registro x6 tenemos el valor de la variable var1: 0xCAFEBACA. Y el registro x5 tiene su dirección. ¡Ya hemos leído nuestra primera variable! 😃

Suma de dos variables

En el RISC-V todas las operaciones aritméticas se realizan entre los registros. Por tanto, para sumar dos variables situadas en memoria primero hay que cargarlas en dos registros, y luego se suman usando la instrucción add. El resultado estará en otro registro

En este ejemplo se suman las variables a y b, inicializadas a 20 y 30. El resultado se almacena en el registro x12

        .data
  	#-- Variables	
a:	.word 20
b:      .word 30
	
	.text

	#-- Leer la variable a en el registro x10
	la x5, a  #-- Para acceder a la variable necesitamos su direccion
	lw x10, 0(x5)  #-- x10 = a
	
	#-- Leer la variable b en el registro x11
	la x5, b #-- Necesitamos la direccion de b
	lw x11, 0(x5) #-- x11 = b
	
	#-- Ahora ya podemos hacer la suma. La depositamos en x12
	add x12, x10, x11  #-- x12 = a + b
	
	#-- Terminar
	li a7, 10
	ecall

Lo ensamblamos y lo ejecutamos. Vemos que en el registro x12 tenemos la suma: 50

RESPONDE A ESTAS PREGUNTAS:

  • ¿Cuántas instrucciones tiene este programa?
  • ¿Cuántos bytes ocupa el código en memoria?
  • ¿Cuántos bytes ocupan las variables?

Suma de dos variables (Versión 2)

Este programa de la suma lo podemos simplificar. Una vez que tenemos en el registro x5 la dirección de la variable a, para acceder a la variable b basta con sumar 4 a x5. Y esto se hace directamente con la instrucción lw x11, 4(x5). El programa quedaría así:

#-- Ejemplo de suma de las variables a y b, 
#-- situadas en memoria. Version 2

	.data

	#-- Variables	
a:	.word 20
b:      .word 30
	
	.text

	#-- Leer la direccion de acceso a las variables
	#-- la variable a estará en 0(x5) y la b en 4(x5)
	la x5, a  
	
	#-- Leer la variable a
	lw x10, 0(x5)  #-- x10 = a
	
	#-- Leer la variable b
	lw x11, 4(x5)  #-- x11 = b
	
	#-- Ahora ya podemos hacer la suma. La depositamos en x12
	add x12, x10, x11  #-- x12 = a + b
	
	#-- Terminar
	li a7, 10
	ecall

RESPONDE A ESTAS PREGUNTAS:

  • ¿Cuántas instrucciones tiene este programa?
  • ¿Cuántos bytes ocupa el código en memoria?
  • ¿Cuántos bytes hemos ahorrado con respecto a la versión anterior?

La instrucción sw

Con la instrucción sw almacenamos un registro en la dirección de memoria indicada. El formato es el siguiente:

Se accede a la dirección de memoria indicada en el registro fuente (RS2) al que se le suma el desplazamiento (offset)

Ejemplos de uso:

  ##-- Almacenar el registro x3 en la dirección x2 + 0
  sw x3, 0(x2)

  ##-- Almacenar el registro x4 en la dirección x4 + 4
  sw x4, 4(x2)

Almacenando la suma de dos variables en otra variable

Completaremos el ejemplo de la suma anterior, pero almacenando el resultado en la variable suma. Así, estamos calculando la expresión: suma = a + b, donde a, b y suma son variables que están situadas en memoria

Para guardar el resultado accedemos a la posición de memoria 8(x5), ya que sabemos que ahí es donde está la variable suma. Haciéndolo de esta manera ahorramos instrucciones

#-- Ejemplo de suma de las variables a y b, 
#-- situadas en memoria. El resultado se 
#-- deposita también en memoria

	.data

	#-- Variables	
a:	.word 20
b:      .word 30
suma:   .word 0   
	
	.text

	#-- Leer la direccion de acceso a las variables
	#-- la variable a estará en 0(x5), la b en 4(x5)
	#-- y suma en 8(x5)
	la x5, a  
	
	#-- Leer la variable a
	lw x10, 0(x5)  #-- x10 = a
	
	#-- Leer la variable b
	lw x11, 4(x5)  #-- x11 = b
	
	#-- Hacer la suma y dejar el resultado en x12
	add x12, x10, x11  #-- x12 = a + b
	
	#-- llevar el resultado a la variable suma
	sw x12, 8(x5)
	
	#-- Terminar
	li a7, 10
	ecall

Lo ensamblamos. Inicialmente vemos que el registro X12 está a 0, y que en la memoria están las variables a y b con los valores de 20 y 30 respectivamente. La variable suma está también a 0

Ahora lo ejecutamos (bien paso a paso o del tirón). Comprobamos que el resultado de la suma que se ha calculado en el registro x12 se ha depositado en la variable suma en memoria

¡Ya sabemos cómo hacer cálculos con variables! 😃

Ejemplo: Rellenando la memoria con valores

Para terminar la teoría de esta sesión, vamos a hacer un programa para rellenar el segmento de datos con los números enteros: 0,1,2,3,4.... cada uno situado en una palabra de memoria. Utilizaremos un bucle infinito, por lo que para probarlo pondremos un breakpoint

El programa utiliza el registro x5 como puntero de datos: contiene la dirección de memoria en donde situar el siguiente número entero. Inicialmente apunta al comienzo del segmento de datos. Cada vez que se guarda un entero se incrementará en 4 unidades, para apuntar a la siguiente palabra

El registro x6 contiene el contador de números enteros, empezando desde 0

#-- Rellenar el segmento de datos con los valores 0,1,2,3,4....

	.data
	
#-- Direccion del segmento de datos	
inicio:

	.text
	
	#-- x5 apunta al comienzo del segmento de datos
	la x5, inicio
	
	#-- x6 es la variable contador, que se incrementa y se 
	#-- almacena en memoria
	li x6, 0
	
bucle:
	#-- Almacenar x6 en la posicion actual, apuntada por x5
	sw x6, 0(x5)
	
	#-- Incrementar el contador
	addi x6, x6, 1
	
	#-- Apuntar a la siguiente palabra de memoria
	addi x5, x5, 4
	
	b bucle

Lo ensamblamos. Nos fijamos que inicialmente en el segmento de datos está todo a 0, así como los registros x5 y x6

Situamos un punto de ruptura en la última instrucción (b bucle) y lo ejecutamos. Pasamos a vista hexadecimal

El registro x5 contiene la dirección 0x10010004, porque se ha incrementado en 4 unidades. En la dirección 0x10010000 se ha escrito un 0 (igual a lo que había inicialmente). El contador (x6) contiene el valor 1

Pasamos otra vez a la vista decimal, para ver mejor los valores. Al ejecutarlo las siguientes veces veremos cómo van apareciendo los números 1,2,3... en las sucesivas palabras del segmento de datos

En esta animación lo vemos en funcionamiento:

Usando el monitor de memoria

El monitor de memoria es una herramienta que se encuentra en el menú Tools/Memory Reference visualization

Nos permite ver los accesos que se realizan a la memoria (lecturas y escrituras). Es muy útil para comprobar si estamos accediendo bien a la memoria, o si hay regiones de la memoria a las que no se accede. Al arrancarlo aparece esta pantalla:

Lo probamos con el programa anterior del rellenado de la memoria: ensamblamos y situamos el breakpoint en la última instrucción. Pinchamos en el botón connect to Program del monitor

Ahora le damos a ejecutar, varias veces. Veremos cómo se van poniendo azules los cuadrados del monitor. El de la esquina superior izquierda se corresponde con la primera palabra del segmento de datos (dirección 0x10010000)

Cuando hemos terminado pinchamos en el botón de Close

En esta animación se muestra en 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

Para aprender es necesario practicar. Es necesario pensar. Y cuando una cosa no te salga a la primera, es el momento de volver a pensar. Y volver a intentarlo. Estos ejercicios son muy fáciles. Están pensados para que organices las ideas. Pero es necesario que los hagas. Tienes que asimilar los conceptos

En los ejercicios en los que haya que hacer un programa que termine, se deben utilizar las instrucciones:

li a7, 10
ecall

Ten en cuenta que estas instrucciones modifican el contenido del registro x17 (ya veremos por qué). Si usas el registro x17 comprueba su valor antes de llamar a estas instrucciones. Todavía no hemos visto qué significa esto (aunque lo podemos intuir), pero hay que usarla siempre para terminar nuestros programas

Cuando se pide calcular el tamaño de un programa en memoria, siempre nos referimos al código y NO a los datos. Cuando se quiera saber el tamaño de los datos se mencionará explícitamente

Ejercicio 1

  1. Escribe un programa en ensamblador (init.s) para inicializar los registros x5, x6, x7 y x8 con los valores 5, 6, 7 y 8 respectivamente. Utiliza li. Además, estos valores se deben transferir a los registros x15, x16, x17 y x18 respectivamente: x15 = x5, x16 = x6, x17 = x7 y x18 = x8. Usa mv. Por último, el programa termina

  2. ¿Cuántas instrucciones tiene el programa?

  3. ¿Cuántos bytes ocupa el programa?

  4. Obtén el código máquina exportando a un fichero en formato hexadecimal (init.hex)

Ejercicio 2

Analiza el siguiente programa

	.data
	
a:
b:
	.word 1,2,3,4
c:
	.word 10
	.word 20
d:	.word 30
e:

	.text
	
	li a7, 10
	ecall 
  1. ¿Cuántas palabras hay almacenadas en el segmento de datos?
  2. ¿Cuántos bytes ocupa el segmento de datos?
  3. ¿Cuántos bytes ocupa el programa?
  4. Escribe en una tabla las direcciones de las etiquetas:
Etiqueta Dirección
a
b
c
d
e
  1. ¿Qué palabra hay almacenada en la dirección 0x10010014?
  2. Rellena esta tabla, indicando qué bytes están almacenados en cada dirección
Dirección Byte
0x10010000
0x10010001
0x10010002
0x10010003
0x10010004
0x10010005
0x10010006
0x10010007
--------- -----
0x10010018
0x10010019
0x1001001A
0x1001001B
  1. Vuelca el segmento de datos a un fichero en formato hexadecimal

Ejercicio 3

Escribe un programa (variables4.s) que tenga un segmento de datos con 4 variables: v1, v2, v3 y v4, inicializadas con los valores 0x12345678, 0x11223344, 0xCACABACA, 0x00FABADA respectivamente. En el segmento de código sólo deben estar las instrucciones para terminar

  1. Ensambla el programa y comprueba que las palabras se encuentran en el segmento de datos
  2. Exporta el segmento de datos a un fichero hexadecimal (variables4.hex) y comprueba que estén los valores de las 4 variables
  3. Exporta el segmento de datos a un fichero binario (variables4.bin) y examina los bytes con el comando hd (o cualquier visor hexadecimal que tengas instalado en tu sistema). ¿Qué byte está almacenado en la quinta posición? ¿A qué dirección corresponde?

Ejercicio 4

  1. Escribe un programa en ensamblador (vardir.s) que defina 4 variables: a,b,c y d inicializadas a 0. Debe cargar en los registros x5,x6,x7 y x8 las direcciones de cada una de ellas usando 4 pseudoinstrucciones la

  2. Ensambla el programa. Abre la tabla de símbolos y comprueba que en los registros x5-x8 se han cargado las direcciones de las variables a-d respectivamente

  3. ¿Cuántos bytes ocupa el programa?

Ejercicio 5

  1. Modifica el programa anterior (llámalo vardir2.s) para que cargue los valores de las variables a,b,c y d en los registros x10, x11, x12 y x13. En el segmento de datos las variables deben estar inicializadas a 1,2,3 y 4 respectivamente. Ensambla el programa y comprueba que los registros x10-x13 tiene los valores 1,2,3 y 4 respectivamente

  2. ¿Cuántos bytes ocupa el programa?

Ejercicio 6

  1. Haz una nueva versión del programa anterior (vardir3.s) que haga lo mismo PERO usando sólo el registro x5 con la dirección de la variable a. Este registro NO se puede modificar, siempre debe contener la dirección de a, y es el único que se puede usar para acceder al resto de variables. En los registros x10-x13 se deben cargar los valores de las variables a,b,c y d (que valen 1,2,3 y 4, igual que en la versión anterior)

  2. ¿Cuántos bytes ocupa este nuevo programa?

  3. ¿Cuál de los dos métodos usarías para acceder a las variables si queremos que el programa ocupe el menor espacio posible en la memoria?

Ejercicio 7

  1. Escribe un programa (expresion.s) que calcule la siguiente expresión: f = (a + b + c) - (d - 3). Implementa la expresión tal cual, sin hacer simplificaciones y respetando la prioridad de los paréntesis

Las variables a,b,c,d y f están en la memoria, inicializadas con los valores -5, 2, 30, 5 y 0 respectivamente. El resultado final se debe depositar en la variable f

  1. Ensámblalo y comprueba que el valor calculado en la variable f es correcto

  2. ¿Cuántos bytes ocupa el programa?

  3. ¿En qué dirección de memoria está almacenada la variable f?

Ejercicio 8

  1. Escribe un programa (fibonacci.s) que calcule la sucesión de Fibonacci. En las variables fib0 y fib1 se deben almacenar los valores iniciales: 0 y 1 y a partir de ellos se calculan el resto de los términos de la sucesión, que se irán almacenando en palabras consecutivas de la memoria

El cálculo se realiza indefinidamente, usando un bucle infinito. Para probarlo, coloca un Breakpoint y ejecuta el programa manualmente para comprobar que en la memoria aparecen los términos de la sucesión

  1. Quita el breakpoint y dale a ejecutar... (deja configurada el simulador para funcionar a la máxima velocidad). Espera unos 10 segundos. ¿Qué sucede? ¿Sabrías explicar qué ha pasado?

Ejercicio 9

Utiliza el monitor de memoria con el programa anterior de Fibonacci. Pon un breakpoint y comprueba si se accede a todas las posiciones de memoria secuencialmente

Ejercicio 10

En un sistema con RISC-V que está funcionando en la estación espacial internacional se ha detectado la llegada de unos rayos gamas que podrían haber destruido ciertas direcciones de memoria. Necesitamos crear un programa que acceda a esas posiciones, y sólo a esas, escribiendo el valor 0xAA5555AA en ellas

Las direcciones a comprobar son 4, correspondientes a las palabras 1, 4,5 y 8 del segmento de datos. Al ejecutar el programa, se debe generar el siguiente patrón en el Monitor de memoria

Ejercicio 11

Escribe un programa que genere este patrón de acceso a memoria en el visualizar de memoria 😃

Notas para el profesor

  • Título informal de la clase: "Sumando variables..."
  • Nuestro objetivo es entender cómo se realiza la operación c = a + b, donde a,b y c son VARIABLES almacenadas en memoria
  • Ya sabemos cómo hace el procesador para calcular una expresión sencilla con variables matemáticas: hay que descomponerla en operaciones más sencillas ENTRE REGISTROS
  • ¿Y si estamos programando un juego como el Fornite donde hay que hacer cálculos con millones de datos?
  • La información no nos cabe en los registros (sólo hay 32)... necesitamos guardarlo en memoria. Son nuestras variables
  • Ahora la memoria contiene el programa y, también, los datos
  • Eso sí, los datos, para usarlos, hay que llevarlos de memoria a registros, realizar el cálculo, y situarlos de vuelta en la memoria... ¡Hay mucho trasiego de datos entre los registros y la memoria!
  • En esta sesión se introducen TODOS los conceptos necesarios para realizar esta transferencia entre memoria y registros
  • Estos mecanismos nos permite alcanzar nuestro objetivo de realizar el cálculo c = a + b. ¡Cuántas cosas ocurren para que esto se realice! ¡Cuántos conceptos hay involucrados en esta operación tan básica!

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