Unidad 2: Soporte de procedimientos, cadenas, constantes, etc.

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