Adrianistán

La belleza de MIPS

03/05/2017
Todos los ordenadores, móviles y en general, cualquier dispositivo que lleva software necesita un procesador. Los procesadores se agrupan por familias, familias de procesadores que se programan igual, en un lenguaje llamado ensamblador. La más popular es Intel x86, presente en cualquier PC y en algunos móviles, tablets y servidores. Pero no voy a hablaros hoy de x86, ni de ARM, sino de MIPS. El ensamblador hecho bello. Adentrémonos en este mundo. Si nunca has visto ensamblador, este es tu momento. Si ya lo has visto, quizá te apetezca recordar algunas cosas.

MIPS, reinventemos la rueda... pero bien


Los orígenes de la arquitectura MIPS se remontan a 1981 cuando John L. Hennessy y su equipo de la Universidad de Stanford buscan implementar un procesador lo más simple posible. Al contrario que en las arquitecturas de tipo CISC lo que se busca en una de tipo de RISC como MIPS es definir las instrucciones más simples posibles y optimizar estas. ARM es otro ejemplo de arquitectura RISC. Para optimizar aún más, MIPS hace un uso intensivo de la segmentación.

Los procesadores MIPS ganaron popularidad rápidamente. Fueron usados por Silicon Graphics (SGI) para sus workstations con sistema operativo IRIX. Allí fueron usadas en estudios de Hollywood, centros científicos, etc En su máximo esplendor estas máquinas acogieron grandes avances, como la invención de OpenGL y del sistema de archivos XFS.

Esta relación de MIPS con el mundo multimedia le hizo popular entre las consolas. La Nintendo 64, la PlayStation 2 o la PSP llevan procesador MIPS.



Finalmente MIPS ha entrado en cierta decadencia. SGI cerró y las consolas decidieron apostar por otro tipo de procesadores. Sin embargo MIPS sigue vivo. Es usado en routers y algunos móviles. Existe Loongson, una variación de la aquitectura MIPS usada en China. Incluso ha habido placas estilo Raspberry Pi con MIPS, como por ejemplo, la Creator CI20.

Cuatro principios del diseño


Estos principios se aplican en MIPS.

Tres tipos de instrucciones


La belleza de MIPS se basa en su diseño, claro, conciso y con pocas excepciones. Empecemos por lo básico, en MIPS todas las instrucciones son de 32 bits y son de una de estas tres categorías:

32 registros de 32 bits


En MIPS existen 32 registros. Bastantes si los comparamos con x86 de Intel. ¿Qué es un registro? Es donde se guardan los datos que se van a necesitar en las operaciones. Como si fueran cajas donde podemos guardar cosas, las cajas de la CPU. Salvo un par de ellos especiales, en realidad todos son iguales, no obstante, por convenio ciertos registros se usan para unas cosas y otros para otras.

Tomado de Wikipedia

Operaciones básicas


Las operaciones básicas son la suma, la resta, la suma inmediata, los desplazamientos, las operaciones lógicas and, or y nor, las operaciones de carga y guardado en memoria, el comparador menor que, los branch y los saltos. Y ya esta. Todo lo demás se implementa mediante pseudoinstrucciones, es decir, instrucciones que el ensamblador convierte en varias instrucciones básicas. Muchos se habrán sorprendido que no haya operación de multiplicar. O que no haya move. O que solo se pueda comparar con menor que. Todas esas cosas las podemos hacer pero debemos ser conscientes de que son pseudoinstrucciones y MIPS en realidad no tiene esas operaciones registradas. Veamos un ejemplo de código MIPS. Primero tenemos el código en C equivalente, y después el código MIPS.


a = b + c + 5




# a en $s0, b en $s1, c en $s2
add $s0, $s1, $s2 # Sumamos $s1 + $s2 y ponemos el resultado en $s0
addi $s0, $s0, 5 # Sumamos a $s0 la constante 5 y dejamos el resultado en $s0


Por norma general en MIPS cuando queramos usar una constante en vez de un registro bastará con poner una i (de inmmediato) al final. Así en vez de usar una instrucción de tipo R usaremos una de tipo I.

Branch y jump


La magia de los computadores reside en que son capaces de tomar decisiones. Pueden ejecutar bucles y evaluar condiciones. En ensamblador este tipo de cosas se hacen con saltos. Sin embargo MIPS incluye dos tipos distintos de saltos.

Los branch evalúan condiciones y saltan en caso de que se cumpla a una posición del código N bytes por arriba o por debajo de la dirección de memoria actualmente almacenada en $ra.

