Prof. Edgardo Gho, UNLaM
Ver enYouTubeCapítulo 2
Unidad 0: Assembly RISC-V
Video de la cátedra de Arquitectura de Computadoras (UNLaM) sobre programación en assembly RISC-V. El profesor Edgardo Gho recorre la organización de la memoria de un programa, el trabajo con registros e inmediatos, las operaciones aritméticas y lógicas, el acceso a memoria y a vectores, la entrada y salida con ecall, los saltos, las subrutinas y las convenciones de llamada de RISC-V. Para seguir la práctica, el simulador de LYSZT ejecuta el mismo conjunto de instrucciones RV32IM, con lecciones guiadas en español y ejercicios que se corren en el navegador.
Temas
La organización de la memoria de un programa
- Los compiladores y ensambladores separan el programa en secciones: .text almacena las instrucciones, .data los datos inicializados y .bss los datos no inicializados.
- El stack es una memoria tipo pila para datos temporales, que se recuperan en el orden inverso al que entraron; por ejemplo, los argumentos que una función en una PC x86 recibe por el stack.
- El heap es el espacio para la memoria dinámica, por ejemplo cuando se llama a malloc y el tamaño se define en tiempo de ejecución.
- En Ripes, .data y .bss son equivalentes; el curso trabaja exclusivamente con .text y .data.
Registros e inmediatos
- Para cargar el valor 128 en t0 se usa addi t0, x0, 128: como x0 siempre vale 0, sumarle 128 deja el 128 en el destino.
- El formato tipo I soporta inmediatos de 12 bits con signo, así que el rango utilizable es de -2048 a 2047.
- Para la parte alta de un registro se usa lui (formato tipo U), que carga los 20 bits más significativos y completa el resto con ceros, por ejemplo 0x12345000 en x5.
- Queda como ejercicio combinar lui y addi para asignar 0x87654321, y comparar el resultado con la pseudoinstrucción li, que carga un número de 32 bits y el ensamblador la traduce a las instrucciones equivalentes.
Operaciones aritméticas y lógicas
- li se traduce a la instrucción que exista en el set; cargar 2047 solo necesita addi porque cabe en 12 bits con signo, y el decremento se hace sumando -1 con addi.
- Para negar un número se usa el formato tipo R: sub x4, x0, x5 calcula 0 menos x5, y el resultado queda en complemento a dos.
- El formato R siempre es rd = rs1 operación rs2: sumas, restas, slt que graba 1 o 0 según qué registro es menor (con su versión sin signo sltu), desplazamientos lógicos y aritméticos, y las lógicas and, or y xor.
Acceso a la memoria de datos
- Las variables se declaran en .data con nombre, formato y valor inicial: .word para 32 bits, .half para 16 y .byte para 1.
- la (load address) es una pseudoinstrucción que resuelve la dirección de una variable; el ensamblador acomoda las secciones en rangos consecutivos de direcciones y la traduce a instrucciones relativas al PC.
- lw lee una palabra con el modo base más desplazamiento, escribiendo el registro destino y entre paréntesis el registro base y el offset (0 para el inicio de una variable).
- sw escribe en memoria de la misma forma: el ejemplo lee una variable con lw, le suma 1 al valor y la guarda de vuelta con sw; queda como práctica el mismo ciclo con lb y lbu, que leen un solo byte.
Vectores
- Para acceder al elemento que indica un índice almacenado en memoria, primero se lee el índice con la y lw.
- Como cada palabra ocupa 4 bytes, el offset se calcula multiplicando el índice por 4; sin instrucción de multiplicar, se usa un desplazamiento a la izquierda de 2 (slli), que duplica dos veces.
- La dirección final es la base del vector más el offset, y de ahí se lee el elemento con lw.
Entrada y salida con ecall
- Ripes provee funciones de soporte como si fuera un sistema operativo, que se invocan con la instrucción ecall.
- El número de operación va en a7 y los argumentos en a0: con 4 se imprime el string cuya dirección está en a0 (terminado en cero), y con 1 se imprime el entero contenido en a0.
- No existen printf ni scanf; todo el acceso a la consola pasa por estas llamadas.
Saltos y bucles
- Las etiquetas marcan posiciones del programa para referenciarlas desde las instrucciones de salto.
- Los saltos condicionales, como bge, saltan si la condición se cumple y si no continúan con la instrucción siguiente.
- El ejemplo imprime los elementos de un vector hasta encontrar uno negativo, con un bucle que imprime con ecall, avanza el puntero de a 4 bytes y repite mientras el valor leído sea mayor o igual a cero.
- El ensamblador calcula el offset del salto como la diferencia entre la dirección de la instrucción y la de la etiqueta: negativa hacia atrás, positiva para esquivar código.
Subrutinas
- Una subrutina es un segmento de código con etiqueta, reutilizable, que recibe datos y devuelve un resultado.
- jal (jump and link) salta a la etiqueta y guarda en el registro indicado la dirección de retorno; por convención se usa x1 (ra).
- ret es la pseudoinstrucción que retorna con esa dirección, y el ejemplo min compara a0 y a1 con blt y devuelve el menor en a0.
- El programa termina con un bucle infinito, un beq de x0 consigo mismo, como un while(1).
Convenciones de llamada
- Las convenciones de llamada son los acuerdos entre quien escribe una subrutina y quien la usa, sobre cómo se pasan argumentos y se devuelven resultados.
- RISC-V define los registros a0 a a7 para pasar argumentos (y el stack si hacen falta más) y a0, con a1 para resultados de más de 32 bits, para devolverlos.
- Registros caller-saved: quien llama debe guardarlos antes porque la subrutina puede destruirlos; son ra, los temporales t0 a t6 y los argumentos a0 a a7.
- Registros callee-saved: la subrutina debe preservarlos y restaurarlos antes de retornar; son el stack pointer y los s0 a s12.
- El global pointer y el thread pointer los usan los compiladores y no se definen en la convención, y x0 nunca necesita guardarse porque siempre vale cero.
