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

Jupiter

Si está utilizando un Linux distinto al nuestro, instale Jupiter:

sudo add-apt-repository ppa:andrescv/jupiter
sudo apt-get update
sudo apt-get install jupiter

Pueden correr Jupiter de forma gráfica utilizando lo siguiente:

jupiter

o en modo línea de comandos utilizando lo siguiente:

jupiter [options] <files>

las opciones disponibles son las siguientes:

[General Options]
  -h, --help               show Jupiter help message and exit
  -v, --version            show Jupiter version
  -l, --license            show Jupiter license

[Simulator Options]
  -b, --bare               bare machine (no pseudo-instructions)
  -s, --self               enable self-modifying code
  -e, --extrict            assembler warnings are consider errors
  -g, --debug              start debugger
      --start <label>      set global start label (default: __start)
      --hist <size>        history size for debugging

[Cache Options]
  -c, --cache              enable cache simulation
      --assoc <assoc>      cache associativity as a power of 2 (default: 1)
      --block-size <size>  cache block size as a power of 2 (default: 16)
      --num-blocks <num>   number of cache blocks as a power of 2 (default: 4)
      --policy <policy>    cache block replace policy (LRU|FIFO|RAND) (default: LRU)

[Dump Options]
      --dump-code <file>   dump generated machine code to a file
      --dump-data <file>   dump static data to a file

La documentación de Jupiter la pueden encontrar en el siguiente link.

Detalles de RISC-V

  • Los programas de RISC-V van en un archivo de texto 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 # codigo 10: exit
ecall     # llamada al entorno
  • 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.

Recordatorio de Assembler

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 es el siguiente:

  • Los registros aX se utilizan como argumentos cuando se manda a llamar a una función.
  • Los registros aX se utilizan como valores de retorno de las funciones.
  • 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).

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:

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

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);
    }
}