-
Notifications
You must be signed in to change notification settings - Fork 22
L2: Practica 1
- Tiempo: 2h
-
Objetivos de la sesión:
- Aprender cómo funciona el procesador, a nivel general
- Entender la idea de código máquina
- Comprender cómo se almacenan los programas en la memoria
- Aprender a simular programas usando puntos de ruptura
- Entender las etiquetas
- Aprender nuevas instrucciones: add y sub
- Saber cómo programar expresiones con sumas y restas en ensamblador
- Fecha: 2019/Oct/03
Haz click en la imagen para ver el vídeo en Youtube
Haz click en la imagen para ver el vídeo en Youtube
Haz click en la imagen para ver el vídeo en Youtube
Haz click en la imagen para ver el vídeo en Youtube
- Funcionamiento de un procesador
-
Actividades guiadas
- Estudiando el programa contador
- El mapa de memoria
- El segmento de código (Text)
- La directiva .text
- Obteniendo el código máquina en un fichero
- Almacenamiento de las instrucciones en memoria: Little endian
- Exportando el código máquina como fichero binario
- Etiquetas
- Sumando registros: Instrucción add
- Restando registros. Instrucción sub
- Calculando expresiones con sumas y restas
- Recopilación de instrucciones hasta el momento
- Actividades NO guiadas
- Autores
- Licencia
- Enlaces
En esta asignatura estamos aprendiendo a programar un procesador en lenguaje ensamblador, para conocer bien sus tripas. ¿Cómo funciona un procesador?
En la asignatura de electrónica digital aprendiste a diseñar y construir circuitos digitales a partir de puertas lógicas y biestables. Estos circuitos tienen existencia física. Para construirlos hay que colocar los chips y unirlos mediante cables
Los construimos con una función específica: sumar números (calculadora), contador, controlador de un robot, apertura de una cerradura mediante un código, alarma despertador...
Si queremos modificar uno de estos circuitos para que haga otra cosa, no hay más remedio que rediseñarlo. Las conexiones que tiene ya NO valen. Hay que unir los componentes lógicos de otra forma, añadir nuevos componentes, y eliminar los no usados
Se dice que estos circuitos tienen la lógica cableada. Ya está unida. No podemos modificarla sin romper el circuito. Son por tanto unas máquinas que tienen un único propósito: aquel para el que fueron construidas
Este es un ejemplo de un circuito digital, hecho con puertas lógicas y registros, cuyas uniones están en la placa (PCB). Es un transmisor-receptor digital. Si lo queremos modificar para añadir alguna funcionalidad, hay que rediseñar el circuito, y montar uno nuevo
Sin embargo, con estos circuitos lógicos podemos construir una máquina más general. Una máquina cuya función no esté determinada a priori por sus componentes internos, sino por las instrucciones que ejecute. Así, la misma máquina física funcionará de una forma u otra según el programa que ejecute. Es el mismo hardware, pero cambiando el programa, hacemos que realice funciones diferentes. ¡Bienvenidos al mundo del software!
Alan Turing bautizó a esta máquina como La máquina universal, ya que se puede programar para comportarse como se quiera
El funcionamiento del procesador es muy sencillo. Las instrucciones son números que se encuentran almacenados en una memoria, en direcciones consecutivas. El procesador lee de esta memoria una instrucción, la ejecuta y pasa a la siguiente. El proceso se repite indefinidamente: leer instrucción, ejecutar instrucción, leer instrucción, ejecutar instrucción, leer instrucción, ejecutar instrucción...
En esta animación se muestra el funcionamiento. En la memoria hay 4 instrucciones, en posiciones consecutivas: 20, 21, 22 y 23. El procesador accede a cada una de ellas secuencialmente, una detrás de otra, ejecutándolas
Sólo se muestran 4 instrucciones, pero el procesador continua ejecutando instrucciones indefinidamente. Una vez que está alimentado, nunca para. Es un circuito que está cableado para tener ese comportamiento: Ejecutar instrucciones una detrás de otra sin parar
En el procesador hay un registro interno, llamado contador de programa (PC) que contiene la dirección de memoria de la instrucción a ejecutar. En el ejemplo anterior la primera instrucción estaba en la dirección 20. Por eso el contador de programa contiene inicialmente este valor: 20. El procesador accede a la dirección de memoria indicada por el PC, la ejecuta y luego incrementa el PC para que apunte a la siguiente instrucción. El proceso se repite indefinidamente mientras el procesador esté encendido
Animación:
Hay instrucciones que cambian el valor del contador de programa, que veremos más adelante. Al cambiar este valor, el procesador ejecutará instrucciones situadas en otras posiciones de memoria
¿Y qué son las instrucciones? Son números almacenados en la memoria, que representan acciones a realizar por el procesador. Así, por ejemplo, el programa contador que hicimos en la sesión 1 del laboratorio está formado por las siguientes instrucciones situadas a partir de la dirección de memoria 0x00400000
En el RISC-V que usamos las instrucciones son de 32-bits, por lo que cada una ocupa 4 bytes. Por eso la segunda instrucción está en la dirección 0x00400000 + 4 = 0x00400004 y la tercera en la 0x00400008
Este programa de ejemplo lo forman las siguientes instrucciones: 0x00000293, 0x00128293, 0xffdff06f, y ocupa en total 12 bytes
Las instrucciones en su formato numérico, las que realmente entiende el procesador, se denominan código máquina
Entender los programas directamente en código máquina es muy complicado para los humanos. Así, por ejemplo, al editar el programa del contador anterior, directamente en código máquina, tendría esta pinta:
00000293
00128293
ffdff06f
Por ello usamos el lenguaje ensamblador. A cada instrucción del procesador le asignamos un nombre que tenga sentido, que denominamos nemónico. A cada instrucción "humana" en ensamblador, le corresponde una instrucción en código máquina
La traducción de lenguaje ensamblador a código máquina se denomina ensamblado. Y la realiza un programa denominado ensamblador.
El proceso inverso también es posible: se denomina desensamblado. A partir del código máquina se obtiene el programa en lenguaje ensamblador
Vamos a revisar todos estos conceptos usando el simulador RARs
Utilizaremos el ejemplo del programa contador, que se reproduce aquí:
#-- Programa contador
#-- El registro x5 se incrementa indefinidamente
.text
#-- Inicializar el registro x5 a 0
addi x5, x0, 0
bucle:
#-- Incrementar el registro x5 en una unidad
addi x5, x5, 1 #-- x5 = x5 + 1
#-- Repetir indefinidamente
b bucle
Lo ensamblamos. En la pestaña Execute, vemos en las dos columnas de la izquierda la dirección de cada una de las instrucciones y el código máquina correspondiente
Viendo esta información podemos responder a preguntas de este tipo:
- ¿Cuántas instrucciones en código máquina tiene este programa? Respuesta: 3
- ¿Cuánto espacio ocupa este programa en memoria? Respuesta: tiene 3 instrucciones. Cada una ocupa 4 bytes, por tanto en total ocupa: 4*3 = 12 bytes
- ¿Cuál es la dirección de la primera instrucción? Respuesta: 0x00400000
El RISC-V es de 32 bits. Eso significa que podemos acceder hasta 2 elevado a 32 direcciones, lo que nos da un total de 4GiB de almacenamiento. En cada dirección de memoria se almacena 1 byte
Siempre que trabajemos con direcciones usaremos la notación hexadecimal. Así, la dirección inicial es 0x00000000 y la final 0xFFFFFFFF (que se puede pronunciar con el sonido "fffffffff", indicando que es una dirección muy alta 😂😂😂😂)
Hay una zona especial, denominada segmento de código o segmento de texto (text). Es ahí donde se almacena nuestro programa. En el simulador RARs empieza en la dirección 0x00400000. Esa será la dirección de comienzo SIEMPRE. Existen más zonas de la memoria que iremos aprendiendo. El conjunto de todas las direcciones disponibles, y el uso que se hace de ellas, se denomina mapa de memoria
El mapa de memoria que usaremos en el RARs lo podemos ver desde en menú Settings/Memory Configuration
Se nos abre una ventana nueva con toda la información. El mapa de memoria que tenemos es el que viene por defecto (Default). En la parte inferior comprobamos que la dirección de comienzo del segmento de código (.text base address) es la 0x00400000. Además vemos las direcciones del resto de zonas, que estudiaremos más adelante
Aunque siempre utilizaremos el mapa de memoria que viene por defecto, es interesante ver que hay otras disposiciones. Esto depende del diseñador del sistema. El arquitecto software/hardware que diseña el sistema digital con microprocesador es el que establece la disposición de los elementos de la memoria. En el RARs disponemos de 3 configuraciones diferentes, aunque en este laboratorio sólo usaremos la primera
Nuestros programas se sitúan en la región de la memoria denomina segmento de código, que en el RARs comienza en la dirección 0x00400000
Es importante recalcar que las instrucciones del RISCV son de 4 bytes (32 bits) por lo que cada instrucción ocupa 4 direcciones de memoria consecutivas. Su primer byte SIEMPRE se encuentra en una posición de memoria múltiplo de 4. Esto se denomina estar alineada. Así, el último dígito hexadecimal de las direcciones donde están las instrucciones siempre acabarán en 0, 4, 8, ó C
Vamos a editar el programa contador para añadir una segunda cuenta: que el registro x6 cuente de dos en dos. Esto nos permitirá tener un programa más largo y ver mejor las direcciones de memoria donde está almacenado
#-- Programa con dos contadores
#-- El registro x5 se incrementa de uno en uno
#-- El registro x6 se incrementa de dos en dos
.text
#-- Inicializar el registro x5 a 0
addi x5, x0, 0
#-- x6 = 0
addi x6, x0, 0
bucle:
#-- Incrementar el registro x5 en una unidad
addi x5, x5, 1 #-- x5 = x5 + 1
#-- x6 = x6 + 2
addi x6, x6, 2
#-- Repetir indefinidamente
b bucle
Lo ensamblamos y echamos un primer vistazo al resultado obtenido:
Nos fijamos en el título de la ventana debajo de la pestaña Execute: dice Text Segment. Efectivamente, ahí encontraremos información sobre el segmento de código: direcciones, código máquina, instrucciones en ensamblador, etc.
Responde a las siguientes preguntas:
- ¿Cuántas instrucciones tiene ahora nuestro programa?
- ¿Cuál el programa en código máquina?
- ¿Cuántos bytes de memoria ocupa nuestro programa?
- ¿Cuál es la dirección de la primera instrucción?
- ¿Cuál es la dirección de la última instrucción?
- ¿Cuál es el último dígito hexadecimal de la dirección de cada instrucción?
Esta última pregunta es muy interesante... Vamos a fijarnos en el último dígito de la dirección de cada instrucción:
Son 0, 4, 8, C, y 0... ¡Todos múltiplos de 4! (Recordemos que la C en hexadecimal es el número 12). Esto es así porque las instrucciones del segmento de código DEBEN estar alineadas. Si alguna no estuviese en una dirección múltiplo de 4, se produciría una error de ejecución (excepción)
En los programas que hemos probado hasta ahora, además de los comentarios y las instrucciones, aparecen unas palabras de color fucsia:
Se denominan directivas. NO son instrucciones para el procesador. Es decir, no generan código máquina. Son comandos para dar información al programa ensamblador
La directiva .text sirve para indicar al ensamblador que todo lo que venga a continuación se deberá situar en el segmento de código. Vamos a probar este programa, que NO tiene instrucciones, sólo comentarios y la directiva .text
#-- Probando la directiva .text
#-- Ensamblador: A partir de aquí deberás situar todo
#-- en el segmentos de código
.text
#-- Todo lo que coloquemos aquí se insertará en el
#-- segmento de código
Ahora lo ensamblamos...
¡¡El segmento de código está vacío!!! Claro, no hay instrucciones. La directiva .text NO es una instrucción del procesador. NO genera código máquina
El código máquina de nuestros programas lo podemos exportar a un fichero. Esto es muy útil al trabajar con sistemas reales, a los que hay que cargarles el código máquina (el programa ejecutable) para que lo ejecuten
Si tenemos una placa con un procesador RISC-V podremos ejecutar nuestros programas en ella. Primero simulamos con el RARs para comprobar que todo funciona bien, luego exportamos el código máquina y lo cargamos en la placa
Para exportar el código máquina primero es necesario ensamblar el programa y luego pinchar en el botón Dump machine code en la barra superior
Nos aparecerá una ventana donde elegimos el formato para el fichero ejecutable. Vamos a seleccionar en el desplegable de la derecha el formato Hexadecimal Text. Son las instrucciones en código máquina en caracteres ASCII para poder abrirlos con un editor de textos cualquiera
Pinchamos en el botón Dump to file. Nos pedirá el nombre (y directorio) del fichero. Escribimos contador.hex y pinchamos en SAVE
Se nos genera el fichero contador.hex, en el directorio indicado. Lo podemos abrir con cualquier editor de textos. En linux podemos usar, por ejemplo, Gedit:
En esta animación se muestra el proceso completo:
¡Ya sabemos generar ficheros ejecutables, con el código máquina! 😃😃
Las instrucciones ocupan 4 bytes, y sabemos que SIEMPRE están en posiciones múltiplo de 4 (alineadas). Así, un programa con dos instrucciones se almacenará en memoria como se indica en esta figura. La primera instrucción a partir de la dirección 0x00400000 y la segunda a partir de 0x00400004:
Sin embargo, las direcciones de memorias se refieren a 1 Byte, pero las instrucciones ocupan 4 bytes. Si tomamos como ejemplo la primera instrucción, que es 0x00128293. ¿Cuál de esos cuatro bytes está almacenado en la dirección 0x00400000 y cuáles en las siguientes?
Esto es una cuestión de convenio. Cualquier orden sería válido. El criterio que usa el RISC-V es el de Little endian, que se resume así: El byte de menor peso se sitúa SIEMPRE en la dirección menor (la alineada)
Antes hemos obtenido el fichero contador.hex con las instrucciones en código máquina. Vamos a volver a exportarlo, pero un formato binario: se almacenan todos los bytes, uno detrás de otro
En el desplegable seleccionamos el formato Binary
Le indicamos que lo queremos grabar en el fichero contador.bin
Se trata de un fichero binario. Ya NO lo podemos editar con un editor de textos normal. Tendremos que usar una herramientas que nos permite ver el contenido de archivos binarios. En linux tenemos muchas, por ejemplo el GHex, o el hd en la consola
Recordamos las instrucciones que hay en el fichero contador.hex:
00000293
00000313
00128293
00230313
ff9ff06f
Ahora vemos el fichero contador.bin desde la consola de Linux, mediante el comando:
hd contador.bin
Nos saldrá esto:
Vamos a analizarlo. Nuestro programa tenía 5 instrucciones, y como cada una es de 4 bytes, en total ocupará 20 bytes. En la parte central vemos efectivamente los 20 bytes del fichero:
El tamaño del fichero binario también lo podemos comprobar ejecutando el comando:
ls -l contador.bin
Efectivamente es un archivo que contiene 20 bytes, con el código máquina de mi programa
Ahora analizamos los bytes. Buscamos las instrucciones, para ver cómo se han guardado. Nos ponemos en modo matrix. Somos Neo 😎
¡Encontramos las instrucciones! Pero vemos que están al revés. Eso es por el convenio usado: little endian. Primero está el byte de menor peso, y a continuación los siguientes. El último es el de mayor peso. Al leer el fichero binario lo estamos viendo con los pesos cambiados. Cuando lo escribimos como números empezamos escribiendo el byte de mayor peso. Pero con el convenio little endian es al revés
Al programar el procesador RISC-V, utilizamos instrucciones en lenguaje ensamblador, en vez de usar el código máquina. Somos humanos, qué le vamos a hacer 😊
Con las direcciones de memoria ocurre lo mismo. El procesador usa direcciones numéricas, pero son muy complicadas para los humanos. Por ello, cuando nos referimos a direcciones de memoria en nuestros programas en ensamblador, utilizaremos etiquetas en vez de números
En el programa de ejemplo del contador hemos usado nuestra primera etiqueta, aunque no lo sabíamos:
Las etiquetas se definen poniendo un nombre en la parte izquierda terminado en dos puntos (:). Cuando el ensamblador encuentra la definición de una etiqueta nueva, calcula la dirección de memoria de la instrucción a continuación de la etiqueta y se la asigna
La etiqueta bucle del ejemplo del contador hace referencia a la dirección de memoria donde se encuentra la instrucción addi x5, x5, 1. Vamos a ensamblar el programa y obtener empíricamente cuál es esta dirección:
Se encuentra en la dirección 0x00400008. Por tanto, la etiqueta bucle representa esta dirección
Si ahora necesitamos referirnos a esta dirección en alguna parte de nuestro programa en ensamblador, sólo hay que colocar la etiqueta sin los dos puntos al final. El ensamblador la sustituirá por la dirección correcta. Esto es lo que sucede con la última instrucción: b bucle. Ya veremos más adelante los detalles, pero esta instrucción le indica al procesador que salte (branch) a la dirección indicada por la etiqueta bucle. Es decir, que salte a ejecutar la instrucción situada en la dirección 0x00400008, que es addi x5,x5,1
En nuestros programas en ensamblador podemos definir tantas etiquetas como queramos, incluso aunque luego no las usemos. No pasa nada. Son sólo eso: etiquetas. Sustitutos humanos para las direcciones numéricas
Vamos a editar el programa del contador añadiendo varias etiquetas para saber cuáles son sus direcciones:
#-- Programa con dos contadores
#-- El registro x5 se incrementa de uno en uno
#-- El registro x6 se incrementa de dos en dos
#-- Se han añadido más etiquetas
.text
inicio: #-- Esta etiqueta tiene la direccion de la primera instrucccion
#-- (que ya sabemos que es 0x00400000)
#-- Inicializar el registro x5 a 0
addi x5, x0, 0
etiqueta1: #-- Etiqueta de prueba
#-- x6 = 0
addi x6, x0, 0
bucle:
#-- Incrementar el registro x5 en una unidad
addi x5, x5, 1 #-- x5 = x5 + 1
#-- x6 = x6 + 2
addi x6, x6, 2
etiqueta2: #-- Otra prueba
#-- Repetir indefinidamente
b bucle
fin: #-- Esta etiqueta contiene la direccion de la posicion de memoria
#-- siguiente a la instrucción b bucle
El programa en el simulador queda así:
En total se han definido 5 etiquetas: inicio, etiqueta1, bucle, etiqueta2 y fin. La única que se está usando es bucle. Las otras se han definido, pero no se usan en ninguna parte del código. Esto NO es un problema
La etiqueta inicio contiene la dirección de la primera instrucción, que sabemos que es 0x0040000. Ahora lo comprobaremos. La etiqueta etiqueta1 debe contener la de la segunda instrucción, etiqueta2 la de la última, y la etiqueta fin la siguiente dirección a la última instrucción
Para ver los valores de cada etiqueta, ensamblamos el programa y le damos a la opción Settings/Show Labels Windows
En la derecha de la ventana del segmento de código nos aparece una nueva, llamada Labels, que contiene las etiquetas que hemos definido, junto a sus direcciones equivalentes
Comprobamos que efectivamente la etiqueta inicio contiene la dirección de la primera instrucción, como esperábamos. Viendo etiqueta2, sabemos que la dirección de la última instrucción es la 0x00400010
A partir de ahora, si desde el código necesitamos acceder a alguna dirección de memoria, SIEMPRE usaremos las etiquetas. Somos humanos. Tenemos que usar las herramientas para humanos 😊
Vamos a aprender nuestra segunda instrucción: add. Con ella podemos realizar sumas entre registros. La sintaxis es:
Se asigna la suma de los dos registros fuentes al registro destino. Algunos ejemplos de uso son:
- Almacenar en el registro x5 la suma de los registros x3 y x4:
add x5, x4, x3 #-- x5 = x4 + x3
- Asignar al registro x7 el valor del registro x6
add x7, x6, x0 #-- x7 = x6 + 0 = x6
- Incrementar el registro x8 en las unidades que indica x3
add x8, x8, x3 #-- x8 = x8 + x3
- Multiplicar por dos el registro x9
add x9, x9, x9 #-- x9 = x9 + x9 = 2 * x9
Vamos a hacer un programa para calcular la suma de los números enteros: 1+2+3+4+.... Usaremos un bucle infinito como el del contador. En cada vuelta del bucle se calcula un nuevo término de la serie
El registro x5 se usa como un contador normal. En cada vuelta se incrementa en 1,2,3,4,5,... En el registro x6 se va acumulando el resultado de la suma. Inicialmente ambos registros deben valer 0
#-- Programa para calcular la suma de los números enteros: 1+2+3+4+5+...
#-- El resultado se deja en el registros 6
#-- Usamos el registro x5 como contador: 1,2,3,4,5,6...
#-- En cada vuelta sumamos el valor de x5 a la cuenta acumulada en x6
.text
#-- Inicializamos los registros x5,x6 a 0
addi x5, x0, 0
addi x6, x0, 0
#-- Bucle principal
bucle:
#-- Incrementar contador principal
addi x5, x5, 1 #-- x5 = x5 + 1
#-- Obtener termino i-simo
add x6, x6, x5 #-- x6 = x6 + x5
#-- Repetir el bucle
b bucle
Lo ensamblamos. Vamos a probarlo para comprobar que funciona. Ejecutamos paso a paso las 4 primeras instrucciones, pulsando cuatro veces seguidas el botón de paso y nos detenemos al alcanzar la instrucción b bucle
Hemos completado el cálculo del primer término de la serie. El contador (x5) vale 1, y la serie (x6) vale también 1. Hasta aquí todo correcto. Activamos la visualización de los números en decimal
Para facilitar la depuración del código, vamos establecer un punto de ruptura (Breakpoint) en el lugar en el que estamos parados ahora (la instrucción b bucle). De esta forma podemos ejecutar el programa y, cada vez que llegue a ese punto, la ejecución se detiene, permitiéndonos analizar la situación. Para activar el Breakpoint hay que pulsar sobre la casilla de verificación de la izquierda de la instrucción b bucle
Ahora pulsamos el botón de play para ejecutar el programa. Se ejecuta la siguiente pasada del bucle y se vuelve a detener en el punto en el que estábamos. En la consola aparece el mensaje execution paused at breakpoint: suma-enteros.s para indicarnos que ha alcanzado el punto de parada, y que se ha detenido
Analizamos los valores de los registros. El contador vale 2 (es el segundo término), y la suma acumulada es 3 (1 + 2 = 3). Repetimos el proceso varias veces, pulsando el botón de play hasta detenernos en un valor cualquiera, por ejemplo cuando el contador vale 10:
El resultado calculado por nuestro programa es de 55, que es correcto. Una forma rápida de hacer los cálculos para saber que es correcto es abrir un terminal de python. Yo en linux uso ipython3, que se invoca con la siguiente línea
ipython3
Hacemos el cálculo de los 10 primeros números enteros y comprobamos que es 55
De forma más genérica, podemos calcular el término n-simo, cuya fórmula es: n * (n + 1) / 2 (La buscamos en wikipedia)
Por ejemplo, calculamos el valor para n = 20:
El resultado es 210. Ejecutamos nuestro programa hasta que x5 valga 20 y comprobamos si el resultado es el correcto
¡Funciona! Siempre que hagamos un programa en ensamblador para realizar cálculos, es muy importante asegurarnos que los cálculos están bien hechos, comparándolo los resultados con una fuente fiable
En esta animación vemos el proceso completo de prueba de nuestro programa
La instrucción para restar registros tiene la misma sintaxis que la instrucción add:
Se asigna la resta de los dos registros fuentes al registro destino. Algunos ejemplos de uso son:
- Almacenar en el registro x5 la resta de los registros x3 y x4:
sub x5, x4, x3 #-- x5 = x4 - x3
- Asignar al registro x7 el valor del registro x6
sub x7, x6, x0 #-- x6 = x6 - 0 = x6
- Decrementar el registro x8 en las unidades que indica x3
sub x8, x8, x3 #-- x8 = x8 - x3
Calcularemos la serie de los enteros con signos alternados: 1-2+3-4+5-.... Usaremos el mismo enfoque que en el ejemplo anterior: un bucle infinito. En cada vuelta incrementamos el contador, hacemos la suma, volvemos a incrementar el contador y lo restamos. Usaremos el registro x5 como contador y el registro x6 es el resultado parcial de la serie
#-- Programa para calcular la serie infinita de los enteros con signos
#-- alternados: 1 - 2 + 3 - 4 + 5 -...
#-- El resultado se deja en el registros 6
#-- Usamos el registro x5 como contador: 1,2,3,4,5,6...
#-- En cada vuelta sumamos el valor de x5 y restamos x5+1 a la cuenta acumulada en x6
.text
#-- Inicializamos los registros x5,x6 a 0
addi x5, x0, 0
addi x6, x0, 0
#-- Bucle principal
bucle:
#-- Incrementar contador principal
addi x5, x5, 1 #-- x5 = x5 + 1
#-- Sumar resultado acumulado
add x6, x6, x5 #-- x6 = x6 + x5
#-- Incrementar x5 de nuevo
addi x5, x5, 1 #-- x5 = x5 + 1
#-- Restar al resultado acumulado
sub x6, x6, x5
#-- Repetir el bucle
b bucle
Igual que con el ejemplo anterior, ensamblamos y ponemos un punto de ruptura en la última instrucción (b bucle). Le damos al play. Observamos que en la primera pasada del bucle, donde se realiza el cálculo x6 = 1 - 2, el resultado es efectivamente -1
Realizamos 10 iteraciones, por ejemplo (hasta que x5 valga 20, porque se incrementa de 2 en 2 en cada pasada). Obtenemos el resultado -10
Comprobamos con el ipython3 que el resultado es correcto, evaluando esta expresión:
1-2+3-4+5-6+7-8+9-10+11-12+13-14+15-16+17-18+19-20
El resultado es el esperado: -10
Con las tres instrucciones que ya conocemos: addi, add y sub, podemos implementar programas en ensamblador que calculen el valor de expresiones genéricas con sumas y restas, como por ejemplo esta:
f = (a + b) - c + d + 50
donde las letras a, b, c, d y f representan variables, siendo f en la que se almacena el resultado
Lo primero que hacemos es elegir qué registros van a representar a cada variable, incluida la del resultado final f:
Suponemos que esos registros ya tiene los valores de las variables (los que sean) y ahora realizamos el cálculo. Un trozo de código que realiza este cálculo es el siguiente: (¡aunque hay muchas formas diferentes de hacerlo!):
#----- Codigo para calcular la expresion generica
#---- x10 = (x5 + x6) - x7 + x8 + 50
#-- Calculamos f = (a + b)
add x10, x5, x6
#-- Calculamos (-c + d). Lo almacenamos por ejemplo en x9
sub x9, x8, x7 # x9 = d - c
#-- Calculamos (-c + d + 50)
addi x9, x9, 50
#-- Calculamos la expresion final: (a + b) + (-c + d + 50)
add x10, x10, x9
#-- Terminar
li a7, 10
ecall
Para probarlo le damos valores conocidos a los registros x5, x6, x7, x8. Por ejemplo 1,2,3 y 4 respectivamente. Luego realizamos el cálculo y observamos el resultado final en el registro x10, que debe ser 54
El programa completo es el siguiente:
#-- Programa para calcular la expresion:
#-- f = (a + b) - c + d + 50
#-- Suponemos lo siguiente:
#-- x5 = a, x6 = b, x7 = c, x8 = d
#-- El resultado final se guarda en x10
.text
#--- Dar un valor a las variables (puede ser cualquiera)
#-- para comprobar que el cálculo funciona
addi x5, x0, 1
addi x6, x0, 2
addi x7, x0, 3
addi x8, x0, 4
#----- Codigo para calcular la expresion genérica
#---- x10 = (x5 + x6) - x7 + x8 + 50
#-- Calculamos f = (a + b)
add x10, x5, x6
#-- Calculamos (-c + d). Lo almacenamos por ejemplo en x9
sub x9, x8, x7 # x9 = d - c
#-- Calculamos (-c + d + 50)
addi x9, x9, 50
#-- Calculamos la expresion final: (a + b) + (-c + d + 50)
add x10, x10, x9
#-- Terminar
li a7, 10
ecall
Como este programa no tiene bucles infinitos, lo podemos ejecutar directamente. El resultado que nos calcula es 54
Con el ipython3 comprobamos si este resultado es correcto. Lo hacemos con estos comandos:
a,b,c,d = 1,2,3,4
f = (a + b) - c + d + 50
- Instrucciones básicas: Son las que se transforman a código máquina y que ejecuta el procesador
- Directivas: Dar información al programa ensamblador. No generan código máquina
Es el momento de que practiques los conceptos. Resuelve los siguientes ejercicios utilizando sólo las instrucciones en ensamblador que conocemos: add, sub, addi y b. Como ocurre siempre en programación, la solución de muchos ejercicios no es única. Se puede implementar de múltiples formas
Abre en el simulador el programa de la suma de los números enteros: suma-enteros.s Responde a las siguientes preguntas:
- ¿Cuántas instrucciones en código máquina tiene?
- ¿Cuántos bytes de memoria ocupa el programa?
- Completa esta tabla, indicando la instrucción en código máquina y su dirección (alineada)
Dirección (alineada) | Instrucción en código máq. |
---|---|
0x0040000 | |
0x0040004 | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x |
- Completa la tabla indicando qué byte del programa está almacenado en qué dirección
Dirección | Byte |
---|---|
0x0040000 | |
0x0040001 | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x | |
0x |
- Simula el programa y obtén el valor de la suma de los 25 primeros números enteros. Coloca un Breakpoint en la instrucción de salto b para que pare en cada iteración. Dale al botón de play hasta llegar al valor pedido
-
Modifica el programa de la suma de los números enteros (suma-enteros.s) para realizar la suma de los números pares: 2 + 4 + 6 + 8 + 10 + ....
-
¿Cuántas instrucciones en código máquina tiene?
-
¿Cuántos bytes de memoria ocupa el programa?
-
Completa esta tabla, indicando la instrucción en código máquina y su dirección (alineada)
Dirección (alineada) | Instrucción en código máq. |
---|---|
0x0040000 | |
0x | |
.... |
- Completa la tabla indicando qué byte del programa está almacenado en qué dirección
Dirección | Byte |
---|---|
0x0040000 | |
0x0040001 | |
0x | |
0x | |
.... |
- Simula el programa y obtén el valor de la suma de los 25 primeros números pares. Coloca un Breakpoint en la instrucción de salto b para que pare en cada iteración. Dale al botón de play hasta llegar al valor pedido
-
Escribe un programa para calcular la sucesión de fibonacci: 0,1,1,2,3,5,8,13,21,34,55... El programa es similar al de la suma de los números enteros: un bucle infinito. Dejar el resultado de cada iteración en el registro x7
-
Simula el programa poniendo un Breakpoint para que se pare en cada iteración del bucle. Obten el valor del término f(10) de fibonacci
-
¿Cuántas instrucciones en código máquina tiene?
-
¿Cuántos bytes de memoria ocupa el programa?
-
Completa esta tabla, indicando la instrucción en código máquina y su dirección (alineada)
Dirección (alineada) | Instrucción en código máq. |
---|---|
0x0040000 | |
0x | |
.... |
- Completa la tabla indicando qué byte del programa está almacenado en qué dirección
Dirección | Byte |
---|---|
0x0040000 | |
0x0040001 | |
0x | |
0x | |
.... |
Escribe un programa en ensamblador para calcular la expresión:
f = (a + b + c) - [(d - a) + (e + 3) - 30]
Implementa la expresión tal cual, sin aplicar simplificaciones ni cálculos previos
Comprueba el resultado para los valores de las variables: a=2, b=4, c=6, d=8, e=10
- Obtén la expresión general que calcula el siguiente programa en ensamblador
.text
addi x5, x0, 3
addi x6, x0, 5
addi x7, x0, 7
addi x8, x0, 9
sub x9, x7, x8
add x6, x6, x6
add x6, x6, x6
sub x9, x6, x9
add x5, x5, x5
add x5, x5, x9
addi x10, x5, 15
#-- Terminar
li a7, 10
ecall
-
¿Para qué valores se prueba?
-
¿Cuánto vale el resultado
-
¿Cuál es el valor del byte de la posición de memoria 0x00400015
-
¿Cuántos bytes de memoria ocupa este programa?
- ¿Cuál es el código máquina de estas dos instrucciones?
addi x0, x0, 0
addi x0, x0, 0xFA
-
Analiza la diferencia entre ellos. A partir de esos código máquina, ¿Cuál crees que sería el código máquina de la instrucción addi x0, x0, 0xBB? Intenta obtener el código máquina por deducción, y luego compruébalo con el ensamblador
-
Obtén el código máquina de estas instrucciones. Escríbelas en binario y marca los bits que sean diferentes en ambas
addi x0, x1, 0
addi x0, x2, 0
- Repite el punto anterior pero con estas dos instrucciones. ¿Qué bits son diferentes?. ¿Podrías deducir en qué bits de la instrucción en código máquina se codifica el número de registro?
addi x0, x0, 0
addi x31, x0, 0
¿Qué dirección le corresponde a cada etiqueta en este programa? Completa la tabla
.text
ini:
a:
addi x5, x5, 3
addi x6, x6, 5
addi x7, x7, 7
b:
addi x8, x8, 9
sub x9, x7, x8
add x6, x6, x6
add x6, x6, x6
c:
sub x9, x6, x9
add x5, x5, x5
add x5, x5, x9
d:
addi x10, x5, 15
li a7, 10
fin: ecall
last:
Etiqueta | Dirección |
---|---|
ini | 0x |
a | 0x |
b | 0x |
c | 0x |
d | 0x |
fin | 0x |
last | 0x |
Sabiendo que el PC tiene el valor 0x0040000C, ¿qué instrucción de este programa es la siguiente que se va a ejecutar?
.text
ini:
a:
addi x5, x5, 3
addi x6, x6, 5
addi x7, x7, 7
b:
addi x8, x8, 9
sub x9, x7, x8
add x6, x6, x6
add x6, x6, x6
c:
sub x9, x6, x9
add x5, x5, x5
add x5, x5, x9
d:
addi x10, x5, 15
li a7, 10
fin: ecall
last:
Sabiendo que la etiqueta b tiene el valor 0x00502020. ¿Cuál es el valor del resto de etiquetas de este fragmento de programa?. Completa la tabla
#----- Fragmento de codigo (no es el principio)
b:
addi x8, x8, 9
sub x9, x7, x8
add x6, x6, x6
add x6, x6, x6
c:
sub x9, x6, x9
add x5, x5, x5
add x5, x5, x9
d:
addi x10, x5, 15
li a7, 10
fin: ecall
Etiqueta | Dirección |
---|---|
b | 0x |
c | 0x |
d | 0x |
fin | 0x |
Escribe un programa en ensamblador que calcule esta expresión:
f = (d - c) + 15 - (a + b)
para los siguientes valores de las variables:
- a = 1, 2, 3, 4, 5....
- b = 2, 3, 4, 5, 6 ....
- c = 3, 4, 5, 6, 7 ....
- d = 4, 5, 6, 7, 8 ....
Así, en la primera iteración se calcula f para los valores: a=1, b=2, c=3, d=4. En la siguiente para a=2, b=3, c=4, d=5...
- ¿Cuánto vale la expresión en la iteración 10? Simúlalo con breakpoints para comprobarlo
- Título informal de la clase: "Calculando expresiones"
- Queremos realizar el siguiente cálculo:
f = (a + b) - c + d + 50
. Lo expresamos en lenguaje matemático. a,b,c,d,f son variables matemáticas - Si usamos lenguaje de alto nivel, como python, la expresión se escribe tal cual. Y una vez asignados valores a las variables se realiza el cálculo
- Sin embargo, el procesador no es capaz de entender esa expresión. Hay que dividirla en trocitos más pequeños. Hay que "descuartizarla"
- ¿Quién es el descuartizador? Típicamente el compilador... pero como estamos a bajo nivel, seremos nosotros los que hagamos esa función
- En la clase se introducen todos los conceptos previos para lograr el objetivo de entender cómo implementar una expresión de ese tipo
- Katia Leal Algara
- Juan González-Gómez (Obijuan)
L1: Práctica 1-1. RARs
L2: Práctica 1-2. Ensamblador
L3: Práctica 1-3. Variables
L4: Pract 2-1. E/S mapeada
L5: Práctica 2-2: Inst. ecall
L6: Prác 2-3: Cadenas
L7: Práct 3-1: Bucles y saltos
L8: Práct 3-2: Cadenas II
L9: Pract 4-1: Subrut. Nivel-1
L10: Pract 4-2: La pila
L11: Pract 4-3: Recursividad
L12: Pract 5-1. Heap. Listas
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
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