Saltar a contenido

Lab 8 (RISC-V)

En este laboratorio van a programar en lenguaje ensamblador para practicar y reforzar los conocimientos que adquirieron durante CC3.

Antes de empezar, vamos a obtener los archivos necesarios desde Github Classroom:

https://classroom.github.com/a/Ye5uh7oc

Jupiter

Jupiter ya viene instalado en el contenedor que estamos usando. Para ejecutar un archivo utilice:

jupiter archivo.s

Nota: cuando queremos ejecutar algún programa de COOL que usamos jupitercl archivo.s, para el laboratorio usamos jupiter archivo.s sin el cl, pronto veremos en clase por qué

Recordando RISC-V

  • Los programas de RISC-V van en un archivo con extension .s.
  • Los programas deberían de llevar un label global __start que se utilizará como punto de inicio.
  • Los programas deberían de terminar de la siguiente manera:
li a0, 10
ecall
  • Las etiquetas o labels terminal con dos puntos.
  • Los comentarios comienzan con un numeral o con punto y coma.
  • No pueden poner más de una instrucción por línea.

Instrucciones importantes

Uno de los requisitos de CC4 es dominar los temas de CC3, incluyendo programar en lenguaje ensamblador. RISC-V es una arquitectura RISC por lo cual es muy fácil de utilizar. Algunas instrucciones que deberían conocer hasta el momento son:

# carga el contenido de memoria en la direccion (pc + 8) y lo guarda en t1
lw t1, 8(sp)
# guarda a0 en memoria en la direccion (pc + 8)
sw a0, 8(sp)

# guarda el inmediato 5 en el registro t1
li t1, 5

# guarda la direccion de foo en el registro t1
la t1, foo

# suma los registros t1 y t2 y el resultado lo guarda en t3
add t3, t1, t2
# suma el registro sp con el inmediato 4 y guarda el resultado en sp
addi sp, sp, 4

# salta a la etiqueta label
j label
# salta a la etiqueta label y guarda en el registro ra PC + 4
jal label
# salta a la direccion contenida en el registro ra
jr ra

# si t1 y t2 son iguales, realizar salto hacia foo
beq t1, t2, foo

Cuando realizamos llamadas a funciones en assembler debemos ser cuidadosos de no perder las direcciones de retorno. Este y otros datos deben ser guardados en el stack al inicio de la llamada y restaurados cuando esta termina.

El convenio de RISC-V que conocimos en CC3 el siguiente:

  • Los registros aX se utilizan como argumentos cuando se manda a llamar a una función.
  • El registro a0 se utiliza para devolver resultados.
  • Los registros tX se utilizan como temporales, cuyo valor puede perderse entre llamadas.
  • Los registros sX sobreviven a llamadas.
  • El registro sp es el puntero hacia el stack.
  • El registro ra contiene la dirección de retorno (pc + 4).

Este convenio cambiará en CC4 pues usaremos un accumulator machine, pero aún nos sirve para este laboratorio.

Veamos un ejemplo sencillo de un ciclo en RISC-V:

.text
.globl __start           # indicamos que __start es global y punto de partida

__start:
    li t0, 0             # i
    li t1, 10            # max
cond:
    bge t0, t1, endLoop  # terminamos si i >= max
body:
    mv a1, t0            # movemos t0 a a1 para imprimirlo
    li a0, 1             # codigo ecall para imprimir un entero
    ecall                # imprimimos i
step:
    addi t0, t0, 1       # step del loop
    j cond               # salto hacia la condicion
endLoop:
    li a0, 10            # codigo ecall para salir de un programa
    ecall                # salimos del programa

Veamos ahora un programa con llamadas recursivas:

.data
    msg: .string "El resultado es: "

.text
.globl __start            # indicamos que __start es global y punto de partida

__start:
    li a0, 5              # queremos calcular factorial de 5
    jal factorial         # llamamos a factorial
    mv s0, a0             # guardamos el resultado en s0
    
    la a1, msg            # cargamos la direccion de msg en a1
    li a0, 4              # codigo ecall para imprimir un string
    ecall                 # imprimimos el string
    
    mv a1, s0             # movemos s0 a a1 (que tenia el resultado de factorial)
    li a0, 1              # codigo ecall para imprimir un entero
    ecall                 # imprimimos el entero
    
    li a0, 10             # codigo ecall para salir del programa
    ecall                 # salimos del programa

factorial:
    # a0 trae el resultado
    # ra tiene la direccion de retorno
    # sp apunta hacia el tope del stack
    bne a0, x0, notZero    # si a0 != 0 saltar a notZero
    li a0, 1               # de lo contrario devolver 1
    jr ra                  # saltar a la direccion de retorno

notZero:
    addi sp, sp, -8        # protegemos algunos valores en el stack (2 words)
    sw s0, 0(sp)           # guardamos s0 porque lo vamos a utilizar en la funcion
    sw ra, 4(sp)           # guardamos ra para no perder la direccion de retorno
    
    mv s0, a0              # guardamos a0 en s0 para no perderlo
    
    addi a0, a0, -1        # decrementamos a0 para la siguiente llamada: fact(n - 1)
    jal factorial          # llamamos recursivamente a factorial
    
    mul a0, a0, s0         # efectuamos n * fact(n - 1)
    
    lw s0, 0(sp)           # restauramos s0
    lw ra, 4(sp)           # restauramos ra
    addi sp, sp, 8         # liberamos el espacio
    
    jr ra                  # saltamos a la direccion de retorno

Para el laboratorio realizaremos dos ejercicios, ambos utilizan recursión. La recursión es importante en ensamblador pues nos transmite la idea de al finalizar una tarea, el stack debe quedar tal como llegó originalmente.

Ejercicio 1: fibonacci.s

En un archivo llamado fibonacci.s implementen la función de fibonacci en lenguaje ensamblador RISC-V.

Ejercicio 2: hanoi.s

Otra función recursiva, traduzca a RISC-V el siguiente código que sirve para resolver el juego de torres de Hanoi.

void hanoi(int numeroDeDiscos, int T_origen, int T_destino, int T_alterna) {
    if (numerodeDiscos == 1) {
        printf("mueva el disco de la torre: ");
        printf("%i", T_origen);
        printf(" hacia la torre: ");
        printf("%i\n", T_destino);
    } else {
        hanoi(numeroDeDiscos - 1, T_origen, T_alterna, T_destino);
        hanoi(1, T_origen, T_destino, T_alterna);
        hanoi(numeroDeDiscos - 1, T_alterna, T_destino, T_origen);
    }
}