Los jump saltan a una dirección de memoria de forma absoluta. Como MIPS es de 32 bits y las instrucciones también son de 32 bits resulta evidente que no es posible saltar a cualquier dirección de memoria, no al menos en una simple instrucción. Los bits que faltan, los más significativos se dejan a los mismos que había en donde se hizo el jump.

Esto parece muy complicado pero veamos que no lo es tanto en la práctica. Como recomendación, usa jumps para saltar a subrutinas y usa branch para hacer ifs y bucles.


int x = 5;
do{
x--;
}while(x!=0);




# x en $s0
.text
main:
addi $s0, $zero, 5 # Cargamos 5 a $s0
# es equivalente a la pseudoinstrucción li $s0, 5
bucle:
addi $s0, $s0, -1 # las constantes también pueden ser negativas
bne $s0, $zero, bucle # branch on not equal. Si $s0 != $zero, se salta a bucle. Si no, se sigue para abajo




int double(int x){
return x*2;
}
...
int y = 5;
y = double(y);




.text
main:
addi $s0, $zero, 5
add $a0, $s0, $zero #podriamos usar la pseudoinstruccion move
jal double
move $s0, $v0 #pseudoinstruccion para copiar de $v0 a $s0
li $v0, 10
syscall # ejecuta una syscall, con código el que hay en $v0 (10 vamos a suponer que es salir del programa)
double:
sll $v0, $a0, 1
jr $ra


¿Qué hace jal? Llama a la subrutina double ejecutando un salto y poniendo $ra a la siguiente dirección (para retomar el ciclo de ejecución normal cuando salgamos de la subrutina con jr $ra).

Cargando datos


Hasta ahora hemos trabajado siempre con datos que ya estaban en los registros. ¿Qué pasa si queremos recorrer un array por ejemplo? Un array es un tipo de dato más complejo, que no entra en un registro. Este ejemplo suma los números del array.


int* x = [1,2,3,4,5];
int suma = 0;
for(int i =0;i<4;i++){
suma = suma + x[i]
}




.data
x: .word 1,2,3,4,5
.text
main:
li $s0, 0
li $t9, 5
la $t0, x
bucle:
lw $t1, 0($t0)
add $s0, $s0, $t1
addi $t0, $t0, 4
addi $t9, $t9, -1
bne $zero, $t9, bucle
li $v0, 10
syscall


Usamos la cabecera data para registrar datos en el stack que el sistema operativo repartirá como pueda. Le llamamos x y contiene WORDS, que en MIPS son 4 bytes, es decir, 32 bits.

la es una pseudoinstrucción (usa lui y ori internamente) que sirve para cargar direcciones de 32 bits en un registro. En este caso cargamos la posición de donde empieza el vector x en $t0. $t0 es un puntero ahora.

lw es la instrucción importante aquí, significa load word y permite cargar palabras de la memoria a un registro. Le indicamos donde queremos que se guarde ($t1), y donde está el dato en memoria ($t0). Adicionalmente le indicamos el desplazamiento, que digamos es una constante que podemos aplicar para cargar unos bytes antes o después de lo que indique el registro. Esto es muy útil, pero en este ejemplo no tiene sentido usarse y lo he dejado a 0.

¿Por qué sumamos 4 a $t0 en cada pasada? Hemos dicho que los WORD en MIPS ocupan 32 bits, 4 bytes. Pues en esa operación estamos moviendo el puntero 4 bytes más en la memoria, para pasar al siguiente elemento del vector. Esto es aritmética de punteros. ¡Chachi pistachi!

El emulador Mars


¿Ya has visto por qué MIPS es tan bonito? A diferencia de otros ensambladores, la sintaxis de MIPS es muy regular, con comportamientos predecibles aunque no conozcamos exactamente la instrucción. Si quieres probar tus destrezas en MIPS existen varios simuladores. El que uso ahora mismo se llama Mars, está hecho en Java y es bastante completo. Es posible inspeccionar la memoria al completo, ir paso a paso en cada instrucción, insertar breakpoints y observar el valor de los registros en cada momento.

Descargar Mars

También si nos vemos con fuerzas podremos pasar a hardware real con Linux. OpenWrt, presente en routers, o Debian, en la Creator CI20 pueden ser buenas opciones.

Conclusión


Seguro que si habías programado con anterioridad en otro ensamblador esto te ha parecido muy sencillo. Y es que MIPS no es especialmente complejo.
Tags: programacion cpu hardware linux cpp ensamblador computador mars mips