UNIDAD 2: Instrucciones: el lenguaje de las computadoras. 2.1 Introducción Para comandar una computadora se le debe “hablar” en su lenguaje. Las palabras del lenguaje de una máquina son llamadas instrucciones, y su vocabulario es llamado repertorio de instrucciones. En esta sección se expone el repertorio de instrucciones de una computadora real, considerando una forma entendible para los humanos (ensamblador) y una forma entendible por la computadora (código máquina). Podría pensarse que los lenguajes de máquina son tan diversos como los lenguajes de los humanos, sin embargo no es así, los lenguajes de máquina son muy similares entre sí; esto se debe a que todas las computadoras son construidas con tecnologías de hardware basadas en principios fundamentales similares y por que hay unas cuantas operaciones básicas que todas las máquinas deben proporcionar. Más aún, los diseñadores de computadoras tienen una meta común: Encontrar un lenguaje que haga fácil la construcción del hardware y el compilador; mientras se maximiza el rendimiento y se minimiza el costo. Esta meta ha sido honrada a lo largo del tiempo; la siguiente cita fue escrita antes de que fuera posible adquirir una computadora y es tan cierta actualmente, como lo fue en 1947: Es fácil ver por métodos lógicos formales que existen ciertos [repertorios de instrucciones] que son en abstracto adecuados para controlar y causar la ejecución de cualquier secuencia de operaciones. . . . Las consideraciones realmente decisivas desde el punto de vista actual, en seleccionar un [repertorio de instrucciones], son más de naturaleza práctica: La simplicidad del equipo demandado por el [repertorio de instrucciones], y la claridad de su aplicación a problemas realmente importantes junto con la velocidad del manejo de esos problemas. Burks, Goldstine, y von Neumann, 1947 La “simplicidad del equipo” es una consideración tan valiosa para las máquinas actuales como lo fue en los 50’s. La meta de este capítulo es mostrar un repertorio de instrucciones que siga este consejo, mostrando como éste es representado en hardware y su relación con los lenguajes de alto nivel. El repertorio de instrucciones bajo estudio es de una arquitectura MIPS, usado por NEC, Nintendo, Silicon Graphics, y Sony; entre otros. Y es un repertorio de instrucciones típico diseñado en los 80’s. 2.2 Operaciones y operandos. Toda computadora debe realizar operaciones aritméticas. La notación en el lenguaje ensamblador MIPS Add a, b, c Instruye a la computadora a sumar las dos variables b y c, y colocar la suma en a. 1 Esta notación es rígida, en el sentido que cada instrucción aritmética de MIPS realiza una sola operación y debe tener tres variables (“variable” en un sentido vago, mas adelante se analizarán los operandos en MIPS). Si se quieren sumar las variables b, c, d y e en la variable a, será necesaria la secuencia: add add add a, b, c a, a, d a, a, e # La suma de b y c es puesta en a # La suma de b, c y d es puesta en a # La suma de b, c, d y e es puesta en a Se requiere de tres instrucciones para sumar cuatro variables. Como en todos los ensambladores, cada línea contiene a lo más una instrucción. Los comentarios inician con el símbolo # y terminan al final del renglón. El número natural de operandos para una operación aritmética como la suma es tres, los dos operandos a sumar y el operando donde se colocará el resultado. Por lo que es adecuado que las instrucciones aritméticas cuenten con tres operandos: no más o menos; de acuerdo con la filosofía de mantener un hardware simple, es evidente que el hardware para un número variable de operandos es más complicado que el hardware para un número fijo. Esta situación ilustra el primero de cuatro principios para el diseño de hardware: Principio de diseño 1: La simplicidad favorece la regularidad. En los ejemplos siguientes se muestra la relación que existe entre un lenguaje de alto nivel y el código MIPS. Ejemplo: Compilando dos asignaciones a código MIPS Este segmento de un programa en C contiene cinco variables a, b, c, d y e: a = b + c; d = a – e; La traducción de C al ensamblador MIPS la realiza el compilador correspondiente, mostrar el código MIPS que produciría para estas asignaciones. Respuesta: Una instrucción MIPS opera con dos operandos fuentes y pone el resultado en un operando destino. Para dos asignaciones simples, solo se requiere de dos instrucciones: add sub a, b, c d, a, e 2 Ejemplo: Compilando una asignación mas compleja Para la siguiente asignación en C: f = (g + h) – (i + j); ¿Qué produciría el compilador? Respuesta: El compilador separa la asignación y utiliza variables temporales, de manera que el resultado es: add add sub t0, g, h t1, i, j f, t0, t1 # La variable temporal t0 contiene g + h # La variable temporal t1 contiene i + j # f obtiene t0 – t1, que es (g + h) – (i + j) Hasta el momento no se ha puesto atención a los símbolos involucrados en el código MIPS, sin embargo, a diferencia de los lenguajes de alto nivel, los operandos de las instrucciones no pueden ser cualquier variable; mas bien quedan limitados por un número especial de localidades llamadas registros. Los registros son los ladrillos en la construcción de una computadora, se definen durante el diseño del hardware y quedarán visibles al programador cuando la computadora este completada. El tamaño de los registros en la arquitectura MIPS es de 32 bits; los grupos de 32 bits son tan frecuentes que por lo general se les da el nombre de palabra en la arquitectura MIPS. Una principal diferencia entre las variables de un lenguaje de programación y los registros, es el limitado número de registros. MIPS tiene 32 registros, por lo que para una instrucción aritmética se puede elegir entre 32 registros de 32 bits para los tres operandos. La razón del límite en 32 registros se explica con el segundo principio del diseño de hardware. Principio de diseño 2: Si es más pequeño es más rápido. Un número muy grande de registros automáticamente incrementa el tiempo del ciclo de reloj, simplemente por que las señales eléctricas requieren de más tiempo cuando necesitan viajar más lejos. Directivas tales como “Si es mas pequeño es más rápido” no son absolutas; 31 registros no pueden ser mas rápidos que 32. Los diseñadores de hardware deben tomar muy en cuenta este principio y balancear entre el deseo anormal de los programadores por contar con un número grande de registros con el deseo de los diseñadores de mantener un ciclo de reloj rápido. 3 Aunque se podrían simplemente considerar los números del 0 al 31 para nombrar a los registros, para no confundirlos con valores constantes, por conveniencia se les antepone el símbolo de pesos ($), de manera que los registros son $0, $1, ..., $31. Además, para simplificar el trabajo del compilador, los registros que correspondan directamente con los nombres de las variables en alto nivel, se nombrarán con $s0, $s1,...; y los registros temporales serán $t1, $t2, $t3,... Esto sólo por convención, en la tabla 2.1 se muestran estas y otras convenciones utilizadas. Name $zero $v0-$v1 $a0-$a3 $t0-$t7 $s0-$s7 $t8-$t9 $gp $sp $fp $ra Register number Usage 0 the constant value 0 2-3 values for results and expression evaluation 4-7 arguments 8-15 temporaries 16-23 saved 24-25 more temporaries 28 global pointer 29 stack pointer 30 frame pointer 31 return address Tabla 2.1 Convenciones utilizadas aplicadas en el uso de registros. Puede notarse que el registro $0 siempre contendrá el valor 0, este convención es bastante útil cuando se realizan comparaciones con cero o brincos condicionales. Ejemplo: Uso de registros. Se repite el ejemplo anterior, pero al utilizar los nombres de los registros en el código ensamblador. f = (g + h) – (i + j); ¿Qué produciría el compilador? Respuesta: Suponiendo que las variables f, g, h, i y j se asocian con los registros $s0, $s1, $s2, $s3 y $s4, respectivamente. add add sub $t0, $s1, $s2 $t1, $s3, $s4 $s0, $t0, $t1 # El registro $t0 contiene $s1+ $s2 # El registro $t1 contiene $s3+ $s4 # f obtiene $t0 – $t1, que es (g + h) – (i + j) Los lenguajes de programación usan variables simples que se asocian directamente con registros; sin embargo también utilizan estructuras de datos un poco más complejas como es los arreglos; y es imposible que un arreglo alcance dentro de los registros del procesador. 4 Entrada Control Memoria Camino de los datos Salida Procesador Fig. 2.1 Cinco componentes clásicos de una computadora Recordando los cinco elementos clásicos de la computadora (que se repite en la figura 2.1, por conveniencia), se observa que debido al número limitado de registros, el lugar mas adecuado para las estructuras de datos es la memoria. Sin embargo, puesto que las instrucciones aritméticas solo se realizan con los operandos en registros, la arquitectura MIPS debe incluir algunas instrucciones que permitan la transferencia de datos de memoria a registros y viceversa. La memoria puede ser considerada como un arreglo unidimensional, grande, con las direcciones actuando como índices en el arreglo, iniciando en la 0. Por ejemplo, en la figura 2.2, la dirección del tercer dato en memoria es 2 y su valor es Memoria [2] = 10. Fig. 2.2 Direcciones de Memoria y su contenido en algunas localidades La instrucción que mueve datos desde la memoria a un registro se le conoce como carga, y su formato es: lw $s0, 100 ( $s1 ) que significa $s0 = Memoria[$s1+ 100 ] lw es el nombre de la instrucción (load word), el primer registro que aparece es el que será cargado ($s0, en este caso), luego se incluye una constante (100, en este caso) la cual se considera como un desplazamiento (offset) y finalmente entre paréntesis aparece otro registro ($s1, en este caso), el cual es conocido como registro base. La dirección de la palabra a cargar se forma sumando el valor del registro base con el desplazamiento. 5 Ejemplo: Carga de memoria. Para la siguiente asignación en C: g = h + A[8]; ¿Qué produciría el compilador? Suponiendo que el comienzo del arreglo A se encuentra en el registro $s0, y que las variables g y h se asocian con los registros: $s1 y $s2, respectivamente. Respuesta: Primero se debe tener acceso a la memoria para la lectura del dato: lw $t0, 8( $s0) # El registro $t0 contiene A[8] Ahora ya es posible realizar la suma: add $s1, $s2, $t0 # g = h + A[8] Los compiladores son los que se encargan de asociar las estructuras de datos con la memoria. Por lo que el compilador debe poder colocar la dirección adecuada en las instrucciones de transferencias. Puesto que 8 bits (1-byte) son útiles en muchos programas, la mayoría de computadoras conservan el direccionamiento por bytes individuales. Por lo que el direccionamiento por palabras se refiere a la lectura de un conjunto de 4 bytes. Esto significa que las direcciones secuenciales de palabras deben diferir en 4. En la figura 2.2 se mostraron a los datos con direcciones continuas, sin embargo, puesto que se están considerando palabras (de 32 bits), la distribución correcta de los datos sería la que se muestra en la figura 2.3. Debido a que las memorias organizan a los datos por bytes, cuando se manejan arreglos de palabras se afecta a los índices de los mismos, entonces el compilador debe calcular adecuadamente la dirección del dato que será transferido. En el último ejemplo, para que se haga la lectura correcta del elemento 8 del arreglo A, el desplazamiento debe multiplicarse por 4, de manera que a $s0 se le sume 32 (8x4), así se seleccionará al dato A[8] y no a A[8/4]. 6 Fig. 2.3 Direccionamiento por palabras. La instrucción complementaria a la carga se llama almacenamiento (store), para transferir un dato desde un registro a la memoria. Su formato es similar al de las instrucciones de carga: sw $s0, 100 ( $s1 ) que significa Memoria[$s1+ 100 ]=$s0 sw es el nombre de la instrucción (store word). Ejemplo: Carga y Almacenamiento. Para la siguiente asignación en C: A[12] = h + A[8]; ¿Qué produciría el compilador? Suponiendo que el comienzo del arreglo A se encuentra en el registro $s0, y que la variable h se asocia con el registro $s1: Respuesta: Primero se debe acceder a la memoria para la carga del dato: lw $t0, 32( $s0) # El registro $t0 contiene A[8] Ahora ya es posible realizar la suma: add $t0, $s1, $t0 # El registro $t0 contiene h + A[8] Por último se realiza el almacenamiento: sw $t0, 48( $s0) # A[12]= h + A[8] Los índices del arreglo (8 y 12) se deben multiplicar por 4 para obtener las direcciones adecuadas de los datos en memoria. 7 Ejemplo: Usando una variable como índice de un arreglo. La siguiente asignación utiliza a la variable i como índice del arreglo A: g = h + A[i]; ¿Qué producirá el compilador? Suponiendo que el comienzo del arreglo A se encuentra en el registro $s0, y que las variables g, h e i se asocian con los registros: $s1, $s2 y $s3, respectivamente. Respuesta: Antes de accesar a la memoria se debe obtener la dirección adecuada del dato a leer, puesto que solo se han considerado instrucciones de suma, primero se obtendrá i x 4, realizando i + i = 2i, y luego 2i + 2i = 4i: add add $t0, $s3, $s3 $t0, $t0, $t0 # $t0 = i + i = 2i # $t0 = 2i + 2i = 4i La carga se hace con la instrucción: add $t0, $t0, $s0 # $t0 tiene la dirección de A[i] lw $t1, 0( $t0) # $t1 = A[i] Finalmente se realiza la suma: add $s1, $s2, $t1 # g = h + A[i] Muchos programas tienen más variables que registros en el procesador. En consecuencia, el compilador intenta mantener a las variables más usadas en registros y el resto en memoria, usando cargas y almacenamientos para mover variables entre registros y memoria. El proceso de poner a las variables menos usadas (o aquellas que se usarán posteriormente) en memoria se conoce como derramamiento de registros (spilling registers). El principio de hardware que relaciona el tamaño con la velocidad sugiere que la memoria debe ser mas lenta que los registros porque el tamaño del conjunto de registros es menor que el de la memoria. El acceso a los datos es más rápido si los datos están en registros. Y los datos son más útiles cuando están en registros por que una instrucción aritmética se aplica sobre dos registros, mientras que los accesos a memoria solo manipulan un dato. En conclusión, los datos en los registros en MIPS toman un menor tiempo y tienen una productividad más alta que los datos en la memoria. Para aumentar el rendimiento, los compiladores MIPS deben usar los registros eficientemente. 8 2.3 Representación de instrucciones. Hasta el momento se han considerado algunas instrucciones MIPS compuestas de un nombre (nemotécnico) y una serie de operandos; sin embargo dentro de la computadora las instrucciones se almacenan en memoria como pequeños capacitores cargados o descargados y se transfieren entre dispositivos como señales eléctricas con niveles de voltajes altos (5 volts) y bajos (0 volts). Por lo que son suficientes dos símbolos para la representación de las instrucciones. Un sistema numérico con dos símbolos es el sistema binario, por eso se utiliza para la representación de las instrucciones. Sólo hemos considerado 2 tipos de instrucciones, aritméticas y de transferencia de datos, si las comparamos podremos notar que en ambos casos existen tres operandos, en el caso de las instrucciones aritméticas los tres operandos son registros (y como son 32 registros, con 5 bits es suficiente su representación) y para las transferencias de datos dos operandos son registros y el tercero es una constante, es evidente que no es posible disponer solo de 5 bits para el valor de la constante, puesto que su valor sería muy limitado. Lo que implica que si se quiere conservar el mismo formato para ambas instrucciones, estas tendrían diferentes tamaños. Esto da pie al tercer principio de diseño: Principio de diseño 3: Un buen diseño demanda compromisos. Los diseñadores de MIPS enfrentaron el problema de decidir si mantenían a todas las instrucciones del mismo tamaño, generando con ello diferentes formatos de instrucciones o si mantenían el formato ocasionando instrucciones de diferentes tamaños. Se comprometieron con el primer punto y buscando regularidad con el tamaño de los datos, todas las instrucciones en MIPS son de 32 bits. De manera que se tienen diferentes formatos, las instrucciones aritméticas son del Tipo-R por que solo se aplican sobre registros. El formato para las instrucciones Tipo-R es: op rs rt rd shamt funct 6 bits 5 bits 5 bits 5 bits 5 bits 6 bits El significado para cada uno de los campos es: op: Operación básica de la instrucción, tradicionalmente llamado opcode. Su valor es 0 en el caso de las operaciones aritméticas. rs: El primer operando fuente. rt: El segundo operando fuente. rd: El registro destino, obtiene el resultado de la operación. shamt: Cantidad de desplazamiento (shift amount), solo se aplica a las instrucciones de desplazamiento, aquí su valor será 0 (se revisa mas adelante). funct: Función, selecciona una variante de la operación, por ejemplo, la suma y resta ambas son operaciones aritméticas, pero realizan diferentes operaciones. Su valor es de 32 para la suma y 34 para la resta. 9 Las instrucciones lw y sw son del tipo-I, por que incluyen una constante (dato inmediato). El formato para las instrucciones del tipo-I es: op rs rt inmediato 6 bits 5 bits 5 bits 16 bits En este caso tenemos: op: Su valor es 35 para las cargas y 43 para los almacenamientos. rs: Registro base para la dirección de memoria. rt: Registro a ser cargado o almacenado. inmediato: Constante que corresponde al desplazamiento. Ejemplo: Trasladando a código máquina. ¿Cuál es el código máquina de la instrucción: add $t0, $s1, $s2? Respuesta: Tomando como referencia la tabla 1, el registro $t0 es el 8 , $s1 es el 17 y $s2 es el 18. De manera que la representación en decimal de esta instrucción es: 0 17 18 8 0 32 Y su representación binaria corresponde a: 000000 10001 10010 01000 00000 100000 6 bits 5 bits 5 bits 5 bits 5 bits 6 bits Ejemplo: Trasladando cargas y almacenamientos. La asignación en C: A[12] = h + A[8]; Produjo el siguiente código en ensamblador: lw add sw $t0, 32( $s0) $t0, $s1, $t0 $t0, 48( $s0) # El registro $t0 contiene A[8] # El registro $t0 contiene h + A[8] # A[12]= h + A[8] ¿Cuál es su correspondiente código máquina? 10 Respuesta: La versión decimal del código máquina es: 35 16 8 0 17 8 43 16 8 32 8 0 32 48 Para obtener los números a los que corresponden los registros, puede usarse la tabla 2.1, la versión binaria del código corresponde a: 100011 10000 01000 000000 10001 01000 101011 10000 01000 0000000000100000 01000 00000 100000 0000000000110000 Actualmente las computadoras se construyen bajo dos principios clave: 1. Las instrucciones se representan como números. 2. Los programas pueden ser almacenados en memoria para ser leídos o escritos al igual que los números. Estos principios permiten el concepto del programa almacenado, en la figura 2.4 se muestra la potencialidad de este concepto, específicamente la memoria puede contener el código fuente para un programa editor, el correspondiente código máquina compilado, el texto que el programa compilado está usando, y también al compilador que generó el código máquina. Fig. 2.4 El concepto del programa almacenado, ilustrado por medio de un ejemplo. 11 Instrucciones para tomar decisiones Lo que distingue a las computadoras de las simples calculadoras es su capacidad para tomar decisiones basadas en los datos de la entrada. En los lenguajes de alto nivel esta acción se realiza con la expresión if algunas veces acompañada con expresiones goto y etiquetas. La arquitectura MIPS cuenta con dos instrucciones de brincos condicionales. La primera: beq registro1, registro2, L1 compara el contenido del registro1 con el del registro2, si son iguales, la siguiente instrucción será la que se encuentra ubicada en la etiqueta L1 (beq – branch if equal). La segunda instrucción es: bne registro1, registro2, L1 y en este caso el brinco se realizará si los contenidos de los registros no son iguales (bne – branch if not equal). Ejemplo: Compilando una expresión if en un brinco condicional. Para el siguiente código: L1: if ( i == j ) goto L1 f = g + h; f = f – i; Suponiendo que las cinco variables (f a j) se asocian con los registros ($s0 a $s4) ¿Cuál es el código MIPS compilado? Respuesta: L1: beq add sub $s3, $s4, L1 $s0, $s1, $s2 $s0, $s0, $s3 # Se realiza la comparación # Si no fueron iguales se hace la suma # Si ocurrió la igualdad, la siguiente instrucción es la # resta Los compiladores se encargan de colocar etiquetas cuando éstas no aparecen en el código de alto nivel. Esta es otra ventaja de la escritura de programas en lenguajes de alto nivel. Como un complemento a los brincos condicionales, la arquitectura MIPS cuenta con la instrucción: j Etiqueta 12 Por medio de la cual se realiza un salto incondicional, de manera que la siguiente instrucción a ejecutarse es la que se especifica después de la etiqueta. Ejemplo: Compilando una estructura if-then-else. Para el siguiente código: if ( i == j ) f = g + h; else f = g – h; Si nuevamente las se asocian con los registros ($s0 a $s4) ¿Cuál es el código MIPS compilado? Respuesta: El compilador generará una serie de etiquetas en forma automática, de acuerdo al flujo del programa. Una opción es la siguiente: De manera que si i == j se continúa con la suma y luego un salto a la etiqueta exit. En caso de que la igualdad no se cumpla, se hace el salto a la etiqueta else: bne add j Else: add Exit: $s3, $s4, Else $s0, $s1, $s2 Exit $s0, $s1, $s2 # Si no son iguales brinca a la etiqueta Else # Si fueron iguales hace la suma # y salta a la etiqueta Exit. # Si no fueron iguales hace la resta # y termina la decisión. Además de las elecciones entre dos alternativas, con estas instrucciones es posible la ejecución de ciclos repetitivos. 13 Ejemplo: Compilando un lazo simple. Se tiene el lazo en C: Loop: g = g + A[i] i = i + j; if( i != h ) goto Loop; Suponiendo que las variables g, h i y j se asocian con los registros $s1, $s2, $s3 y $s4, respectivamente y que el registro base del Arreglo A es $s5 ¿Cuál es el código MIPS compilado? Respuesta: Primero se requiere obtener el valor de A[i] en un registro temporal: Loop: add add add lw $t0, $s3, $s3 $t0, $t0, $t0 $t0, $t0, $s5 $t1, 0( $t0) # $t0 = i + i = 2i # $t0 = 2i + 2i = 4i # $t0 contiene la dirección de A[i] # $t1 = A[i] Luego se realizan las sumas: add add $s1, $s1, $t1 $s3, $s3, $s4 # g = g + A[i] #i=i+j Por último se hace el brinco condicional: bne $s3, $s2, Loop # Si I != j continúa en el lazo. Las sentencias goto son poco usadas por los entendidos con la programación estructurada, pero con estas instrucciones es posible la compilación de los ciclos: while y do-while. Ejemplo: Compilando un ciclo while. Se tiene el ciclo repetitivo: while (save[i] == k) i = i + j; Si las variables i, j y k se asocian con los registros $s3, $s4 y $s5, respectivamente y que el registro base del Arreglo save es $s6 ¿Cuál es el código MIPS compilado? 14 Respuesta: Para que pueda compararse el valor de save[i], debe obtenerse en un registro temporal: Loop: add add add lw $t1, $s3, $s3 $t1, $t1, $t1 $t1, $t1, $s6 $t1, 0( $t1) # $t1 = i + i = 2i # $t1 = 2i + 2i = 4i # $t1 contiene la dirección de save[i] # $t1 = save[i] Ahora es posible comparar a save[i] con k, si son diferentes, termina el ciclo: bne $t1, $s6, Exit # si save[i] es diferente de k, termina el ciclo. Dentro del ciclo se realiza la suma: add $s3, $s3, $s4 #i=i+j El ciclo se repite: j Loop # Salta a la siguiente iteración Exit: La prueba de igualdad o desigualdad para un salto es la mas popular, sin embargo algunas veces es útil evaluar si una variable es menor que otra, por ejemplo en los ciclos repetitivos for, en los cuales se va incrementando (o decrementando) una variable y se continúa en el ciclo mientras sea menor que otra (o mayor que 0). MIPS cuenta con la instrucción slt (set on less than), que compara dos registros y modifica a un tercero de acuerdo con el resultado de la comparación. Por ejemplo: slt $t0, $s1, $s2 Pondrá un 1 en $t0 si $s1 < $s2, en caso contrario, $t0 contendrá 0. Ejemplo: Prueba de la instrucción slt. ¿Cuál es el código que prueba si una variable a (asociada con $s0) es menor que una variable b (asociada con $s1) y brinca a la etiqueta less si la condición se mantiene? Respuesta: slt bne $t0, $s0, $s1 $t0, $zero, less # $t0 = 1 si a < b y $t0 = 0 en caso contrario # $t0 no es igual a 0, brinca a la etiqueta less Notar que se está aprovechando el hecho de que el registro cero contiene el valor 0. 15 Las estructuras de decisión if-then-else son ampliamente usadas, sin embargo en muchos programas se tiene diferentes alternativas a seguir después de evaluar una expresión. Para ello algunos lenguajes manejan estructuras de decisión múltiple, por ejemplo, la estructura switch-case del lenguaje C (o similares). Se espera que una estructura de este estilo sea más eficiente que múltiples comparaciones individuales. Para conseguirlo, los compiladores deben generar una tabla de direcciones de salto, de manera que se obtenga la dirección destino de la tabla y se realice el salto en forma inmediata. Para tales situaciones, la arquitectura MIPS incluye a la instrucción jr (jump register) la cual realizará un salto incondicional a la dirección contenida en el registro especificado en la instrucción. Ejemplo: Compilando una estructura switch-case. El siguiente código C selecciona entre cuatro alternativas dependiendo si el valor de k es 0, 1, 2 o 3: switch ( k ) { case 0: case 1: case 2: case 3: } f = i + h; break; f = g + h; break; f = g - h; break; f = i - j; break; /* /* /* /* k = 0 */ k = 1 */ k = 2 */ k = 3 */ Suponer que las seis variable f a k corresponden a los registros $s0 al $s5 y que el registro $t2 contiene 4. ¿Cuál es el correspondiente código MIPS? Respuesta: El objetivo es evaluar a la variable k para indexar a la tabla de direcciones, y posteriormente saltar al valor cargado. Pero primero es necesario asegurarse que k está en un caso válido: slt $t3, $s5, $zero # Prueba si k < 0 bne $t3, $zero, Exit # Si k < 0, termina slt $t3, $s5, $t2 # Prueba si k <4 beq $t3, $zero, Exit # Si k >= 4, termina Si el valor de k es válido, para que pueda utilizarse como índice, debe multiplicarse por 4 add add $t1, $s5, $s5 $t1, $t1, $t1 # $t1 = k + k = 2k # $t1 = 2k + 2k = 4k 16 Supongamos que existen cuatro palabras secuenciales en memoria que inician en la dirección contenida en $t4 y contienen la dirección correspondiente a las etiquetas L0, L1, L2 y L3. Para obtener la dirección adecuada para el salto se utilizan las instrucciones: add lw $t1, $t1, $t4 $t0, 0( $t1) # $t1 = dirección de la tabla_de_saltos[k] # $t1 = tabla_de_saltos[k] Un salto a registro desviará el flujo del programa a la opción correspondiente: jr $t0 # salto basado en el registro t0. Las instrucciones que se realizarán en cada caso, de acuerdo con el valor de k son: L0: L1: L2: L3: add j add j sub j sub $s0, $s3, $s4 Exit $s0, $s1, $s2 Exit $s0, $s1, $s2 Exit $s0, $s3, $s4 Exit: # k = 0 => f = i + j # k = 1 => f =g + h # k = 2 => f = g - h # k = 3 => f = i – j # fin del switch-case Resumen: Los operandos en MIPS son: Las instrucciones consideradas hasta el momento del repertorio MIPS son: 17 El lenguaje de máquina para las instrucciones consideradas hasta el momento es: Tarea 3: 1. Obtener el código MIPS de la asignación: x[10] = x[11] + c; Asociar a c con el registro $t1 y suponer que el arreglo x inicia en la dirección 4000. 2. Escriba el código máquina generado para el ejercicio anterior. 3. Con el ensamblador MIPS, indique la secuencia de instrucciones que evalúe a los registros $s0, $s1 y $s2 y deje el valor del menor en $s3. 4. Escriba el código máquina generado para el ejercicio 3. 5. El siguiente código acumula los valores del arreglo A en la variable x: for ( x = 0, i = 0; i < 10; i++ ) x = x + A[i]; ¿Cuál es el código MIPS para este código? Suponga que el comienzo del arreglo A esta en el registro $s3, que el registro $t1 contiene 10, que la variable x se asocia con $s1 y la variable i con $s2. 6. Transforme la siguiente asignación: c = ( a > b ) ? a : b; a código MIPS. Asocie a, b y c con $s0, $s1 y $s2, respectivamente. 18 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 - $v1 : 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 ( jal – jump and link ) Dirección_del_procedimiento 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. 19 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 20 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 los 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. Se dice que un procedimiento es aislado si no invoca a otros procedimientos (como el del ejemplo anterior), en ese caso, para las variables locales se utilizan los registros temporales y no necesitan ser respaldadas en la pila. 21 Procedimientos anidados Es común que un procedimiento invoque a otros procedimientos, en este caso ocurrirá un conflicto si no se busca como manejar 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? ¿Qué pasará con el retorno del procedimiento A? Para que no existan problemas en el manejo de procedimiento anidados 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 22 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 $v0, $zero, 1 # retorno = 1 add $sp, $sp, 8 # Restablece al apuntador de la pila jr $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 (procedure frame). Se destina 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. 23 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 : 24 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 el comienzo del arreglo y. Puesto que el procedimiento es aislado, sólo se emplean registros temporales, para la variable i se utilizará $t0: strcpy: add $t0, $zero, $zero # inicializa a i con cero L1: add add $t1, $a1, $t0 $t2, $a0, $t0 # 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 $t0, $t0, 1 L1 #i=i+1 # otra iteración jr $ra # Finaliza la rutina 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 instrucciones que manejen constantes, después de un reset, una arquitectura podría cargar algunos valores en memoria para utilizarlos 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: 25 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 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 del negativo de la constante. 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: Asociando la variable i con $s0 y a x con $s1: Addi Addi Loop: 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 26 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 será 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 El manejo de constantes grandes no sea muy rápido, pero esto no es tan importante porque 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. 27 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. 28 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). 29 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 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] Ahora es posible hacer la carga de v[k], y de v[k + 1] Lw Lw $t0, 0($t1) $t2, 4($t1) # reg $t0 (temp) = v[k] # reg $t2 = v[k + 1] Finalmente se hace el almacenamiento en memoria. sw sw $t2, 0($t1) $t0, 4($t1) # v[k] = reg $t2 # 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 30 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 Beq $t0, $s0, $a1 $t0, $zero, exit1 # $t0 = 1 si i < n # Si $t0 = 0, termina este ciclo Se continuaría con el interior de este ciclo repetitivo. 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 ciclo for externo es: for1o: Add slt Beq Addi J $s0, $zero, $zero $t0, $s0, $a1 $t0, $zero, exit1 .... .... $s0, $s0, 1 for1o #i=0 # $t0 = 1 si i < n # Si $t0 = 0 (i >= n), termina este ciclo # Cuerpo del primer for # i ++ # Va a la siguiente iteración Exit1: 31 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, 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 J $s1, $s1, -1 for2o # El decremento j— # 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 $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 #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] 32 Beq Addi J $t0, $zero, exit2 .... .... $s1, $s1, -1 For2o # 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 for interno incluye la llamada a la función swap, la cual aparentemente solo requiere de la instrucción: Jal swap Sólo 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 se transfieren $a0 y $a1a registros seguros. Esto se hace con las instrucciones: Add Add $s2, $a0, $zero $s3, $a1, $zero # Se respalda el primer argumento # Se respalda el segundo argumento Luego, ya será posible asignar los parámetros: Add Add $a0, $s2, $zero $a1, $s1, $zero # $a0 = v primer argumento de swap # $a1 = j segundo argumento de swap Es conveniente realizar el respaldo de argumentos en registros seguros al inicio del procedimiento y luego emplear estos registros en el cuerpo del procedimiento. Esto hace posible la modificación de los registros de los argumentos cada vez que sea necesario. 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, en sort se utilizan los registros $s0, $s1, $s2 y $s3, también deben respaldarse en la pila para preservarlos durante la llamada. El prólogo del procedimiento sort es entonces: Addi Sw Sw Sw Sw Sw $sp, $sp, -20 $ra, 16( $sp ) $s3, 12( $sp ) $s2, 8( $sp ) $s1, 4( $sp ) $s0, 0( $sp ) # Hace espacio para 5 registros # 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: 33 Respalda a los registros en la Pila Sort: Addi Sw Sw Sw Sw Sw Respalda parámetros Lazo externo $sp, $sp, -20 # Hace espacio para 5 registros $ra, 16( $sp ) # Salva a $ra en la pila $s3, 12( $sp ) # Salva a $s3 en la pila $s2, 8( $sp ) # Salva a $s2 en la pila $s1, 4( $sp ) # Salva a $s1 en la pila $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 for1o: Slt Beq Addi for2o: Slt Bne Add Lazo interno Add Add Lw Lw Slt Beq Pase de parámetros y Add llama a swap Add Jal Lazo interno Addi J Lazo externo Exit2: Addi J Exit1: Lw Lw Lw Lw Lw Addi Jr $s0, $zero, $zero $t0, $s0, $s3 $t0, $zero, exit1 #i=0 # $t0 = 1 si i < n # Si $t0 = 0 (i >= n), termina $s1, $s0, -1 $t0, $s1, $zero $t0, $zero, exit2 $t1, $s1, $s1 $t1, $t1, $t1 $t2, $s2, $t1 $t3, 0($t2) $t4, 4($t2) $t0, $t4, $t3 $t0, $zero, exit2 #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 $a0, $s2, $zero $a1, $s1, $zero swap # $a0 = v primer argumento de swap # $a0 = j segundo argumento de swap # Llama a swap $s1, $s1, -1 for2o # El decremento j— # El salto a la siguiente iteración $s0, $s0, 1 # El incremento i++ for1o # El salto a la siguiente iteración Restauración de Registros $ra, 16( $sp ) # Recupera a $ra $s3, 12( $sp ) # Recupera a $s3 $s2, 8( $sp ) # Recupera a $s2 $s1, 4( $sp ) # Recupera a $s1 $s0, 0( $sp ) # Recupera a $s0 $sp, $sp, 20 # Recupera la pila Retorno del procedimiento $ra 34 2.10 Apuntadores contra arreglos. El manejo de apuntadores es uno de los aspectos más interesantes en los lenguajes de programación; los apuntadores y los arreglos comparten algunas características, sin embargo, muchas veces los compiladores traducen de manera diferente un programa con arreglos que otro con apuntadores. A continuación se consideran dos versiones de un procedimiento, una con arreglos y la otra con apuntadores, el objetivo del procedimiento es limpiar un arreglo, es decir, colocar 0 en todas sus localidades. void { clear1( int array[], int size ) /* Versión con arreglos */ int i; for( i = 0; i < size; i++) array[i] = 0; } void { clear2( int *array, int size ) /* Versión con apuntadores */ int *p; for( p =&array[0];p < &array[size]; p++) *p = 0; } Versión con arreglos de Clear Los dos argumentos se reciben en los registros $a0 y $a1, respectivamente. La variable i se asocia con $t0 puesto que clear no llama a otros procedimientos. clear1: add $t0, $zero, $zero # se inicializa i = 0 for1: slt beq add add add sw addi j $t1, $t0, $a1 $t1, $zero, fin_for1 $t2, $t0, $t0 $t2, $t2, $t2 $t2, $t2, $a0 $zero, 0($t2) $t0, $t0, 1 for1 # $t1 = 1 si i < size # Si $t1 tiene 0, el for termina # $t2 = 2*i # $t2 = 4*i # $t2 tiene la dirección de array[i] # Limpia la localidad array[i] # i ++ fin_for1: jr $ra Versión con apuntadores de Clear Se tiene la misma asociación de registros con los parámetros y el apuntador p se asocia con el registro $t0 35 clear2: add $t0, $a0, $zero # p apunta al comienzo del arreglo for2: add add add slt beq sw addi j $t1, $a1, $a1 $t1, $t1, $t1 $t1, $t1, $a0 $t2, $t0, $t1 $t2, $zero, fin_for2 $zero, 0($t0) $t0, $t0, 4 for2 # $t1 = 2*size # $t1 = 2*t1 = 4*size # $t1 = &array[size] # $t2 = 1 si p < &array[size] # Si $t2 tiene 0, el for termina # Limpia la localidad apuntada por p # p ++, el apuntador avanza 4 fin_for2: jr $ra En el código anterior se muestra una versión del código MIPS basada en apuntadores, puede observarse que, debido a que la dirección del elemento array[size] no cambia, las tres instrucciones empleadas para calcularlo pueden omitirse en el lazo, produciendo el código: clear2: for2: fin_for2: add $t0, $a0, $zero # p apunta al comienzo del arreglo add add add slt beq sw addi j $t1, $a1, $a1 $t1, $t1, $t1 $t1, $t1, $a0 $t2, $t0, $t1 $t2, $zero, fin_for2 $zero, 0($t0) $t0, $t0, 4 for2 # $t1 = 2*size # $t1 = 2*t1 = 4*size # $t1 = &array[size] # $t2 = 1 si p < &array[size] # Si $t2 tiene 0, el for termina # Limpia la localidad apuntada por p # p ++, el apuntador avanza 4 jr $ra Este código es más rápido porque incluye menos instrucciones en la parte repetitiva. En general, los compiladores capaces de optimizar código intentan manejar los arreglos sumando una variable para obtener la dirección del i-ésimo elemento, en lugar de realizar multiplicaciones. El código resultante será más rápido porque para toda arquitectura, una suma es más rápida que una multiplicación. Si la función clear se aplica sobre un arreglo de 500 elementos, ¿Qué tan rápida es la versión 2 del código con respecto a la versión 1(Suponiendo que se realiza una instrucción en cada ciclo de reloj)? La respuesta a esta pregunta puede obtenerse recordando los aspectos vistos acerca del rendimiento de las computadoras (capítulo 1). 36 Resumen: En la siguiente tabla se muestran detalles de los operandos, en memoria o registros: MIPS operands Name 32 registers Example $s0-$s7, $t0-$t9, $zero, $a0-$a3, $v0-$v1, $gp, $fp, $sp, $ra, $at Memory[0], 230 memory Memory[4], ..., words 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. 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] Y en la siguiente tabla se muestra un resumen con las instrucciones consideradas en este documento. 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 $s1 = $s2 + 100 Used to add constants $s1 = Memory[$s2 + 100] Word from memory to register Memory[$s2 + 100] = $s1 Word from register to memory $s1 = Memory[$s2 + 100] Byte from memory to register Memory[$s2 + 100] = $s1 Byte from register to memory 16 Loads constant in upper 16 bits $s1 = 100 * 2 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 Data transfer Conditional branch Unconditional jump Instruction $s1, $s2, 100 if ($s2 < 100) $s1 = 1; Comments Compare less than constant else $s1 = 0 2500 $ra 2500 go to 10000 Jump to target address go to $ra For switch, procedure return $ra = PC + 4; go to 10000 For procedure call 37 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); } 38 2.11 Un simulador del repertorio de instrucciones En esta sección se describe al simulador SPIM, creado por el Dr. James Larus, graduado en la Universidad de Wisconsin, Madison. Y actualmente investigador de la empresa Microsoft. SPIM es un simulador autónomo para programas en lenguaje ensamblador escritos para los procesadores R2000/R3000, los cuales son procesadores de 32 bits de la corporación MIPS. SPIM lee y ejecuta el código en lenguaje ensamblador, proporciona un depurador simple y un juego simple de servicios del sistema operativo. SPIM soporta casi el conjunto completo de instrucciones del ensamblador-extendido para el R2000/R3000 (omite algunas comparaciones de punto flotante complejas y detalles del sistema de paginación de memoria.). El doctor Larus tiene disponible al programa SPIM para diferentes sistemas operativos, los cuales pueden obtenerse libremente desde su página web situada en: http://www.cs.wisc.edu/~larus/ en la que inmediatamente se encuentra el vínculo al programa SPIM. encuentra el código fuente completo y documentación. También se Es necesario descargar el programa para evaluar al repertorio de instrucciones bajo estudio. En esta sección solo se muestran algunos aspectos del programa SPIM útiles para simular los programas hasta el momento realizados. En la versión para WINDOWS el programa SPIM tiene el aspecto que se presenta en la figura 2.7, en la que se distinguen cuatro ventanas: La primera ventana contiene a los registros, se muestra el valor de todos los registros de propósito general (de $0 a $31), además del Contador del Programa (PC) y de otros registros para el manejo de excepciones (una excepción es un evento erróneo debido a alguna incongruencia durante la ejecución de un programa). También se muestran dos registros HI y LO, estos registros son dedicados a las multiplicaciones y divisiones La segunda ventana contiene una parte de la memoria en la que se colocarán los programas de usuario (el código a evaluar), mostrando las instrucciones en notación simbólica (ensamblador) y en código máquina. En esta ventana se observa al código descrito en el archivo exceptions.s, este código corresponde a una especie de kernel para la máquina e incluye una llamada a la función main, de manera que cualquier programa que se quiera simular deberá incluir al procedimiento principal (main). La idea es que los usuarios avanzados puedan hacer sus propias rutinas para que hagan un manejo diferente de las excepciones. 39 La tercera ventana muestra una parte de la memoria en la que se colocarán los datos, en hexadecimal. Esto incluye una sección de propósito general, una parte dedicada a la pila (stack) y otra que forma parte del Kernel. La cuarta ventana es la ventana de mensajes, en la que se describen los diferentes eventos que van ocurriendo durante la simulación. Fig. 2.7 Aspecto del programa SPIM para Windows 2.11.1 La consola del programa SPIM Además de las cuatro ventanas que se encuentran en la ventana principal del programa, cuando el programa se ejecuta se despliega en pantalla otra ventana conocida como la consola del programa SPIM (ver figura 2.8). La consola es el mecanismo por medio del cual se van a insertar datos al programa o se van a observar algunos resultados del mismo. El Kernel incluido permite el manejo de una instrucción denominada SYSCALL. Con SYSCALL se realiza una llamada al Kernel para solicitar algún servicio, que puede consistir en la captura de un dato o bien la presentación de resultados en la consola. Antes de invocar a SYSCALL, se debe especificar el número de servicio en el registro $V0, y si el servicio requiere argumentos, éstos se deberán colocar en los registros $a0 y $a1, dependiendo del número de argumentos, sin embargo, si el servicio es para números en 40 punto flotante, se utilizará al registro $f0 para el argumento (La arquitectura MIPS incluye 32 registros para el manejo de números en punto flotante, y un hardware dedicado para las operaciones en punto flotante). En la tabla 2.2 se muestran todos los servicios que soporta el Kernel. Fig. 2.8 La consola del Programa SPIM Tabla 2.2 Servicios que nos proporciona el Kernel del Programa SPIM 2.11.2 Pseudo instrucciones. Debido a que el repertorio MIPS es un repertorio de instrucciones reducido, para dar un poco mas de flexibilidad a los programadores, es posible generar un conjunto de pseudo instrucciones; una pseudo instrucción realiza algún tipo de operación, sin embargo no tiene una interpretación directa en Hardware, sino que tiene que traducirse a una o mas instrucciones reales para que pueda ser ejecutada. Así por ejemplo, la pseudo instrucción: move reg_destino, reg_fuente Mueve el registro fuente al registro destino, pero no es una instrucción real, sino que el simulador SPIM la traduce a: ori reg_destino, $zero, reg_fuente 41 Para las multiplicaciones, se puede utilizar la pseudo instrucción: mul $s1, $s2, $s3 esta pseudo instrucción en realidad es traducida en las instrucciones siguientes: mult $s2, $s3 mflo $s1 # Esta es la instrucción que multiplica a $s2 con $s3, pero el # resultado lo deja en los registros HI y LO # Esta instrucción coloca la parte baja del resultado y la # coloca en $s1toma la parte baja del resultado Puede notarse que la pseudo instrucción es suficiente cuando se sabe que el resultado alcanza en un registro de 32 bits. Pero si se están manipulando números muy grandes, además de la pesudo instrucción se debería usar a la instrucción: mfhi $s4 # Para colocar la parte alta en un registro de propósito general La pseudo instrucción mul también puede usarse con el segundo parámetro con un valor inmediato, por ejemplo la pseudo instrucción: mul $t4, $t1, 4 Es traducida a: ori $1, $0, 4 mult $9, $1 mflo $12 Otra pseudo instrucción bastante útil es la siguiente: la $a0, str1 Cuando se codifica un programa y se van a utilizar cadenas constantes, se sabe que éstas se colocarán en memoria, sin embargo se ignora en que dirección serán colocadas, por lo que no se sabría como direccionarlas. Con la se obtiene en el registro $a0 la dirección donde inicia la cadena str1 (la – load address). Esta pseudo instrucción es traducida a dos instrucciones, la primera para cargar la parte alta de la dirección (lui) y la segunda para obtener la parte baja (ori). Existen más pesudo instrucciones, sólo se han mencionado las mas comunes y que son necesarias para el desarrollo de algunos programas que se realizarán para la evaluación del simulador. En el apéndice A del texto “Computer Organization & Design, The hardware/software interface” (página A-51) se encuentra un listado con todas las instrucciones de los procesadores MIPS R2000/R3000 y las pseudo instrucciones que soporta el simulador SPIM. Este apéndice esta disponible en formato PDF en la página del Dr. Larus, su referencia es: http://www.cs.wisc.edu/~larus/HP_AppA.pdf . 42 2.11.3 Ejemplos de uso del simulador: Ejemplo 1: El programa “Hola Mundo” Se trata de un programa que desplegará en la consola a la cadena “HOLA MUNDO”, básicamente se requiere obtener la dirección del inicio de la cadena y solicitar el servicio 4 al kernel (con la instrucción SYSCALL). El código del programa es: main: addi $v0, $zero, 4 la $a0, cadena syscall jr $31 .data cadena: .asciiz # Se usará el servicio 4 # Se obtiene el argumento # Solicita el servicio # Termina la función principal "Hola Mundo" La salida en la consola es: Observaciones: El código se puede escribir con cualquier editor de texto (texto sin formato) y salvarse con cualquier extensión, se sugiere s por asociación con el programa. El código principal debe incluir a la etiqueta main por que en el kernel del programa existe un salto hacia esa etiqueta. La ejecución puede hacerse paso a paso con <F10> o con múltiples pasos con <F11>. O simplemente con el comando go, con <F5>. Una vez que finalice el código de usuario, si se continúa ejecutando, se regresa del main al Kernel y solicita el servicio 10 (exit). Ejemplo 2: Un programa que suma dos números. En este ejemplo se usará a la consola para obtener dos enteros, luego los enteros se sumarán y se mostrará el resultado. 43 El código del programa: main: addi $v0, $zero, 4 la $a0, str1 syscall # Servicio 4 # se imprime una cadena # para pedir un número addi $v0, $0, 5 syscall add $8, $0, $v0 # Servicio 5 # se lee el número # se coloca en $8 addi $v0, $zero, 4 la $a0, str2 syscall # Servicio 4 # se imprime una cadena # para pedir el otro número addi $v0, $0, 5 syscall add $9, $0, $v0 # servicio 5 # se lee el otro numero # se coloca en $9 addi $v0, $zero, 4 la $a0, str3 syscall # Servicio 4 # para indicar que se # dará el resultado add $a0, $8, $9 addi $v0, $0, 1 syscall # Se coloca la suma como argumento # Servicio 1 # se muestra el resultado addi $v0, $zero, 4 la $a0, str4 syscall # Servicio 4 # muestra una cadena de # terminación del programa jr # fin del main $31 .data str1: str2: str3: str4: .asciiz "Dame un numero: " .asciiz "Dame otro numero: " .asciiz "La suma de los numeros es : " .asciiz "\n\nFin del programa, Adios . . ." Una corrida generó en la consola: 44 Ejemplo 3: El factorial de un número. Este programa está basado en la función recursiva que se presentó en la sección 2.5 (Soporte de procedimientos) sólo se le hicieron algunas modificaciones para el manejo correcto de las constantes. En este programa se utilizó la pseudo instrucción li (por load immediate) para cargar una constante en un registro, que es equivalente a hacer una operación OR del registro 0 con la constante y colocar el resultado en el registro que se quiere cargar. El código del programa: main: addi sw $sp, $sp, -4 $ra, 4 ($sp) li $v0, la $a0, syscall li $v0, syscall add $s0, # Hace espacio en la Pila # Salva la dirección de retorno 4 str1 # Salida a la consola 5 # Lectura de un numero $0, $v0 # El numero esta en $v0, se copia a $s0 add jal $a0, $0, $s0 fact # Prepara el parametro # Llama al factorial add $s1, $v0, $zero # Respalda el resultado li $v0, la $a0, syscall addi $v0, add $a0, syscall 4 str2 # Una cadena a la consola $0, 1 $s0, $zero # Una entero a la consola li $v0, la $a0, syscall addi $v0, add $a0, syscall li $v0, la $a0, syscall 4 str3 # Una cadena a la consola $0, 1 $s1, $zero # Una entero a la consola 4 str4 lw addi jr $ra, 4 ($sp) $sp, $sp, 4 $31 addi sw sw $sp, $sp, -8 $ra, 4 ($sp) $a0, 0 ($sp) fact: # Recupera la dirección de retorno # Restablece el tope de la Pila # # # # La funcion que obtiene el factorial Hace espacio en la Pila Salva la dirección de retorno Salva al argumento n 45 # Se evalúa para ver si ocurre el caso base (cuando n < 1): slti $t0, $a0, 1 # $t0 = 1 si n < 1 beq $t0, $zero, L1 # salta a L1 si no ocurre el caso base # 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. addi addi jr $v0, $zero, 1 $sp, $sp, 8 $ra # retorno = 1 # Restablece al apuntador de la pila # Finaliza regresando el resultado en $v0 # Si no ocurre el caso base, prepara la llamada recursiva L1: addi $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 addi $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 str1: str2: str3: str4: .data .asciiz .asciiz .asciiz .asciiz "Dame un numero: " "El factorial del numero " " es : " "\n\nFin del programa, Adios . . ." Como ejemplo, se obtuvo el factorial de 7. Ejemplo 4: Manejo de un arreglo. En este ejemplo se pretende mostrar como un arreglo de n enteros puede ser ubicado en la pila. Para ello solo se piden algunos enteros al usuario para luego imprimirse en pantalla en orden inverso. 46 El código C que realiza las actividades deseadas es: void main() { int i, n, *A, *t; printf("Manejo de un arreglo\n"); printf("Indica el tamaño del arreglo: "); scanf("%d", &n); A = (int *) malloc(n*sizeof(int)); for( t = A, i = 0; i < n; i++, t++) { printf(" Dame el dato %d : ", i); scanf("%d", t); } printf("\nLos números en orden inverso: \n"); for( t = &A[n-1], i = 0; i < n; i++, t--) printf(" %d\n", *t); free(A); } Las variables i, n, A y t se asocian con los registros: $t0, $t1, $t2 y $t3 (este procedimiento es aislado). El código MIPS correspondiente al código C anterior es: # Programa que maneja un arreglo imprimiendo los elementos en orden inverso main: # El Primer Mensaje li $v0, 4 la $a0, str1 syscall #Mensaje que pide el tamaño del arreglo li $v0, 4 la $a0, str2 syscall #Se toma el valor li $v0, 5 syscall add $t1, $v0, $0 #El tamaño del arreglo se guardo en $t1 # Se reserva el espacio en memoria mul $t4, $t1, 4 sub $sp, $sp, $t4 addi $t2, $29, 0 #inicia el primer for: addi $t0, $0, 0 addi $t3, $t2, 0 for1: #en $t4 esta el total de bytes requeridos en la pila #Se hace espacio en la pila para los datos #$t2 es el apuntador al comienzo del arreglo # i = 0 # t = A slt $t5, $t0, $t1 beq $t5, $zero, fin_for1 #Se pide el número li $v0, 4 la $a0, str3 syscall #imprime el indice del numero 47 li $v0, 1 add $a0, $0, $t0 syscall #imprime los dos puntos li $v0, 4 la $a0, str4 syscall #Toma el valor de la consola li $v0, 5 syscall sw $v0, 0($t3) addi $t0, $t0, 1 addi $t3, $t3, 4 #i ++ #t ++ j for1 #En este ciclo se pidieron los Numeros fin_for1: #El mensaje de aviso li $v0, 4 la $a0, str5 syscall # Inicio del segundo for addi $t0, $0, 0 # i = 0 addi $t4, $t1, -1 # $t4 = n - 1 mul $t4, $t4, 4 # $t4 = 4 * (n - 1) add $t3, $t2, $t4 # t = &A[n-1] for2: slt $t5, $t0, $t1 beq $t5, $zero, fin_for2 #imprime el numero lw $t6, 0($t3) li $v0, 1 add $a0, $0, $t6 syscall # Primero lo carga #imprime el retorno de carro li $v0, 4 la $a0, str6 syscall addi $t0, $t0, 1 addi $t3, $t3, -4 #i ++ #t -- j for2 fin_for2: mul $t4, $t1, 4 add $sp, $sp, $t4 jr $ra str1: str2: str3: str4: str5: str6: #en $t4 esta el total de bytes requeridos en la pila #Se libera el espacio solicitado en la pila #fin de la función main #Cadenas del programa .data .asciiz " Manejo de un arreglo \n" .asciiz "Indica el tamaño del arreglo : " .asciiz "Dame el número " .asciiz " : " .asciiz "\nLos numeros en orden inverso son: \n .asciiz "\n " 48 " Los resultados arrojados en la consola para una corrida del programa son: 2.11.4 Configuración del programa PSIM El programa PSIM permite configurar algunos parámetros; la consola de configuración se encuentra en la opción settings del menú simulator, y tiene la siguiente forma: En la ayuda del programa se describe el objetivo de cada una de las opciones de configuración. El programa funciona correctamente con los valores preestablecidos. 49 Práctica de Laboratorio Los siguientes ejercicios son para familiarizarse con el simulador SPIM y se considerarán como la práctica 1 del segundo parcial, para cada ejercicio se deben incluir: i. Comentarios en el archivo con el código MIPS. ii. El código que envíe los nombres de los integrantes del equipo, al iniciar o finalizar el programa. Ejercicios: 1. Realice un programa que solicite 3 números e indique cual es el menor de ellos. 2. Realice la función principal para el programa SORT y simúlelo, recordar que debe incluirse a la función SWAP, el número de elementos deberá ser solicitado al usuario (sug. Puede usarse la pila para almacenar el arreglo). 3. Realice un programa que obtenga el menor y el mayor de un arreglo de n elementos proporcionados por el usuario (sug. Acondicione la función desarrollada en la tarea anterior). 4. Con base en la tarea anterior, realice un programa que obtenga al n-ésimo término de la serie de Fibonacci. 50