Arquitectura del Repertorio de Instrucciones.

Anuncio
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
Descargar