2.5 Soporte de procedimientos El manejo de procedimientos o rutinas es uno de los aspectos más importantes de la programación estructurada; por lo que cualquier repertorio de instrucciones debe de incluirlo de alguna manera. Cuando se ejecuta un procedimiento, intrínsecamente se están realizando los siguientes pasos: a) Se colocan los argumentos (o parámetros) en algún lugar donde el procedimiento puede accesarlos. b) Se transfiere el control al procedimiento. c) Se adquieren los recursos de almacenamiento necesarios para el procesamiento. d) Se realiza la tarea deseada. e) Se coloca el valor del resultado en algún lugar donde el programa invocador puede accesarlo. f) Se regresa el control al punto de origen. Anteriormente se mencionó que los registros son más rápidos de manipular que las localidades en memoria, por lo tanto, para proporcionar algunas facilidades al compilador, se dedican algunos registros para el manejo de procedimientos: $a0 - $a3 : Cuatro registros en los cuales se pueden pasar los argumentos. Si el número de argumentos es mayor, el resto deberá estar en memoria. $v0 - $v2 : Dos registros en los cuales se colocarán los valores de retorno. $ra : Un registro para almacenar la dirección de retorno, una vez que el procedimiento termine. (Sus correspondientes números pueden conocerse consultando la tabla 2.1). Además de la reserva de estos registros, MIPS incluye una instrucción que provoca un salto hacia la dirección del procedimiento al mismo tiempo que guarda la dirección de la instrucción siguiente a la llamada en el registro $ra (la dirección de retorno), la instrucción es: jal Dirección_del_procedimiento ( jal – jump and link ) Aunque no se ha mencionado, pero al tener el programa almacenado en memoria, debe contarse con un registro que indique que instrucción se esta ejecutando. A este registro por tradición se le denomina contador del programa (program counter) o PC en forma abreviada. La arquitectura MIPS incluye a este registro, no es parte de los 32 registros de propósito general y tampoco existen las facilidades para leerlo o modificarlo. La instrucción jal guarda en $ra el valor de PC + 4 para ligar la siguiente instrucción en el retorno del procedimiento. De manera que el final de la rutina debe marcarse con la instrucción: jr $ra La instrucción de salto a registro que se utilizó también en las estructuras switch-case. Un punto importante en la llamada a los procedimientos, es que deben preservar el valor de algunos registros durante las invocaciones de los mismos, de manera que los registros conserven lo que contenían antes de que el procedimiento fuera invocado, para ello, su valor debe respaldarse en memoria. También ya se ha mencionado que si se requiere de un mayor número de argumentos éstos pueden ubicarse en memoria. Una Pila (stack) es la estructura más adecuada para resolver este tipo de situaciones, y en este caso, como en muchas otras arquitecturas, la pila crece hacía abajo, es decir, de las direcciones más altas hacia las más bajas. El registro dedicado como apuntador de la pila es el $sp (stack pointer), que corresponde al $29. Sin embargo, por la sencillez de la arquitectura MIPS no se incluyen instrucciones propias para la pila, mas bien los accesos se hacen combinando instrucciones simples, esto se ilustra en el ejemplo siguiente: Ejemplo: Un procedimiento que no llama a otros procedimientos. ¿Cuál sería el código MIPS para el procedimiento siguiente? int { ejemplo_simple ( int g, int h, int i, int j ) int f; f = ( g + h ) – ( i + j ); return f; } Respuesta: Los argumentos reciben en los registros: $a0, $a1, $a2 y $a3, que corresponden a las variables g, h, i y j, para f se utiliza al registro $s0. El procedimiento inicia con una etiqueta que corresponde a su nombre: ejemplo_simple: Lo primero que se realiza es el respaldo de las variables a utilizar: sub $sp, $sp, 12 # Hace espacio en la Pila sw $t1, 8 ($sp) # Salva a $t1 para uso posterior sw $t0, 4 ($sp) # Salva a $t0 para uso posterior sw $s0, 0 ($sp) # Salva a $s0 para uso posterior Lo siguiente es el cuerpo del procedimiento: add $t0, $a0, $a1 # $t0 = g + h add $t1, $a2, $a3 # $t1 = i + j sub $s0, $t0, $t1 # $f = ( g + h ) – ( i + j ) add $v0, $s0, $zero Antes de terminar se deben recuperar los valores almacenados en la Pila: lw $t1, 8 ($sp) # Recupera a $t1 lw $t0, 4 ($sp) # Recupera a $t0 lw $s0, 0 ($sp) # Recupera a $s0 add $sp, $sp, 12 # Ajusta al puntero de la Pila Finaliza la rutina: jr $ra # Regresa a la instrucción posterior # a la llamada a la rutina En la figura 2.5 se muestra el comportamiento de la pila para este ejemplo. Fig. 2.5 Comportamiento de la Pila (a) Antes de la rutina, (b) durante la rutina y (c) después de la rutina. En el ejemplo anterior se respaldaron en la pila todos lo registros por que se supuso que su valor podría estar siendo utilizado en alguna otra parte del programa. Para evitar accesos innecesarios a la memoria (y mejorar el rendimiento), los registros se dividen de la siguiente manera: $t0 - $t9 : 10 registros temporales cuyo valor no se preserva durante las llamadas a procedimientos. $s0 - $s7 : 8 registros seguros cuyo valor se preservará durante las llamadas a procedimientos. Procedimientos anidados Es común que un procedimiento invoque a otro procedimiento, en este caso ocurrirá un conflicto si no se busca como hacer esto. Por ejemplo, si un procedimiento A recibe como argumento al número 3, éste estará en el registro $a0. Si el procedimiento A invoca a un procedimiento B y le pasa como argumento al número 7 ¿Qué ocurrirá si se quiere usar el valor 3 después de la llamada al procedimiento B? Se esperaría que no hubiera problemas en el manejo de procedimiento anidados y para ello se utiliza la pila, para conservar todos los registros que se van a utilizar dentro del procedimiento y recuperarlos justo antes de que el procedimiento finalice. Existen dos criterios para este respaldo de registros: Guardar Invocador: El procedimiento invocador debe respaldar todos los registros que usará el procedimiento invocado, y recuperarlos una vez que termine el procedimiento. El procedimiento invocado no respalda registro alguno. Guardar Invocado: El procedimiento invocado es el que se encarga de respaldar y recuperar a los registros, el invocador simplemente realiza la invocación. El segundo criterio es el más ampliamente usado y es el que se aplica en el siguiente ejemplo: Ejemplo: Un procedimiento recursivo. Consideremos la función que obtiene el factorial de un número: int { fact ( int n ) if ( n < 1) return 1; return n * fact(n – 1); } Respuesta: Puesto que el procedimiento es recursivo, debe conservarse la dirección de retorno y el valor del argumento, para que no se pierda en la medida en que se profundiza dentro de la recursividad: fact: sub sw sw $sp, $sp, 8 $ra, 4 ($sp) $a0, 0 ($sp) # Hace espacio en la Pila # Salva la dirección de retorno # Salva al argumento n Se evalúa para ver si ocurre el caso base (cuando n < 1): slt $t0, $a0, 1 # $t0 = 1 si n < 1 beq $t0, $zero, L1 # Si n no es menor que 1 continua en la función Si ocurre el caso base, deberían recuperarse los datos de pila, pero como no se han modificado, no es necesario. Lo que si se requiere es restablecer al puntero de la pila. add add jr $v0, $zero, 1 # retorno = 1 $sp, $sp, 8 # Restablece al apuntador de la pila $ra # Finaliza regresando el resultado en $v0 Si no ocurre el caso base, prepara la llamada recursiva L1: sub $a0, $a0, 1 #n=n-1 jal fact # llama a fact con n – 1 Después de la llamada, se hace la restauración de los registros: lw $a0, 0($sp) # Recupera el valor de n lw $ra, 4($sp) # recupera la dirección de retorno add $sp, $sp, 8 # Restablece al apuntador de la pila Para concluir, se actualiza el valor de retorno y se regresa el control al invocador: mul $v0, $a0, $v0 # Retorno = n * fact (n – 1) jr $ra # regresa al invocador La complejidad en los programas reales es que la pila también es usada para almacenar variables que son locales a los procedimientos, que no alcanzan en los registros, tales como arreglos locales o estructuras. El segmento de pila que contiene los registros salvados de un procedimiento y las variables locales es llamado marco del procedimiento o (procedure frame). Se designa a un registro como apuntador al marco (el registro $fp que corresponde a $30), en la figura 2.6 se muestra el comportamiento de la pila, junto con el apuntador del marco. Fig. 2.6 Comportamiento del apuntador del marco del procedimiento (a) Antes de una llamada (b) durante la llamada, y (c) después de la llamada. Algunos programas usan el apuntador de marco, en general éste puede no ser utilizado. Una ventaja de su uso es que se cuenta con un registro base, a partir del cual se encuentran todas las variables locales de un procedimiento, por lo que el compilador puede usar este registro para liberar la cantidad de memoria adecuada, ante la salida abrupta de un procedimiento. 2.6 Manejo de Cadenas. Inicialmente las computadoras solo procesaban números, sin embargo en la medida en que llegaron a estar comercialmente disponibles, se les utilizó para procesar texto. La mayoría de computadoras utilizan 8 bits para representar un carácter de acuerdo al código americano estándar para el intercambio de información (ASCII – American Standar Code for Information Interchange). Un entero en MIPS utiliza 32 bits, que corresponde al tamaño de los registros, sin embargo utilizar 32 bits por carácter sería un desperdicio de memoria. Es por eso que además de las instrucciones para cargar y almacenar palabras, MIPS incluye instrucciones para cargar y almacenar bytes. La instrucción lb (load byte) carga un byte desde la memoria colocándolo en los 8 bits mas a la derecha de un registro; y la instrucción sb (store byte) toma un byte de los 8 bits mas a la derecha de un registro y los coloca en la memoria. Entonces, para copiar un byte de una localidad de memoria a otra se utilizaría: lb sb $t0, 0 ($sp) $t0, 0($gp) # Se lee el byte de la localidad fuente # Se escribe el byte en la localidad destino. Los caracteres normalmente se combinan para crear cadenas, las cuales tienen un número variable de caracteres. Hay tres opciones para representar una cadena: (1) La primera localidad de la cadena se reserva para que contenga la longitud de la misma, (2) Una variable acompaña a la cadena para que contenga su longitud (como en una estructura), o (3) La última posición de la cadena es indicada por un carácter especialmente utilizado para marcar el fin de la cadena. El lenguaje C utiliza la última opción y reserva el byte con valor cero (carácter NULL en el código ASCII) para indicar el final de las cadenas. Ejemplo: Compilando una función para cadenas. El procedimiento strcpy copia una cadena y en una cadena x, usando el byte de terminación null : void strcpy ( char x[ ], char y[ ]) { int i = 0; while( ( x[i] = y[i] ) != 0 ) i = i + 1; /* copia y prueba al byte */ } Respuesta: De acuerdo a la convención establecida, en $a0 está el comienzo del arreglo x y en $a1 está el comienzo del arreglo y. Para la variable i utilizaremos al registro $s0, que debe respaldarse en la pila antes de modificar su valor: strcpy: L1: L2: sub sw $sp, $sp, 4 $s0, 0 ($sp) # Hace espacio para un dato en la Pila # Salva a $s0 add $s0, $zero, $zero # inicializa a i con cero add add $t1, $a1, $s0 $t2, $a0, $s0 # La dirección de y[i] esta en $t1 # La dirección de x[i] esta en $t2 lb sb $t3, 0 ($t1) $t3, 0 ($t2) # Obtiene un dato de y[i] # Lo copia en x[i] beq $t3, $zero, L2 # Si el dato es NULL, sale del ciclo add j $s0, $s0, 1 L1 #i=i+1 # otra iteración lw add $s0, 0 ($sp) $sp, $sp, 4 # Restaura el valor de $s0 # Ajusta al puntero de la Pila jr $ra # Finaliza la rutina En el ejemplo anterior, puesto que el procedimiento strcpy es un procedimiento aislado (que no invoca a otro procedimiento), su variable local (i) podría asociarse con un registro temporal, y con ello se evitaría el respaldo y restauración del registro $s0. En lo sucesivo, se sugiere que para todos los procedimientos aislados se asocien sus variables locales con registros temporales. 2.7 Manejo de Constantes Muchos programas manejan constantes, algunos usos típicos son: incrementar el índice de un arreglo, contar iteraciones en un lazo, ajustar el apuntador de la pila, etc. De hecho, en los programas reales, alrededor del 50 % de operaciones aritméticas involucran el uso de constantes; por ejemplo, en el compilador de C denominado gcc el 52 % de operaciones aritméticas se aplican sobre constantes, en el simulador de circuitos llamado spice este parámetro corresponde al 69 %. En los ejemplos anteriores se utilizaron algunas constantes como operandos. Sin el manejo de constantes, para que una arquitectura pueda dar solución a problemas como los anteriores, la arquitectura podría implementarse de manera que apartir de un reset cargue algunos valores constantes en memoria para poder utilizaros cuando sea necesario. Esto obligaria a que cada vez que se requiera del uso de alguna constante, se debería incluir una instrucción previa que hiciera la carga. Si las constantes son muy utilizadas, por que no favorecer su uso. Esto da lugar al cuarto y último principio de diseño: Principio de Diseño 4: Hacer el caso común mas rápido. La instrucción addi permite hacer sumas con un constante, por ejemplo: Addi $s0, $s1, 6 realiza la suma $s0 = $s1 + 6 Esta instrucción permite cargar valores constantes en registros, por ejemplo, si se desea que el registro $s4 contenga el valor constante 32, se deberá usar: Addi $s4, $zero, 32 Que nos refleja una vez mas la importancia de mantener un registro con el valor constante 0 (otro uso importante se encuentra en los brincos condicionales). Es importante aclarar que no se cuenta con una instrucción de resta inmediata, y no es necesaria, ya que la resta puede conseguirse con la suma si la constante es negativa. Por ejemplo: Addi $s0, $s0, -4 # Decrementa en 4 el valor de $s0 La instrucción slt (set on less than) es bastante útil por que permite la comparación de dos registros, sin embargo también es bastante frecuente la comparación de un registro con una constante, por lo que existe una instrucción equivalente slti, para la comparación con constantes. Ejemplo: Manejo de Constantes. Con las nuevas instrucciones, ¿Cuál sería el código MIPS que produciría el siguiente ciclo? for( i = 0, x = 0; i < 10; i++ ) x = x + i; Respuesta: Asociemos la variable i con $s0 y a x con $s1: Addi Loop: Addi Slti Beq Add Addi J Exit: $s0, $zero, 0 $s1, $zero, 0 $t0, $s0, 10 $t0, $zero, exit $s1, $s1, $s0 $s0, $s0, 1 Loop #i=0 #x=0 # $t0 = 1 si i < 10 # termina si $t0 tiene 0 #x=x+i Nota: Para que los ejemplos anteriores puedan ser correctamente ensamblados, deberán sustituirse las instrucciones que manejan constantes por su versión correcta. Ahora, estas instrucciones son del tipo I, recordando el formato de las instrucciones tipo I: Significa que el tamaño de las constantes no puede ser mayor a 16 bits, pero los registros son de 32 bits, entonces ¿Cómo podrían manejarse datos de 32 bits? Este parece ser un problema serio puesto que las instrucciones también son de 32 bits, de manera que no puede ser posible que manipulen una constante del mismo tamaño. MIPS incluye una instrucción denominada lui (load upper immediate) cuyo formato es: Lui registro, constante La cual coloca una constante en los 16 bits más significativos del registro, permitiendo agregar los bits menos significativos con la instrucción addi. La instrucción lui es tipo I, de manera que al ser convertida en código máquina, el campo para RS es ignorado (tendrá 0’s) y en el campo RT contendrá el registro que será modificado. Ejemplo: Cargando una constante de 32 bits. ¿Qué instrucciones se requerirían para cargar esta constante de 32 bits en el registro $s0? 0000 0000 0011 1101 0000 1001 0000 0000 Respuesta: Los 16 bits más significativos “0000 0000 0011 1101” corresponden al número: 61 en decimal. Y los 16 bits menos significativos “0000 1001 0000 0000” corresponden al número: 2304 en decimal. Se requiere de 2 instrucciones: Lui $s0, 61 Addi $s0, $s0, 2304 # carga la parte alta # carga la parte baja Tal vez el manejo de constantes grandes no sea muy rápido, lo cual no es tan importante por que en la mayoría de aplicaciones, el valor de las constantes no excede los 16 bits. El uso de constantes de 32 bits no es un caso común. 2.8 Modos de direccionamiento Contar con diferentes formatos de instrucciones, implica contar con diferentes formas de obtener los operandos de las instrucciones. Por lo general a estas múltiples formas se les conoce como modos de direccionamiento. Los modos de direccionamiento en MIPS son: 1.- Direccionamiento por registro, donde los operandos son registros. Los datos a operar están contenidos en 2 registros de 32 bits y el resultado será colocado en otro registro, del mismo tamaño. Ejemplos de instrucciones que usan este modo de direccionamiento: add, sub, slt, etc. 2.- Direccionamiento base o desplazamiento; donde uno de los operandos está en una localidad de memoria cuya dirección es la suma de un registro y una constante que forma parte de la misma instrucción. Ejemplos de instrucciones que usan este modo de direccionamiento: lw, sw, etc. 3.- Direccionamiento inmediato; donde uno de los operandos es una constante que está en la misma instrucción. Ejemplos de instrucciones que usan este modo de direccionamiento: addi, slti, etc. 4.- Direccionamiento relativo al PC, se forma una dirección sumando el registro PC (Program Counter) con una constante, la cual está en la instrucción. El resultado de la suma corresponde a la dirección destino para un brinco condicional. Ejemplos de instrucciones que usan este modo de direccionamiento: beq y bne. 5.- Direccionamiento pseudo directo, donde la dirección destino de un salto corresponde a la concatenación de 26 bits que están en la misma instrucción con los bits más significativos del PC. Ejemplos de instrucciones que usan este modo de direccionamiento: j y jal. Es importante resaltar que, aunque se está revisando una arquitectura de 32 bits, MIPS, como muchas otras arquitecturas, tiene una extensión que maneja instrucciones, datos y direcciones de 64 bits. Esto como una respuesta a la necesidad de manejar programas cada vez más grandes. 2.9 Programas de ejemplo Cuando se traslada desde un lenguaje de alto nivel a ensamblador, en general, se realizan los pasos siguientes: a) Se asocian los registros con las variables del programa. b) Se produce el código para el cuerpo del procedimiento. c) Se preservan los registros a través del procedimiento. 2.9.1 El procedimiento SWAP. El procedimiento swap intercambia dos localidades de memoria. En C este procedimiento es: void swap ( int v[ ], int k ) { int temp; temp = v[k]; v[k] = v[k+1]; v[k+1] = temp; } a) Asociación de registros con variables. El procedimiento swap recibe dos argumentos, por convención, estos argumentos los debe recibir en los registros $a0 (se asocia con el inicio del arreglo v) y $a1 (se asocia con la variable k). Para la variable temp, debido a que swap es un procedimiento aislado, se asociará con el registro $t0 (que no va a preservarse a través de la llamada). b) El cuerpo del procedimiento. En C el cuerpo del procedimiento es: temp = v[k]; v[k] = v[k+1]; v[k+1] = temp; debe considerarse que los datos se constituyen de palabras de 4 bytes, de manera que para obtener la dirección de v[k] primero se debe multiplicar a k por 4. Add $t1, $a1, $a1 # $t1 = 2*k = k + k Add $t1, $t1, $t1 # $t1 = 4*k = 2*k + 2*k Add $t1, $a0, $t1 # $t1 tiene la dirección de v[k] Ahora es posible hacer la carga de v[k], y de v[k + 1] Lw $t0, 0($t1) # reg $t0 (temp) = v[k] Lw $t2, 4($t1) # reg $t2 = v[k + 1] Finalmente se hace el almacenamiento en memoria. sw $t2, 0($t1) # v[k] = reg $t2 sw $t0, 4($t1) # v[k + 1] = reg $t0 (temp) c) Preservación de registros. En este procedimiento no se preserva a algún registro, debido a que es un procedimiento aislado. El código completo para la función swap es: Cuerpo del procedimiento Swap: Add Add Add $t1, $a1, $a1 $t1, $t1, $t1 $t1, $a0, $t1 # $t1 = 2*k = k + k # $t1 = 4*k = 2*k + 2*k # $t1 tiene la dirección de v[k] Lw Lw $t0, 0($t1) $t2, 4($t1) # reg $t0 (temp) = v[k] # reg $t2 = v[k + 1] sw sw $t2, 0($t1) $t0, 4($t1) # v[k] = reg $t2 # v[k + 1] = reg $t0 (temp) jr $ra Retorno del procedimiento # Regresa a la rutina invocadora 2.9.2 El procedimiento SORT. El procedimiento sort ordena los elementos de un arreglo, su código C es: void sort ( int v[ ], int n ) { int i, j; for ( i = 0; i < n; i++ ) for (j = i – 1; j >= 0 && v[j] > v[j + 1]; j--) swap( v, j); } a) Asociación de registros con variables. Los dos argumentos del procedimiento sort se reciben en los registros $a0 (inicio del arreglo v) y $a1 (la variable n). La variable i se asocia con el registro $s0 y j con el registro $s1. b) El cuerpo del procedimiento. Para el ciclo mas externo: For ( i = 0; i < n; i++ ) La primera expresión en ensamblador corresponde a: Add $s0, $zero, $zero #i=0 Luego se realiza la comparación: For1o: slt $t0, $s0, $a1 Beq $t0, $zero, exit1 # $t0 = 1 si i < n # Si $t0 = 0, termina este ciclo El código siguiente corresponde al que se realiza dentro de este ciclo repetitivo (cuerpo del for externo). Una vez que este código concluye, se realiza el incremento y el salto a la siguiente iteración dentro del ciclo repetitivo. Addi J $s0, $s0, 1 For1o # i ++ # Va a la siguiente iteración El esqueleto de este primer ciclo repetitivo es: Add $s0, $zero, $zero #i=0 For1o: slt $t0, $s0, $a1 # $t0 = 1 si i < n Beq $t0, $zero, exit1 # Si $t0 = 0 (i >= n), termina este ciclo .... .... # Cuerpo del primer for Addi $s0, $s0, 1 # i ++ J For1o # Va a la siguiente iteración Exit1: El segundo for en C es: for (j = i – 1; j >= 0 && v[j] > v[j + 1]; j--) La inicialización de la variable j corresponde a: Addi $s1, $s0, -1 #j=i-1 Este ciclo prueba dos expresiones ligadas con un operador AND, si la primera falla, es suficiente para terminar con el ciclo. El cuerpo del for se realiza solo cuando las dos expresiones son verdaderas: For2o: slt Bne $t0, $s1, $zero $t0, $zero, exit2 # $t0 = 1 si j < 0 # Si $t0 = 1 (j < 0), termina este ciclo La segunda expresión a evaluar es v[j] > v[j + 1] si es verdadera se realiza el cuerpo del for, y en caso contrario, el ciclo termina. Esta prueba requiere transferir los datos de memoria a registros para poder compararlos, por lo que primero deberá multiplicarse a j por 4: Add Add Add $t1, $s1, $s1 $t1, $t1, $t1 $t2, $a0, $t1 Se obtienen los datos a comparar: Lw $t3, 0($t2) Lw $t4, 4($t2) # $t1 = 2*j # $t1 = 4*j # $t2 tiene la dirección de v[j] # $t3 tiene el valor de v[j] # $t4 tiene el valor de v[j + 1] Ahora es posible la comparación: slt Beq $t0, $t4, $t3 $t0, $zero, exit2 # $t0 = 1 si v[j + 1] < v[j] # Si $t0 = 0 (v[j + 1] >= v[j]), termina El código siguiente correspondería al cuerpo del ciclo for, y al final se debería incluir: Addi $s1, $s1, -1 # El decremento j— J For2o # El salto a la siguiente iteración Juntando las piezas se obtiene el esqueleto del 2º. Ciclo for: For2o: Addi slt Bne Add Add Add Lw Lw slt Beq Addi J $s1, $s0, -1 $t0, $s1, $zero $t0, $zero, exit2 $t1, $s1, $s1 $t1, $t1, $t1 $t2, $a0, $t1 $t3, 0($t2) $t4, 4($t2) $t0, $t4, $t3 $t0, $zero, exit2 .... .... $s1, $s1, -1 For2o #j=i-1 # $t0 = 1 si j < 0 # Si $t0 = 1 (j < 0), termina este ciclo # $t1 = 2*j # $t1 = 4*j # $t2 tiene la dirección de v[j] # $t3 tiene el valor de v[j] # $t4 tiene el valor de v[j + 1] # $t0 = 1 si v[j + 1] < v[j] # Si $t0 = 0 (v[j + 1] >= v[j]), termina # Cuerpo del segundo for # El decremento j-# El salto a la siguiente iteración Exit2: El cuerpo del ciclo mas interno incluye la llamada a la función swap, la cual aparentemente solo incluiría a la instrucción: Jal swap Sólo que al hacer la llamada debe considerarse el lugar correcto para los parámetros de la función swap. Los argumentos también deben preservarse a través de las llamadas, de manera que será necesario respaldar el valor actual de $a0 y $a1. Esto se hace con las instrucciones: Add $s2, $a0, $zero # Se respalda el primer argumento Add $s3, $a1, $zero # Se respalda el segundo argumento Luego, ya será posible asignar los parámetros: Add $a0, $s2, $zero # $a0 = v primer argumento de swap Add $a1, $s1, $zero # $a0 = j segundo argumento de swap Entonces, para que en el código anterior no afecten los nuevos valores de los parámetros, en lugar de usar $a0 deberá usarse a $s2 y en vez de $a1 deberá usarse a $s3. c) Preservación de registros. El primer registro a consideración es el que contiene la dirección de retorno, para que no se afecte con la llamada a swap. Además, debido a que el registro sort usa a los registros $s0, $s1, $s2 y $s3 debe respaldarlos en la pila para preservarlos durante la llamada. El prólogo del procedimiento sort es entonces: Addi $sp, $sp, -20 # Hace espacio para 5 registros Sw Sw Sw Sw Sw $ra, 16( $sp ) $s3, 12( $sp ) $s2, 8( $sp ) $s1, 4( $sp ) $s0, 0( $sp ) # Salva a $ra en la pila # Salva a $s3 en la pila # Salva a $s2 en la pila # Salva a $s1 en la pila # Salva a $s0 en la pila Al final del procedimiento se recuperarían estos registros de la pila y se retornaría a la función invocadora. El código completo para la función sort es: Mueve parámetros Lazo externo Lazo interno Pase de parámetros y llama a swap Lazo interno Lazo externo Respalda a los registros en la Pila Sort: Addi $sp, $sp, -20 # Hace espacio para 5 registros Sw $ra, 16( $sp ) # Salva a $ra en la pila Sw $s3, 12( $sp ) # Salva a $s3 en la pila Sw $s2, 8( $sp ) # Salva a $s2 en la pila Sw $s1, 4( $sp ) # Salva a $s1 en la pila Sw $s0, 0( $sp ) # Salva a $s0 en la pila Cuerpo del Procedimiento Add $s2, $a0, $zero # Se respalda el primer argumento Add $s3, $a1, $zero # Se respalda el segundo argumento Add $s0, $zero, $zero #i=0 For1o: slt $t0, $s0, $s3 # $t0 = 1 si i < n Beq $t0, $zero, exit1 # Si $t0 = 0 (i >= n), termina Addi $s1, $s0, -1 #j=i–1 For2o: slt $t0, $s1, $zero # $t0 = 1 si j < 0 Bne $t0, $zero, exit2 # Si $t0 = 1 (j < 0), termina este ciclo Add $t1, $s1, $s1 # $t1 = 2*j Add $t1, $t1, $t1 # $t1 = 4*j Add $t2, $s2, $t1 # $t2 tiene la dirección de v[j] Lw $t3, 0($t2) # $t3 tiene el valor de v[j] Lw $t4, 4($t2) # $t4 tiene el valor de v[j + 1] slt $t0, $t4, $t3 # $t0 = 1 si v[j + 1] < v[j] Beq $t0, $zero, exit2 # Si $t0 = 0 (v[j + 1] >= v[j]), termina Add $a0, $s2, $zero # $a0 = v primer argumento de swap Add $a1, $s1, $zero # $a0 = j segundo argumento de swap Jal swap # Llama a swap Addi $s1, $s1, -1 # El decremento j— J For2o # El salto a la siguiente iteración Exit2: Addi $s0, $s0, 1 # El incremento i++ J For1o # El salto a la siguiente iteración Restauración de Registros Exit1: Lw $ra, 16( $sp ) # Recupera a $ra Lw $s3, 12( $sp ) # Recupera a $s3 Lw $s2, 8( $sp ) # Recupera a $s2 Lw $s1, 4( $sp ) # Recupera a $s1 Lw Addi Jr $s0, 0( $sp ) # Recupera a $s0 $sp, $sp, -20 # Recupera la pila Regresa del procedimiento $ra Resumen: En los operandos se muestran detalles del uso de memoria y registros, y en las instrucciones se incorporan las nuevas instrucciones consideradas en este documento. MIPS operands Name 32 registers 30 2 Example $s0-$s7, $t0-$t9, $zero, $a0-$a3, $v0-$v1, $gp, $fp, $sp, $ra, $at Memory[0], memory Memory[4], ..., words Data transfer Conditional branch Unconditional jump Accessed only by data transfer instructions. MIPS uses byte addresses, so sequential words differ by 4. Memory holds data structures, such as arrays, and spilled registers, such as those saved on procedure calls. Memory[4294967292] add MIPS assembly language Example Meaning add $s1, $s2, $s3 $s1 = $s2 + $s3 Three operands; data in registers subtract sub $s1, $s2, $s3 $s1 = $s2 - $s3 Three operands; data in registers add immediate load word store word load byte store byte load upper immediate addi $s1, $s2, 100 lw $s1, 100($s2) sw $s1, 100($s2) lb $s1, 100($s2) sb $s1, 100($s2) lui $s1, 100 Used to add constants $s1 = $s2 + 100 $s1 = Memory[$s2 + 100] W ord from memory to register Memory[$s2 + 100] = $s1 W ord from register to memory $s1 = Memory[$s2 + 100] Byte from memory to register Memory[$s2 + 100] = $s1 Byte from register to memory branch on equal beq $s1, $s2, 25 if ($s1 == $s2) go to PC + 4 + 100 Equal test; PC-relative branch branch on not equal bne $s1, $s2, 25 if ($s1 != $s2) go to PC + 4 + 100 Not equal test; PC-relative set on less than slt $s1, $s2, $s3 if ($s2 < $s3) $s1 = 1; else $s1 = 0 Compare less than; for beq, bne set less than immediate slti jump jump register jump and link j jr jal Category Arithmetic Comments Fast locations for data. In MIPS, data must be in registers to perform arithmetic. MIPS register $zero always equals 0. Register $at is reserved for the assembler to handle large constants. Instruction $s1 = 100 * 2 16 $s1, $s2, 100 if ($s2 < 100) $s1 = 1; Comments Loads constant in upper 16 bits Compare less than constant else $s1 = 0 2500 $ra 2500 Jump to target address go to 10000 For switch, procedure return go to $ra $ra = PC + 4; go to 10000 For procedure call Tarea 4: 1.- Realizar un procedimiento en C que devuelva el mayor de un arreglo de n elementos. 2.- Trasladar el resultado del ejemplo anterior a código MIPS, respetando las convenciones establecidas para la asociación de registros con variables. 3.- Escribir un procedimiento bfind, en lenguaje ensamblador MIPS, que reciba como argumento un apuntador a una cadena terminada con NULL (correspondería a $a0) y localice la primer letra b en la cadena, de manera que el procedimiento debe devolver la dirección de esta primera aparición (se regresaría en $v0). Si no hay b’s en la cadena, entonces bfind deberá regresar un apuntador al carácter nulo (localizado al final de la cadena). Por ejemplo, si bfind recibe como argumento un apuntador a la cadena “embebido” deberá devolver un apuntador al tercer carácter en la cadena. 4.- Escribir un procedimiento bcount, en lenguaje ensamblador MIPS, que reciba como argumento un apuntador a una cadena terminada con NULL (correspondería a $a0) y devuelva el número de b’s que aparecen en la cadena (en el registro $v0). Para la implementación de bcount deberá utilizarse la función bfind desarrollada en el ejercicio anterior. 5.- Escribir un procedimiento en código MIPS para calcular el n-ésimo término de la serie de Fibonacci (F(n)), donde: F(0) = 0 F(1) = 1 F(n) = F(n – 1) + F(n – 2) Si n > 1 Con base en el procedimiento recursivo: int fib( int n ) { if ( n == 0 || n == 1 ) Return n; return fib( n – 1) + fib( n – 2); }