PCSpim

Enviado por Programa Chuletas y clasificado en Informática y Telecomunicaciones

Escrito el en español con un tamaño de 79,07 KB

Índice:
Práctica 1. Estudio del Simulador PCSpim 3
Introducción 3
¿Dónde puedo encontrar el PCSpim? 3
Instalación del programa PCSpim 4
Descripción de la interfaz gráfica 5
Opciones del simulador 8
Sintaxis del ensamblador 9
Escritura y carga de programas 12
Ejecución de programas 14
Depuración de programas 15
Práctica 2. Ejercicios de programación en ensamblador del MIPS R2000 18
Juego de Instrucciones 18
Instrucciones artimético - lógicas 18
Instrucciones de manipulación de constantes 18
Instrucciones de comparación 19
Instrucciones de salto condicional e incondicional 19
Instrucciones de carga 20
Instrucciones de almacenamiento 20
Instrucciones de movimiento de datos 20
Instrucciones de coma flotante 21
Llamadas al sistema 23
Conexión de un terminal al procesador MIPS R2000 24
Ejemplos 27
Ejemplos de uso de las directivas. Gestión de memoria (segmento de datos) 27
Ejemplos de uso de instrucciones aritmético - lógicas 30
Ejemplos de uso de instrucciones de salto. Condiciones y bucles. 31
Ejemplos de uso de instrucciones en coma flotante 32
Ejemplos de llamadas al sistema 33
Ejemplos de llamadas a subrutinas. Llamadas recursivas. Paso de parámetros a través de la pila 34
Bibliografía y referencias adicionales 40

Práctica 1. Estudio del Simulador PCSpim
Introducción

Los/las estudiantes de ingeniería actuales deben, de manera imprescindible, conocer cómo se organizan y funcionan los computadores. Y esto por dos motivos. El primero, porque el computador va a ser una herramienta de trabajo diaria durante sus años de estudio. El segundo, porque será también, con toda seguridad, la herramienta de trabajo futura cuando ejercite su labor como ingeniero/a.

El tercer capítulo de la asignatura de Fundamentos de Computadores está dedicado al estudio del lenguaje ensamblador. Creemos que este aspecto es muy importante porque pone al alumno en el papel del programador, y lo hace en un lenguaje muy cercano al de la propia máquina. El lenguaje utilizado corresponde, como se ha mencionado, al del procesador MIPS R2000, siendo bastante parecido al de la mayoría de procesadores diseñados en la actualidad por otros fabricantes según los principios de la filosofía RISC. Además, se dispone de un simulador del procesador que se ejecuta en entorno Windows, y que ayuda de manera muy eficaz al aprendizaje de este lenguaje por parte del alumno/a.

El presente documento pretende presentar al alumno/a un lenguaje ensamblador, el correspondiente al MIPS R2000, enseñar las reglas, convenios, sintaxis, y juego de instrucciones.
¿Dónde puedo encontrar el PCSpim?

El simulador PCSpim puede, como casi todo, encontrarse en Internet en la página que su autor, James R. Larus, tiene en la Universidad de Wisconsin - Madison:

http://www.cs.wisc.edu/~larus/larus.html

En ella, a parte de documentación adicional, pueden hallarse los ejecutables para distintos sistemas operativos:

Unix http://www.cs.wisc.edu/~larus/SPIM/spim.tar.zUnix http://www.cs.wisc.edu/~larus/SPIM/spim.tar.z
http://www.cs.wisc.edu/~larus/SPIM/spim.tar.gz
DOS http://www.cs.wisc.edu/~larus/SPIM/spimdos.exe DOS http://www.cs.wisc.edu/~larus/SPIM/spimdos.exe
Windows http://www.cs.wisc.edu/~larus/SPIM/spimwin.exe Windows http://www.cs.wisc.edu/~larus/SPIM/spimwin.exe
Instalación del programa PCSpim

La instalación del programa PCSpim se realiza mediante la ejecución del programa spimwin. exe , cuyo icono puede verse en la imagen de la izquierda.
Haciendo doble click sobre el icono, aparece el diálogo que se muestra en la Figura 1, donde pulsando sobre Setup , se lanza la instalación. La única información adicional, que se precisa, es el directorio o carpeta en la que situar el programa ejecutable (ver Figura 2)



Figura 1. Diálogo auto-extractor del programa PCSpim
Figura 2. Petición de la carpeta destino
La carpeta donde queda instalado el programa PCSpim tendrá un aspecto como el que puede verse en la Figura 3. Los iconos más significativos en esta carpeta son:

Ejecutable Ayuda Manejador de excepciones


Figura 3. Programa PCSpim
Descripción de la interfaz gráfica
Figura 4. Ventana principal del programa PCSpim

La ventana principal del programa PCSpim puede verse en la figura anterior. Como cualquier programa Windows dispone de unos elementos básicos, menús, barra de estado y botones de función, que facilitan su uso y nos informan de la situación en la que se encuentra el programa.


Figura 5. Menú y botones de acceso rápido a funciones

En la parte superior de la ventana (Figura 5) puede verse el menú asociado a la aplicación cuyas opciones y subopciones se muestran en la siguiente tabla:

Opción Utilidad Opción Utilidad
File
Simulator
Window
Help

De forma más desglosada:

Opción File Opción File
Open ... Abrir un fichero de extensión .s ó .asm Open ... Abrir un fichero de extensión .s ó .asm
Save log file ... Volcado a fichero de la situación de un fichero
Exit Salir del programa
Opción Simulator Opción Simulator
Clear Registers Inicializar los registros
Reinitialize Inicializar registros y segmentos de memoria
Reload Recargar el último fichero abierto
Go Ejecutar un programa
Continue / Break Continuar la ejecución / Abortar la ejecución
Single step Ejecutar una instrucción
Multiple step Ejecutar múltiples instrucciones
Breakpoints Gestión de puntos de ruptura
Set Value Establecer un valor en una dirección o registro general
Display symbol table Mostrar la tabla de símbolos
Settings Opciones del simulador
Opción Window Opción Window
Next Ventana siguiente
Previous Ventana previa
Cascade Ordenación de las ventanas abiertas en cascada
Tile Muestra las ventanas abiertas de forma apilada
Arrange Icons Organiza las ventanas abiertas y minimizadas
Messages Visualizar la ventana asociada a mensajes
Text segment Visualizar la ventana asociada al segmento de código
Data segment Visualizar la ventana asociada al segmento de datos
Registers Visualizar registros (generales y coma flotante)
Console Visualizar consola de salida
Clear console Limpiar consola
Toolbar Visualizar barra de herramientas
Status bar Visualizar barra de estado
Opción Help Opción Help
Help Topics Ayuda
About PCSpim Acerca de ...

Opciones del simulador

El simulador PCSpim ofrece diferentes opciones modificables a través de la opción del menú Simulator ® Settings. PCSpim utiliza estas opciones para determinar como cargar y ejecutar los programas, por ello es importante verificar su valor antes de intentar cargarlos. La ventana que aparece cuando seleccionamos la opción Settings es la siguiente:


Figura 6. Opción Simulator - Settings

Hay opciones que afectan a la visualización y a la ejecución de los programas. En cuanto a la visualización, y respectivamente, se disponen de las opciones:

Save window positions. Esta opción da la posibilidad de guardar la posición de las ventanas al salir, y recuperar dicha posición al volver a entrar a PCSpim

General registers in hexadecimal.
Esta opción da la posibilidad de visualizar el contenido de los registros generales en hexadecimal (seleccionado) o en decimal (no seleccionado).

Floating point registers in hexadecimal.
Similar a la opción anterior con los registros de la unidad coma - flotante.

En cuanto a la ejecución las opciones posibles son:

Bare machine.
Simula el hardware pelado. Sin pseudoinstrucciones o modos de direccionamiento provistos por el simulador. Normalmente estará desactivada para facilitar la labor del programador y disponer de mayor flexibilidad.

Allow pseudo instructions.
Determina si se admiten pseudoinstrucciones. En este caso será preferible tener la opción marcada para permitir todas las pseudoinstrucciones.

Load trap file.
Determina si se carga el manejador de interrupciones, que por defecto es el fichero indicado en la caja de texto que existe inmediatamente debajo de esta opción (fichero trap.handler que acompaña al simulador). Si este fichero se carga al producirse una excepción, PCSpim salta a la dirección 0x80000080, que contiene el código ne esario para tratar la excepción. La única recomendación cuando se carga el manejador de interrupciones es que la etiqueta que marca el comienzo del programa debe ser main en lugar de __start, ya que esta se utiliza en el manejador de excepciones y si vuelve a utilizarse surge un conflicto de identificador y al intentar cargar el programa aparece el siguiente mensaje de error mostrado en la Figura 7, donde el propio simulador da la opción de verificar las opciones de carga establecidas.


Figura 7. Error en la carga de un programa por duplicidad de etiquetas
Mapped I/O: Permite la posibilidad de entrada/salida mapeada en memoria. Con ella PCSpim simula un dispositivo de entrada/salida: un terminal teclado pantalla. La información se introduce y se muestra usando la ventana Console.

Quiet.
Si está activa el simulador no muestra ningún mensaje cuando se producen excepciones.
En la barra de estado de la ventana principal de la aplicación PCSpim, en la parte inferior izquierda, pueden verse el valor de las principales opciones que presenta el simulador, si están seleccionadas estarán a 1, sino lo están, estarán a 0.

Sintaxis del ensamblador

Las principales reglas de escritura de programas en ensamblador para el MIPS R2000 son las siguientes:

Cada instrucción debe ir en una sola línea, pero aún así se pueden dejar líneas en blanco o que sólo contengan comentarios
Los comentarios comienzan con el símbolo ‘#’ y ocupan desde él hasta el final de la línea.
El aspecto general de cualquier programa escrito en ensamblador del MIPS R2000 es el siguiente:

[etiqueta:] <código de operación><operandos> [#comentario]

Los corchetes denotan opcionalidad. Cada línea de un programa en ensamblador puede comenzar con una etiqueta, secuencia de caracteres alfanuméricos, subrayados y puntos, que no comienzan con un número. Estas etiquetas sirven para hacer referencia a la dirección de memoria donde se ubican los datos o la instrucción correspondiente a esa línea. Etiquetas de uso habitual son, por ejemplo, la de __start utilizada por el simulador PCSpim para referenciar a la primera instrucción a ejecutar, siempre y cuando no se cargue el manejador de interrupciones, en cuyo caso la instrucción que se busca es la etiquetada con main . Las etiquetas son de gran utilidad para realizar saltos, en la implementación de bucles y en la llamada a subrutinas.

Los códigos de operación son palabras reservadas y, por lo tanto, no pueden utilizarse como identificadores válidos de objetos en nuestros programas. Existen dos tipos de códigos de operación, por un lado, las directivas y por otro las instrucciones.

Las directivas sirven para indicarle al ensamblador de qué manera debe tratar los datos y/o las instrucciones que vienen a continuación. En el proceso de ensamblado desaparecen, no generan ninguna traducción máquina. Este término no debe confundirse con el de pseudoinstrucción, que son instrucciones ofrecidas por el ensamblador, y que son traducidas a instrucciones reales (una o más), con el fin de hacer más fácil la labor del programador. Las directivas más importantes que vamos a utilizar son las que se muestran en la Tabla 1.

Directiva Significado Directiva Significado
.align n Alinea el siguiente dato sobre un límite 2n bytes. Si se usa el valor 0 se desactiva automáticamente la alineación de las directivas .half , .word , .float y .double hasta la siguiente directiva .data . .align n Alinea el siguiente dato sobre un límite 2n bytes. Si se usa el valor 0 se desactiva automáticamente la alineación de las directivas .half , .word , .float y .double hasta la siguiente directiva .data .
.ascii str Almacena la cadena str en memoria, pero no la termina con un carácter nulo. La cadena str debe estar entre comillas dobles.
.asciiz str Almacena la cadena str en memoria terminándola con un carácter nulo. La cadena str debe estar entre comillas dobles.
.byte b1, b2, ..., bn Almacena los n valores en bytes consecutivos en memoria.
.data Los elementos declarados tras esta directiva son almacenados en el segmento de datos, concretamente a partir de la dirección expresada en . Si el argumento no aparece se tomará la dirección por defecto.
.double d1, d2, ..., dn Almacena los n valores en coma flotante de doble precisión (64 bits) consecutivamente en memoria.
.end Fin de programa
.float f1, f2, ..., fn Almacena los n valores en coma flotante de simple precisión (32 bits) consecutivamente en memoria.
.globl simbolo Declara simobolo como global y puede ser referenciado desde otros archivos.
.half h1, ..., hn Almacena los n valores en medias palabras (16 bits) consecutivos en memoria.
.space n Reserva n bytes de espacio en el segmento de datos.
.text Los elementos declarados tras esta directiva son almacenados en el segmento de código, concretamente a partir de la dirección . Estos elementos sólo pueden ser instrucciones o palabras (.word). Si el argumento no se utiliza, se tomará la dirección por defecto.
.word w1, w2, ..., wn Almacena los n valores en palabras (32 bits) sucesivas en memoria.
Tabla 1. Directivas del lenguaje ensamblador MIPS R2000

Las instrucciones son las que verdaderamente componen el programa, implementando la funcionalidad que será ejecutada por el procesador.

Acompañando, tanto a instrucciones como a las directivas, están las correspondientes argumentos. En el caso de las instrucciones lo habitual es encontrar registros, datos inmediatos o referencias mediante el empleo de etiquetas. Los registros pueden accederse indistintamente a través de su nombre numérico , $0 a $31, o bien a través de un nombre, que hace referencia al tipo de uso recomendado para el registro. Los registros del procesador MIPS pueden verse en la Tabla 2.

Número de registro Nombre Utilización Número de registro Nombre Utilización
0 $zero Constante 0
1 $at Reservado para el ensamblador
2-3 $v0-v1 Ubicación del resultado de una función
4-7 $a0-$a3 Paso de argumentos a subrutinas
8-15, 24, 25 $t0-$t7, $t8, $t9 Variables temporales
16-23 $s0-$s7 Variables permanentes
26-27 $k0-$k1 Registros reservados para uso del núcleo del S.O.
28 $gp Puntero al área global de datos
29 $sp Puntero de pila
30 $fp Puntero de marco
31 $ra Dirección de retorno
Tabla 2. Denominación y uso de los registros del MIPS R2000
Dentro del campo operandos vamos a encontrar los argumentos que se le pasan tanto a directivas como a instrucciones. Y el modo con el que se hace referencia a esos argumentos es lo que se conoce como modo de direccionamiento. Los modos de direccionamiento son las distintas maneras que puede utilizar el procesador para acceder a los operandos de una instrucción. Para que un programador sea capaz de diseñar programas para un arquitectura dada debe conocer qué juego de instrucciones tiene el procesador y qué modos de direccionamiento puede utilizar para acceder a esos datos.

La arquitectura MIPS, en el nivel más bajo, ofrece un único modo de direccionamiento, que denominaremos modo real. Este modo consiste en sumar algo, al contenido de un registro para obtener la dirección del operando. Según sea ese algo se obtienen distintos modos de direccionamiento, que denominaremos virtuales, lo cuales son utilizados por el programador de la máquina. Estos modos de direccionamiento virtuales, que son los que utilizaremos en nuestros programas se especifican a continuación:

Modos de direccionamiento Ejemplos Modos de direccionamiento Ejemplos
Direccionamiento a registro sub $8, $10, $22
Direccionamiento inmediato addi $10, $10, 4
Direccionamiento base o desplazamiento lw $10, etiqueta($11)
Direccionamiento relativo al contador de programa beq $10, $8, seguir


Escritura y carga de programas

El simulador PCSpim no implementa una opción de edición con lo que los programas deben editarse fuera de línea, para ello puede utilizarse cualquier editor de textos (por ejemplo el bloc de notas), siempre y cuando guardemos el programa editado con la opción sólo texto, para que pueda ser cargado por el simulador posteriormente.

Para ir mostrando las posibilidades que ofrece PCSpim podemos utilizar el bloc de notas para editar y simular el siguiente programa en ensamblador MIPS:

.data 0x10000000
.asciiz “Fundamentos”
.text
.globl __start
__start: lui $20, 0x1000 # cargar parte alta de dirección
ori $20, 0x0000 # cargar parte baja de dirección
addi $21, $0, 0 # se utiliza $21 para ir contando
bucle: lb $19, 0($20) #leemos cada byte
beq $19, $0, fin #comprobamos que no es nulo
addi $21, $21, 1 #incrementamos en 1
addi $20, $20, 1 #pasamos al siguiente byte
j bucle #volvemos al bucle
fin: .end #fin del programa

El programa cuenta la longitud de la cadena de caracteres “Fundamentos” terminada con carácter nulo (“\\0”), situada a partir de la dirección 0x10000000. La longitud de la cadena se almacena en el registro $21.

Una vez editado el programa con cualquier procesador de textos, se procede a su simulación con PCSpim, para ello se carga el fichero con la opción del menú File - Open, esta opción ensambla el programa editado, lo carga en memoria, chequea las etiquetas gestionando una tabla de símbolos, traduce pseudoinstrucciones a instrucciones reales, y hace un análisis sintáctico y semántico del programa editado.

En caso de encontrar errores nos informa de ellos, mostrándonos un diálogo en el que se nos indica el error cometido y su localización dentro del fichero editado, si existen errores subsanables mediante la modificación de determinadas opciones de carga (Simulator - Settings) también se nos da la opción de modificarlas en ese momento y volver a realizar la carga del programa.

Si el programa se carga adecuadamente un mensaje nos informa de ello en la ventana Messages. En las ventanas Data Segment, Text Segment y Registers se muestra la información relativa al segmento de datos, segmento de código y registros respectivamente.

El aspecto que tendría la ventana principal una vez cargado el programa anterior en el PCSpim sería el siguiente:



Aparte de los elementos ya mencionados, y característicos de cualquier aplicación para Windows, pueden observarse diferentes ventanas hijas dispuestas, en este caso, de forma apilada (Tile). De arriba a abajo pueden verse, primero la ventana asociada al segmento de datos y de pila.

La parte reservada al segmento de datos está etiquetada por DATA, y es donde se introducen, inicialmente, los datos estáticos durante el proceso de ensamblado y, posteriormente, los datos dinámicos creados o almacenados durante la simulación del programa. Se representan las posiciones ocupadas, en grupos de cuatro palabras consecutivas a partir de la dirección dispuesta más a la izquierda, en hexadecimal y notación big-endian.

En esa misma ventana puede verse la información almacenada en la pila, a partir de STACK. La zona de memoria que puede utilizar el programa como pila se sitúa desde el extremo superior de la memoria (dirección 0x7fffeffc en modo usuario) y se expande hacia posiciones decrecientes de memoria. El acceso a esta zona de memoria se produce a través del puntero de pila (registro $29). La zona intermedia entre el segmento de pila y el de datos va variando dinámicamente a medida que se ejecuta el programa, es una zona utilizada para almacenar información de forma temporal, guardar un contexto, pasar parámetros o devolverlos desde una subrutina (procedimiento o función).

La segunda de las ventanas que puede verse en la última figura es el segmento de código (Text Segment). Cada línea en esta ventana tiene el siguiente formato:

[0x00400000] 0x3c1410000 lui $20, 4096 ; 5: lui $20, 0x1000

de izquierda a derecha, primero se encuentra la dirección donde está almacenada la instrucción, luego la codificación de la instrucción en hexadecimal, posteriormente la instrucción, y finalmente, después de un punto y coma, un número que se corresponde con la línea que ocupaba la instrucción en nuestro fichero .s y la instrucción que nosotros pusimos. En la mayoría de las ocasiones puede ocurrir que la instrucción que nosotros utilizamos y la que aparece sea la misma, es cuando utilizamos instrucciones reales, implementadas por hardware. En otros casos al utilizar pseudoinstrucciones podremos obtener para una instrucción varias instrucciones reales que consiguen realizar el mismo propósito, esto se realiza de forma transparente al usuario, la labor de conversión la lleva a cabo el ensamblador.











La siguiente ventana que aparece es la asociada a los registros, en ella aparecen reflejados desde el contador de programa (PC), o los registros HI y LO, a los 32 registros de uso general, los registros de la unidad de coma flotante, y los registros del coprocesador 0, utilizados para, entre otras cosas, gestionar la ocurrencia de excepciones. El valor de estos registros puede verse en hexadecimal o en decimal gracias a la opción (General registers in hexadecimal y Floating point registers in hexadecimal en Simulator - Settings ).

La última de las ventanas es la de mensajes, en ella, inicialmente, aparece información de la versión, autor y copyright y conforme se ejecuta el programa informa de la instrucción realizada y de posibles excepciones que puedan producirse, esto último, en función del estado de la opción -quiet en Simulator - Settings.

Ejecución de programas

Si el proceso de carga a finalizado con normalidad, las cuatro ventanas de la parte central de la aplicación contendrán la información comentada anteriormente.

La ejecución de un programa puede realizarse de dos formas distintas, una es sin pausa alguna, utilizando la opción Simulator - Go (o pulsando el botón ), e indicando en el diálogo que se muestra en la Figura 8 la dirección o etiqueta donde comienza el programa principal.


Figura 8. Diálogo para lanzar la ejecución de un programa
Esta forma de ejecutar un programa es poco intuitiva ya que el programa se ejecuta sin pausa alguna, a no ser que se hubiera establecido algún tipo de punto de ruptura o breakpoint. Acción que se realiza mediante la opción Simulador - Breakpoints (o ) que muestra un diálogo similar al siguiente



donde se puede introducir una etiqueta o dirección asociada a una instrucción, que será sustituida por una instrucción break $1 cuando el simulador alcance esta instrucción reconocerá un punto de ruptura y preguntará si se desea proseguir la ejecución,

en caso afirmativo, el simulador continua la ejecución sin pausa alguna hasta alcanzar el final del programa o bien, hasta llegar a otro punto de ruptura.

Depuración de programas

Otra forma de ejecutar un programa, útil para llevar a cabo la depuración del mismo o simplemente para comprobar el funcionamiento del simulador más detenida y detalladamente, es instrucción a instrucción (opción Simulator - Single Step) o un número determinado de instrucciones de golpe (Simulator - Multiple Step). Junto con los puntos de ruptura comentados en el apartado anterior son las herramientas básicas para llevar a cabo la depuración de errores observados al ejecutar un programa.

Los errores se nos pueden presentar en dos fases. La primera puede tener lugar en la carga de un programa, en estos casos Pcspim nos informa mediante un diálogo de los errores detectados, estos errores son de tipo sintáctico o semántico y la solución de los mismos pasa por editar de nuevo el fichero asociado al programa y subsanar el error/es cometido/s. Errores habituales en esta fase de carga pueden ser muchos, entre los más habituales pueden comentarse:

Utilización de mayúsculas en códigos de operación
Cargar el manejador de interrupciones y utilizar la etiqueta __start.
No cargar el manejador de interrupciones y utilizar la etiqueta main.
Hacer uso de registros reservados para el ensamblador o el núcleo del S.O.
Utilizar instrucciones con un número incorrecto de operandos.

La segunda situación en la que un error puede detectarse es durante la ejecución del programa, es decir, el programa no realiza lo que deseamos, es entonces cuando se puede recurrir a la ejecución paso a paso y a la introducción de puntos de ruptura en puntos críticos del mismo, con el fin de detectar la causa del error. Errores típicos en esta fase de ejecución pueden ser:

Paso incorrecto de parámetros
Gestión incorrecta de la pila
Uso incorrecto de los modos de direccionamiento
Fallo en el acceso a los datos
Estructuras de control mal gestionadas
No salvaguardar los registros utilizados al llamar a subrutinas

De todas formas conviene recapitular y asentar conceptos que pueden ser de gran ayuda cuando estemos realizando cualquier fichero fuente para su posterior simulación usando el simulador PCSpim. A modo de resumen podemos seguir el siguiente ciclo de edición:

Edición del fichero fuente.
Ser cuidadoso en la sintaxis
Si estás utilizando la versión 6.0 de PCSpim la última línea debe acabar con
Los registros puedes referirte a ellos con su nombre o con su número asociado pero siempre se preceden del símbolo $.
Hay que tener también cuidado con el uso de las etiquetas, por ejemplo, la etiqueta principal de nuestro programa que es __start, siempre que no esté cargado el manejador de excepciones en cuyo caso es main.
Los códigos de operación son sensibles al uso de mayúsculas y minúsculas. Son siempre en minúsculas.
Los datos inmediatos pueden usarse en decimal o en hexadecimal (precediendo a la cifra con 0x)
Toda línea en ensamblador del MIPS R2000 sigue el siguiente formato:
[etiqueta:] [# comentario]

Cargar el fichero fuente (Opción File -> Open)
Según tenga éxito o no el proceso de ensamblado ocurre lo siguiente:
Si el ensamblado tiene éxito: Las diferentes ventanas asociadas a los segmentos de datos y código se rellenan con datos e instrucciones respectivamente. El PC (registro - contador de programa) toma el valor de la dirección correspondiente a la primera instrucción de nuestro programa. Y el programa estará listo para ser simulado (opciones Go, Single Step, Multiple Step).
Si el ensamblado no tiene éxito: el propio simulador indica en que línea o líneas pueden encontrarse el error o errores en nuestro fichero fuente. Da, también, el propio simulador la posibilidad de abrir el diálogo asociado a la opción Settings lo que nos puede permitir subsanar errores relacionados con el uso de la etiqueta __start/main. Deberíamos en este caso volver al paso 2 y revisar el fichero fuente editado, teniendo en cuenta las recomendaciones comentadas anteriormente.

ódigo>Práctica 2. Ejercicios de programación en ensamblador del MIPS R2000
Juego de Instrucciones

Este apartado presenta las instrucciones que con más frecuencia utilizaremos en la programación del procesador MIPS R2000.

Instrucciones artimético - lógicas
addadd Rdest, Rsrc1, Src2 Suma (con overflow)
addiaddi Rdest, Rsrc1, Inm Suma inmediata (con overflow)
adduaddu Rdest, Rsrc1, Src2 Suma (sin overflow)
addiuaddiu Rdest, Rsrc1, Inm Suma inmediata (sin overflow)
andand Rdest, Rsrc1, Src2 And
andiandi Rdest, Rsrc1, Inm And inmediata
divdiv Rsrc1, Rsrc2 División (con signo)
divudivu Rsrc1, Rsrc2 División (sin signo)
multmult Rsrc1, Rsrc2 Multiplicación (con signo)
multumultu Rsrc1, Rsrc2 Multiplicación (sin signo)
nornor Rdest, Rsrc1, Src2 Nor (Or negada)
oror Rdest, Rsrc1, Src2 Or
oriori Rdest, Rsrc1, Src2 Or inmediata
sllsll Rdest, Rsrc1, Src2 Desplazamiento lógico a la izquierda
srlsrl Rdest, Rsrc1, Src2 Desplazamiento lógico a la derecha
srasra Rdest, Rsrc1, Src2 Desplazamiento aritmético a la derecha
subsub Rdest, Rsrc1, Src2 Resta (con overflow)
subusubu Rdest, Rsrc1, Src2 Resta (sin overflow)
xorxor Rdest, Rsrc1, Src2 Xor (Or exclusiva)
xorixori Rdest, Rsrc1, Src2 Xor inmediata

Instrucciones de manipulación de constantes
li li Rdest, Inm Carga valor inmediato* li Rdest, Inm Carga valor inmediato*
lui lui Rdest, Inm Carga valor inmediato en parte superior. Los bits de menor peso de Rdest se ponen a cero

Instrucciones de comparación
sltslt Rdest, Rsrc1, Src2 Establecer sí menor que
sltislti Rdest, Rsrc1, Inm Establecer sí menor que inmediata
sltusltu Rdest, Rsrc1, Src2 Establecer sí menor que (sin signo)
sltuisltui Rdest, Rsrc1, Inm Establecer sí menor que inmediata (sin signo)


Instrucciones de salto condicional e incondicional

Las instrucciones de bifurcación o de ruptura de secuencia se utilizan para romper la ejecución secuencial de los programas. Esta ruptura de secuencia se puede producir incondicionalmente o bien por alguna condición específica. Según utilicemos un criterio u otro hablaremos de salto incondicional o salto condicional, respectivamente.

beqbeq Rsrc1, Rsrc2, etiqueta Salto si igual
bnebne Rsrc1, Rsrc2, etiqueta Salto si distinto
bgtzbgtz Rsrc, etiqueta Salto si mayor que cero
bgezbgez Rsrc, etiqueta Salto si igual o mayor que cero
bgezalbgezal Rsrc, etiqueta Salto y enlaza si igual o mayor que cero
bltzbltz Rsrc, etiqueta Salto si menor que cero
blezblez Rsrc, etiqueta Salto si igual o menor que cero
bltzalbltzal Rsrc, etiqueta Salto y enlaza si igual o menor que cero
jj etiqueta Salto incondicional
jaljal etiqueta Salto incondicional y enlaza
jalrjalr Rsrc Salto incondicional a registro y enlaza
jrjr Rsrc Salto incondicional a registro

Instrucciones de carga
lala Rdest, Identificador Carga dirección * la Rdest, Identificador Carga dirección *
lblb Rdest, dirección Carga byte (extensión de signo)
lbulbu Rdest, dirección Carga byte (sin extensión de signo)
lhlh Rdest, dirección Carga media palabra
lhulhu Rdest, dirección Carga media palabra
lwlw Rdest, dirección Carga palabra de 32 bits
ldld Rdest, dirección Carga palabra de 64 bits en Rdest y Rdest+1 * ld Rdest, dirección Carga palabra de 64 bits en Rdest y Rdest+1 *

Instrucciones de almacenamiento
sbsb Rsrc, direcciónAlmacena byte bajo
shsh Rsrc, dirección Almacena media palabra en parte baja de Rscr
swsw Rsrc, dirección Almacena palabra de 32
sdsd Rsrc, dirección Almacena palabra de 64 bits en Rsrc y Rsrc+1 * sd Rsrc, dirección Almacena palabra de 64 bits en Rsrc y Rsrc+1 *

Instrucciones de movimiento de datos
move Rdest, RsrcMueve dato desde Rsrc a Rdest move Rdest, RsrcMueve dato desde Rsrc a Rdest * move Rdest, RsrcMueve dato desde Rsrc a Rdest *
mfhimfhi Rdest Mueve dato desde hi a Rdest
mflomflo Rdest Mueve dato desde lo a Rdest
mthimthi Rdest Mueve dato desde Rdest a hi
mtlomtlo Rdest Mueve dato desde Rdest a lo
mfczmfcz rt, td Transfiere el registro rd del coprocesador z al registro rt de la CPU
mfc1.dmfc1.d Rdest, frscr1 Transfiere los registros de coma flotante frsrc1 y frsrc1+1 a los registros rdest y rest+1 de la CPU * mfc1.d Rdest, frscr1 Transfiere los registros de coma flotante frsrc1 y frsrc1+1 a los registros rdest y rest+1 de la CPU *
mtcz mtcz Transfiere el registro rt de la CPU al registro rd del coprocesador z


Instrucciones de coma flotante

Instrucciones aritméticas
add.sadd.s FRdest, FRSrc1, FRSrc2 Suma de números en simple precisión
add.dadd.d FRdest, FRSrc1, FRSrc2 Suma de números en doble precisión
sub.ssub.s FRdest, FRSrc1, FRSrc2 Resta de números en simple precisión
sub.dsub.d FRdest, FRSrc1, FRSrc2 Resta de números en doble precisión
abs.s abs.s FRdest, FRSrc Valor absoluto de un número en simple precisión
abs.dabs.d FRdest, FRSrc1 Valor absoluto de un número en doble precisión
mul.smul.s FRdest, FRSrc1, FRSrc2 Multiplicación de números en simple precisión
mul.dmul.d FRdest, FRSrc1, FRSrc2 Multiplicación de números en doble precisión
div.sdiv.s FRdest, FRSrc1, FRSrc2 División de números en simple precisión
div.ddiv.d FRdest, FRSrc1, FRSrc2 División de números en doble precisión
Instrucciones de carga / almacenamiento
l.sl.s FRdest, dirección Carga un número en simple precisión* l.s FRdest, dirección Carga un número en simple precisión*
l.dl.d FRdest, dirección Carga un número en doble precisión* l.d FRdest, dirección Carga un número en doble precisión*
s.ss.s Frdest, dirección Almacena un número en simple precisión* s.s Frdest, dirección Almacena un número en simple precisión*
s.ds.d Frdest, dirección Almacena un número en doble precisión* s.d Frdest, dirección Almacena un número en doble precisión*
mfc1mfc1 Rdest, FRscr Mueve dato simple al coprocesador 1 a la CPU
mtc1mtc1 Rsrc, Frdest Mueve un dato de la CPU al coprocesador 1
mfc1.dmfc1.d Rest, Frsrc1 Mueve dato doble del coprocesador 1 a la CPU* mfc1.d Rest, Frsrc1 Mueve dato doble del coprocesador 1 a la CPU*
mtc1.dmtc1.d Rsrc, Frdest1 Mueve dato doble de la CPU al coprocesador 1* mtc1.d Rsrc, Frdest1 Mueve dato doble de la CPU al coprocesador 1*
Instrucciones de comparación y salto condicional
c.x.sc.x.s FRsrc1, FRsrc2 Comparación en simple precisión
c.x.dc.x.d FRsrc1, FRsrc2 Comparación en doble precisión
bc1tbc1t etiqueta Salto si verdadero
bc1fbc1f etiqueta Salto si falso


Llamadas al sistema

La programación en lenguaje ensamblador, como hemos visto, requiere un profundo conocimiento de la arquitectura de la máquina. El programador de lenguaje ensamblador puede simplificar su tarea haciendo uso de las llamadas al sistema. Una llamada al sistema no es más que la solicitud al sistema operativo de alguna acción concreta. Esto significa que no es necesario programarlo todo, sino que es posible utilizar facilidades que aporta el sistema operativo, que es quien, en última instancia, se encarga de administrar la máquina. Estas facilidades pueden ser muy diversas: imprimir en pantalla un entero, un número en coma flotante, una cadena de caracteres, leer de teclado un entero, etcétera.

La instrucción que la arquitectura MIPS proporciona para acceder al conjunto de servicios que aporta el sistema operativo se denomina syscall (system call). Para acceder a un servicio, el programa debe realizar los siguientes pasos:

Escribir el tipo de llamada en el registro $2 ($v0)
Escribir los parámetros en los registros $4..$7 ($f12 si se usa coma flotante)
Ejecutar la orden syscall

Las llamadas al sistema dejan el resultado en el registro $2 (ó $f0 si el resultado es en coma flotante).

Servicio Código de llamada Argumentos Resultado
Escribir_entero 1 $4 = entero
Escribir_float 2 $f12 = simple
Escribir_double 3 $f12 = doble
Escribir_cadena 4 $4 = cadena
Leer_entero 5 Entero en $2
Leer_float 6 Simple en $f0
Leer_double 7 Doble en $f0
Leer_cadena 8 $4 = buffer $5 = longitud
Sbrk 9 $4 = despl. Dirección en $2
Exit 10

Conexión de un terminal al procesador MIPS R2000

Además de simular el procesador MIPS R2000 y algunas utilidades del sistema operativo, el simulador PCSpim también puede simular un terminal (pantalla y teclado) conectado al procesador mapeado en memoria.

Cuando un programa se está ejecutando, PCSpim conecta su propio terminal al procesador. El programa puede leer caracteres introducidos desde el teclado durante la ejecución del programa. De forma similar, si PCSpim ejecuta instrucciones de escritura de caracteres en la pantalla del terminal, éstos aparecerán en la ventana correspondiente a la consola. Una excepción a esta regla viene determinada por la combinación de teclas Ctrl+C: este código no se pasa al procesador hace un stop en la ejecución, el terminal es reconectado al simulador PCSpim y se podrán introducir órdenes en él. Para utilizar entrada /salida mapeada en memoria es necesario emplear la opción Simulator - Settings Mapped I/O.

El terminal consta de dos unidades independientes: el receptor y el transmisor. La unidad de recepción lee caracteres del teclado a medida que éstos son introducidos por el usuario. La unidad de transmisión escribe los caracteres en la pantalla del terminal. Estas dos unidades son completamente independientes. Esto significa que, por ejemplo, los caracteres introducidos por el teclado no son enviados automáticamente a la pantalla. En lugar de eso, el procesador debe leer el carácter de entrada del receptor y retransmitirlo al transmisor para que éste lo imprima en pantalla.

El procesador accede al terminal mediante cuatro registros mapeados en el espacio de direcciones de memoria principal. Estos cuatro registros ocupan cuatro direcciones distintas, es decir, están mapeados en cuatro puertos. Al estar mapeados en memoria significa que cada registro aparece igual que una dirección de memoria principal. El registro de control de recepción está en la posición 0xffff0000; únicamente dos bits de éste se utilizan. El bit 0 se denomina bit de preparado (ready); si está a uno indica que un carácter ha sido introducido desde el teclado pero no se ha escrito en la pantalla. Este bit cambia automáticamente de cero a uno cuando se presiona una tecla, y cambia automáticamente de uno a cero cuando este carácter se lee del registro de datos de recepción.


Figura 9. Registro del controlador de terminal

El bit 1 del registro de control de recepción se denomina habilitación de interrupción. Este bit puede ser leído o escrito por el procesador. Inicialmente su valor es cero. Si es puesto a uno por el procesador, el terminal realiza una petición de interrupción de nivel 0 siempre que el bit de preparado esté a 1. Para que la interrupción sea aceptada por el procesador, el sistema de interrupciones debe estar habilitado en el registro de estado del coprocesador. El resto de bits del registro de control de recepción no se utilizan; se leen siempre como ceros y son ignorados en las escrituras.

El segundo registro del terminal se denomina registro de datos de recepción y está ubicado en la posición 0xffff0004. Los ocho bits de menor peso de este registro contienen el código ASCII del último carácter tecleado por el usuario, y el resto de bits están a cero. El registro sólo se puede leer y su valor cambia únicamente cuando se teclea un nuevo carácter. La lectura de su contenido provoca que el bit de preparado del registro de control de recepción se ponga a cero.

El tercer registro del terminal se denomina registro de control de transmisión y está ubicado en la dirección 0xffff0008. Sólo los dos bits de menor peso se utilizan, y se comportan de forma similar a como lo hacían los bits del registro de control de recepción. El bit 0 actúa como bit de preparado y es de sólo lectura. Si está a uno significa que el transmisor está preparado para aceptar un nuevo carácter para imprimirlo en pantalla. Si está a acero indica que el transmisor está ocupado imprimiendo en pantalla el carácter anterior. El bit 1 des el bit de habilitación de interrupción, y puede ser leído y escrito. Si se pone a uno el terminal hará una petición de interrupción de nivel 1 siempre que el bit de preparado esté a uno.

El último registro se denomina registro de datos de transmisión y está ubicado en la dirección 0xffff000c. Cuando se escribe en él, los ocho bits de menor peso se toman como el código ASCII del carácter que hay que imprimir en pantalla. Siempre que es escrito, el bit de preparado del registro de control de transmisión se pone a cero. Este bit se mantendrá a cero hasta que haya transcurrido un tiempo suficiente para transmitir el carácter al terminal; entonces el bit de preparado se pondrá a uno de nuevo. El registro de datos de transmisión sólo se debería escribir cuando el bit de preparado del registro de control de transmisión esté a uno; si la unidad de transmisión no está preparada entonces la escritura en el registro de datos de transmisión se ignorará (la escritura parecerá que se hace correctamente pero el carácter no aparecerá en pantalla).

El siguiente es un ejemplo de utilización de la opción de entrada/salida mapeada en memoria. En él se están leyendo caracteres de teclado y se imprimen por pantalla hasta que se pulsa la combinación de teclas Ctrl+C.

.data 0xffff0000 #se etiquetan los puertos terminal
CR: .space 4 # registro de control de recepción
DR: .space 4 # registro de datos de recepción
CT: .space 4 # registro de control de transmisión
DT: .space 4 # registro de datos de transmisión

.text
.globl main

main: addi $20, $0, 0x00 #inicializar $20 con nulo
addi $15, $0, 0x00 #inicializar $15 con nulo
leer:lw $16, CR($0) #leer el registro de control receptor
andi $20, $16, 0x01 #aplicar la máscara
beq $20, $0, leer #si no hay carácter, repetir lectura
lw $15, DR($0) #leer del registro de datos receptor
escr: lw $16, CT($0) #leer el registro control transmisión
andi $20, $16, 0x01 #aplicar la máscara
beq $20, $0, escr #si no preparado, repetir lectura
sw $15, DT($0) #se envía el carácter a pantalla
j leer #volvemos a leer siguiente carácter
fin: .end
Ejemplos
En las siguientes secciones se irán presentando las diferentes instrucciones, entendiendo como tal tanto instrucciones reales, pseudoinstrucciones y/o directivas.
Ejemplos de uso de las directivas. Gestión de memoria (segmento de datos)

En apartados anteriores se han presentado las directivas, instrucciones que sólo existen en tiempo de ensamblado y que nos permiten guiar al ensamblador en la realización de su tarea, es decir, indicarle en cada momento que es lo que se va a ir encontrando, si datos (.data), si código (.text) y en el caso de los datos, el tipo de esos datos, o mejor, la cantidad de memoria que vamos a destinar a su almacenamiento.

En esta sección de ejemplos se ira dando un repaso a las directivas y se mostraran ejemplos de su uso.

La primera de las directivas que vamos a repasar es la directiva .data, esta directiva le indica al ensamblador que a partir de entonces se encontrará con datos, datos que se irán almacenando en el segmento de datos, a partir de la dirección 0x10010000, a no ser que se especifique otra mayor o igual que la dirección 0x10000000, dirección base del segmento de datos. Su utilización es, por tanto, .data y opcionalmente una dirección que me marca la posición a partir de la que almacenare los datos definidos posteriormente, con el uso de otras directivas.

Los tipos de datos que podemos almacenar y recuperar de memoria, haciendo uso de las instrucciones, pueden ser:

caracteres o bytes,
medias palabras,
palabras,
números en coma flotante y notación IEEE-754 en simple o en doble precisión.

Para ello como vimos en apartados anteriores, se puede hacer uso de las directivas:
.byte
.half
.word
.float / .double

la sintaxis que sigue el uso de estas directivas es la de usar la directiva correspondiente y pasar como argumentos, separados por comas, cada uno de los elementos asociados a cada directiva.

La traducción del uso de estas directivas en el segmento de datos es el empleo de mayor o menor cantidad de memoria (de bits en definitiva) para representar los datos que llevan asociados.

Por ejemplo:

Sea la siguiente secuencia de directivas:
.data 0x10000000
notas: .byte 5, 6, 0x07, 3, 6, 4
.half 9, 130
.word 560, 740
.float 20.4
.double 34.5e-2, 56.0e-28

El estado en el que quedaría la memoria, concretamente su segmento de datos una vez ensamblado el código anterior sería el siguiente:



Como puede apreciarse el almacenamiento de la información se realiza a partir de la dirección que la directiva .data tiene como argumento, en este caso, 0x10000000.
Se almacenan, inicialmente, haciendo uso de la directiva .byte, seis bytes consecutivos en memoria, para ello se destinan, obviamente, 8 bits para cada byte. Seguidamente, se almacenan dos medias palabras (16 bits para cada media palabra) el número 9 y el número 130 (que en hexadecimal es 0x82).
Luego se tendría que almacenar una palabra pero en la palabra curso no hay espacio suficiente con lo que la información se alinea, y se pasa a la siguiente palabra en memoria, así se almacenan 560 (0x230) y 740 (0x2e4).
Finalmente, con las directivas .float y .double podremos almacenar números en coma flotante en simple y doble precisión, es así como se almacenan 20.4 (0x41a33333), 34.5e-2 (parte alta: 0x3fd6147a y parte baja: 0xe147ae14) y 56.0e-28 (parte alta: 0x3a7bbad7 y parte baja: 0xe6865416).

Existen, igualmente, directivas para facilitar el almacenamiento de cadenas de caracteres, en lugar de tener que almacenar estas cadenas usando la directiva .byte y los códigos ASCII asociados a cada carácter. Estas directivas eran .ascii y .asciiz. La diferencia entre una y otra es que la segunda termina la cadena de caracteres con un carácter nulo.

Un ejemplo de su uso puede ser el siguiente:

.data 0x10010000
.ascii “Fundamentos de computadores”
#este almacenamiento sirve para distinguir el comienzo de la segunda cadena
.byte 0xff
.asciiz “Fundamentos de computadores”

El estado de la memoria quedaría como sigue:



La cadena de caracteres “Fundamentos de computadores” se almacena haciendo uso de los códigos ASCII asociados a cada carácter en hexadecimal. Con el fin de distinguir el final de una cadena y el comienzo de la otra cadena se ha introducido el carácter 0xff. A partir del cual puede apreciarse como se repite, en este caso, la cadena “Fundamentos de computadores”. La diferencia entre uno y otro modo de almacenamiento es que el carácter nulo que existe a la izquierda del último carácter correspondiente a la segunda cadena, pertenece a ella. Y si se almacenase un byte después de la segunda cadena de caracteres se almacenaría a partir de la dirección 0x10010048.

Otras directivas que a nivel de datos pueden sernos de utilidad son la directiva .space y la directiva .align.
La directiva .space sirve para reservar espacio continuo en memoria en tiempo de ensamblado, un espacio en bytes que coincide con el argumento pasado a la directiva.
La directiva .align sirve para alinear el siguiente dato a un número de bytes límite 2n donde n es el argumento pasado a la directiva.

Veámoslo con un ejemplo:

.data 0x100020a0
.byte 0xff
.space 12
.byte 0xff
.align 2
.byte 0xff

La memoria, en su segmento de datos quedaría de la forma:



Como puede apreciarse se almacena primero un byte 0xff, luego con el uso de la directiva .space se reservan 12 bytes consecutivos, más tarde se almacena, nuevamente, el byte 0xff y el siguiente byte a 0xff que pretende almacenarse como se precede de una alineación a 4 bytes (2n) se almacena en la siguiente palabra libre en memoria (desaprovechándose dos bytes: 0x100020ae y 0x100020af).

Otras directivas que no están asociadas a la introducción de datos, sino a la de instrucciones son:

la directiva .text, que indica al ensamblador que a partir de entonces lo normal es que se encuentre con códigos de operación correspondiente a instrucciones.
La directiva .globl que tiene como argumento una etiqueta que indica la etiqueta que marca el comienzo de nuestro programa, la primera instrucción que se ejecutara. Por lo general no es otra que __start, a no ser que se haya cargado el manejador de interrupciones en cuyo caso debe utilizarse la etiqueta main.
La directiva .end que se utiliza asociándola a una etiqueta donde saltar cuando el programa haya finalizado.

Ejemplos de uso de instrucciones aritmético - lógicas

Las instrucciones aritméticas y lógicas, pudiéndose incluir aquí también instrucciones de rotación o desplazamiento (lógico o aritmético) responden siempre a instrucciones de tipo R (registro) o de tipo I (inmediato).

El procesador MIPS R2000 tiene una arquitectura de carga almacenamiento, los datos están en memoria se traen de ella, se almacenan en registros y sobre ellos se realizan operaciones aritméticas, o de cualquier otro tipo.

El formato al que responden las instrucciones aritméticas lo hemos presentado en apartados anteriores. Lo que pretendemos ahora es facilitar el uso de estas instrucciones. Cualquier computador es capaz de realizar operaciones aritméticas. Para comenzar a estudiar cómo se programa en el lenguaje esamblador del procesador MIPS R2000 estudiaremos algunos casos sencillos en que se utilizan instrucciones aritméticas.

Sean dos variables a y b, las cuales queremos sumar utilizando este procesador. La manera de indicar esta operación mediante una instrucción en lenguaje de alto nivel es c:= a + b, donde c es la variable que almacena el resultado de la suma. Este modo de expresar la operación depende sólo de la sintaxis del lenguaje de alto nivel, en este caso Pascal, y es independiente de la arquitectura del computador donde se ejecute. Sin embargo, si queremos realizar la operación directamente sobre el procesador MIPS R2000, debemos indicarlo utilizando la sintaxis propia de la instrucción de suma (add):

add c, a, b # c := a + b

Esta notación es fija y cualquier instrucción de suma MIPS debe respetar este formato. Por lo tanto, en una única instrucción no podremos sumar más de dos variables. Nótese que el primer operando de la instrucción es el operando destino, porque almacena el resultado, y los dos últimos son el primer operando fuente y el segundo operando fuente, respectivamente. Así, la instrucción de suma debe manajar obligatoriamente tres operandos.

Si queremos realizar una suma de más operandos, por ejemplo, g := a+b+c+d+e+f, estamos obligados a resolverla respetando la sintaxis MIPS. Por tanto, esta suma se tendrá que descomponer en sumas más pequeñas:

add g, a, b # g := a + b
add g, g, c # g := a + b + c
add g, g, d # g := a + b + c + d
add g, g, e # g := a + b + c + d + e
add g, g, f # g := a + b + c + d + e + f

Otra instrucción sencilla, y muy parecida a la instrucción de suma, es la instrucción de resta, denotada mediante el símbolo sub (substract). Por ejemplo, la traducción que producirá el compilador del código d := a - b + c sera:

sub d, a, b # d := a - b
add d, d, c # d := a - b + c
Ejemplos de uso de instrucciones de salto. Condiciones y bucles.
Una de las grandes ventajas que ofrece el lenguaje ensamblador, frente a otros lenguajes de bajo nivel, como el lenguaje máquina es el poder utilizar etiquetas para referenciar datos o instrucciones. Es esta la idea en la que se apoya la implementación de saltos, de instrucciones condicionadas y la implementación de bucles.

Si es posible etiquetar una instrucción, vamos a poder utilizar esa misma etiqueta para referenciarla y poder en un momento dado saltar a la instrucción que tiene asociada.

Por ejemplo sea el siguiente código:

IF (i = j) THEN a := a + h;
a := a + j;

En el se muestra la ejecución condicionada de una instrucción a := a + h, en función de que se cumpla la igualdad de dos variables i y j.

¿Cómo podría representarse este código en lenguaje ensamblador del MIPS R2000?

Si suponemos que las variables i, j, a y h están en los registros de $16 a $19.

El código sería es siguiente:

bne $16, $17, L1 # Ir a L1 si i es distinto de j
add $18, $18, $19 # a := a + h; (saltada si i es distinto de j)
L1: add $18, $18, $17 # a := a + j; (se ejecuta siempre)


Un tipo de bucle muy habitual es la implementación de un conjunto de instrucción hasta que se cumpla o deje de cumplirse una determinada condición. Esto puede representarse en un lenguaje de alto nivel como sigue:

REPEAT

g := g + A[i];
i := i + h;

UNTIL (i = h)

Se supone que A es un vector de 100 enteros y que el compilador asocia las variables g, h y i a los registros $17 a $19. El vector comienza en la dirección de memoria identificada con la etiqueta Ainicio.

Recordemos que hay que multiplicar el índice de acceso al vector por 4 debido a la capacidad de direccionamiento de bytes que tiene este procesador.

addi $8, $0, 4
bucle: mul $9, $19, $8 # $9 se emplea como registro índice
lw $8, Ainicio($9) # $8 almacena la componente A[i]
add $17, $17, $8 # g := g + A[i]
add $19, $19, $18 # i := i+h
bne $19, $18, bucle # Repetir bucle si i es distinto de j

En este tipo de bucle las instrucciones asociadas al mismo, se ejecutan siempre al menos una vez porque la condición de salida del bucle se evalúa al final. En otros casos puede ser interesante evaluar la condición al principio. Sea el siguiente ejemplo:

i := 1;
WHILE (i<35) DO
BEGIN
x := x - 2;
i := i + 5;
END;
x := x + 18;

La traducción del código anterior a código ensamblador del MIPS R2000 tendrá el siguiente aspecto (suponiendo que las variables i y x están en los registros $22 y $15):

addi $22, $0, 1 # $22 ¬ 1
bucle: slti $10, $22, 35 # $10 ¬ 1 si $22 < 35 / $10 ¬ 0 si $22 ³ 35
beq $10, $0, salir # Salir del bucle sino se cumple la condición
addi $15, $15, -2 # Realizar las instrucciones aritméticas
addi $22, $22, 5 #
j bucle # Saltar incondicionalmente a la etiqueta bucle
salir: addi $15, $15, 18 # Incrementar x := x + 18

El salto fuera del bucle se dará cuando no se cumpla la condición de la guarda. Nótese que en los bucles de este tipo podemos no entrar nunca; esto no es así en los bucles REPEAT, donde se asegura que al menos se entra la primera vez porque la condición se evalúa al final.

Ejemplos de uso de instrucciones en coma flotante

La arquitectura MIPS soporta el estándar IEEE 754 que define los formatos de simple y doble precisión para números en coma flotante. El manejo de los datos en coma flotante es llevado a cabo por el coporocesador 1 (CP1, coprocessor 1) que acompaña al procesador, también denominado unidad de coma flotante. Este elemento extiende el conjunto de instrucciones del procesador MIPS R2000 para manejar datos representados mediante este estándar.

La unidad de coma flotante contiene sus propios registros, que se enumeran como $f0, $f1, $f2, ..., $f31 ya que estos registros son de 32 bits se necesitan dos de ellos para almacenar un valor de 64 bits. Para simplificar el diseño de la unidad, las instrucciones que operan sobre variables en coma flotante sólo pueden utilizar registros pares, incluyendo tanto instrucciones que operan sobre números en doble precisión como en simple precisión.

Un posible ejemplo de utilización de las instrucciones de la unidad de coma flotante es el siguiente:

.data 0x10000000
tam: .word 4 # Tamaño del vector
vector: .float 3.1415, 2.7182 # números p y e
.float 1.0e2, 6.5e-1 # números 100 y 0.65
result: .float 0.0 # resultado de la suma
.text
.globl __start

__start: addi $15, $0, 0 # registro índice
addi $17, $0, 0 # contador de elementos
lw $16, tam($0) # leemos el tamaño del vector
mtc1 $0, $f2 # resultado de la suma f2
bucle: l.s $f4, vector($15) # leer en f4 un elemento
add.s $f2, $f2, $f4 # acumular la suma
addi $15, $15, 4 # incrementar reg. índice
addi $17, $17, 1 # incrementar cont. de ele
slt $8, $17, 4 # ¿se alcanzó el final?
bne $8, $0, bucle # si quedan números regresar
s.s $f2, result($0) # Almacenar resultado
fin: .end

El anterior ejemplo acumula en el registro $f2 el resultado de la suma de los elementos, números en coma flotante y simple precisión, almacenados en el segmento de datos a partir de la dirección vector. Deja el resultado final de la suma en la dirección result.
Ejemplos de llamadas al sistema

Un ejemplo de uso de la instrucción syscall puede ser el siguiente:

.data
cad: .asciiz “El resultado es: ”
.text
.globl __start

__start: addi $20, $0, 4 # Se deja un resultado entero en $20

#Se imprime la cadena cad
addi $2, $0, 4 # $2 ¬ 4 (print_string)
la $4, cad # $4 ¬ dirección de la cadena
syscall

#Se imprime el número entero
addi $2, $0, 1
add $4, $0, $20
syscall

fin: addi $2, $0, 10
syscall
.end


En función del programa anterior el resultado que podría verse en la consola de salida del simulador PCSpim sería el siguiente:




Ejemplos de llamadas a subrutinas. Llamadas recursivas. Paso de parámetros a través de la pila

Las subrutinas (procedimientos o funciones) representan la manera natural en que los programadores estructuran los programas para hacerlos fáciles de comprender y reutilizar. En consecuencia el conjunto de instrucciones de la máquina debe ofrecer algún mecanismo que permita realizar la bifurcación a una subrutina, así como el retorno a la instrucción sucesora a la que lo invocó. Así mismo, deberán establecerse convenios para el paso de parámetros a estas subrutinas y para el anidamiento de las mismas.

Para dar soporte a las subrutinas la arquitectura MIPS proporciona una instrucción que bifurca a una dirección y simultáneamente guarda la dirección de retorno en el registro $31. Esta instrucción se denomina jal (jump and link), es decir, bifurca y enlaza. La sintaxis es:

jal DirSubruina #Salta a la subrutina DirSubrutina y enlaza

El término enlaza que forma parte de la denominación de esta instrucción significa que se forma un enlace para permitir que la subrutina vuelva a la dirección adecuada. Este enlace se suele denominar formalmente dirección de retorno. La última instrucción de la subrutina será, por tanto, un salto sobre el registro $31 ya que es éste quien tiene la dirección de retorno:

jr $31 #Retorno de la subrutina

La siguiente figura muestra gráficamente las acciones que tienen lugar en la llamada a una subrutina.



Hasta ahora hemos analizado el mecanismo básico para la utilización de las subrutinas. Ahora bien, ¿qué ocurriría si una subrutina invoca a otra? Tal como se ha visto, la primera llamada a subrutina almacena la dirección de retorno en el registro $31. Si esta subrutina llama a otra, ejecutará la instrucción jal y se almacenaría en el registro $31 la nueva dirección de retorno, con lo que se perdería la primera.

La forma de resolver el problema anterior consiste en utilizar el segmento de pila. Una pila es una estructura de datos LIFO (last in first out), esto es, el último en entrar es el primero en salir. Las pilas se caracterizan por tenr su propia terminología en las operaciones que hacen referencia al movimiento de datos. Así, colocar un dato en la pila (al fin y al cabo es una escritura en memoria principal) se denomina push; sacar un dato de la pila se denomina pop. El manejo de la pila se da a través del puntero de pila, que se corresponde con el registro $29.

Veamos todo lo anterior con la utilización de un ejemplo:

.data
num: .word 3, 4

.text
.globl __start
__start: #Paso de parámetros
li $a0, 1
li $a1, 2
lw $a2, num
lw $a3, num+4
li $s0, 5
li $t0, 6

subu $sp, $sp, 12
sw $t0, 8($sp) #convenio guardar invocador
sw $t0, 4($sp) #parámetro 6 en pila
sw $s0, 0($sp) #parámetro 5 en pila
jal rutina #salto a la rutina
lw $t0, 8($sp)
addi $sp, $sp, 12

li $v0, 10
syscall #fin del programa

rutina: #suma el contenido de los seis parámetros
#devuelve el resultado en $v0
subu $sp, $sp, 12
sw $ra, 8($sp)
sw $fp, 4($sp)
sw $s0, 0($sp) #convenio guardar invococado
addi $fp, $sp, 12

lw $s0, 0($fp)
lw $t0, 4($fp)
addi $t1, $a0, 0
add $t1, $a0, 0
add $t1, $t1, $a1
add $t1, $t1, $a2
add $t1, $t1, $a3
add $t1, $t1, $s0
add $t1, $t1, $t0
move $v0, $t1 #devolvemos el resultado en $v0

lw $s0, 0($sp)
lw $fp, 4($sp)
lw $ra, 8($sp)
addi $sp, $sp, 12
jr $ra



El ejemplo anterior realiza las siguientes actividades: lo que se pretende es mostrar como se gestiona la llamada a procedimientos y como a estos procedimientos podemos hacerles llegar diferentes parámetros. Existen cuatro registros destinados a la tarea del paso de parámetros $a0... $a3, si nuestro programa tiene que hacer uso de más parámetros, estos se pasan a través de la pila. En nuestro ejemplo los parámetros adicionales se guardan en los registros de uso general $s0 y $t0.

Antes de llamar al procedimiento o función (jal rutina) se realizan los pasos habituales al trabajar con la pila:

Se hace sitio en la pila: subu $sp, $sp, 12
Bajo el convenio de invocador - guardado se guardan en la pila aquellos valores de los registros que no queremos modificar, en este caso $t0. sw $t0, 8($sp)
Se hace uso de la pila para el paso de parámetros de adicionales a esos cuatro que se pueden pasar a través de los registros de uso general. sw $t0, 4($sp), sw $s0, 0($sp).
Salvaguardado todo el contexto y pasados los parámetros a través de la pila se hace la llamada a la rutina. jal rutina.

Este proceso provoca que se abandone la ejecución secuencial de las instrucciones y se produzca un salto a las instrucciones asociadas a la etiqueta rutina. Cuando el procedimiento asociado a estas instrucciones se completa. Este procedimiento sólo realiza la suma de los parámetros pasados al procedimiento, se retorna al programa principal mediante el uso de la instrucción jr $31. Previamente, y siguiendo los puntos anteriormente citados, se salvaguarda el contexto que existe antes de ejecutar las operaciones asociadas al procedimiento rutina, es decir, se hace sitio en la pila para almacenar, en este caso tres palabras, la dirección de retorno ($ra), el puntero de marco ($fp) y siguiendo las directrices de salvaguarda de registros bajo el convenio de invocado - guardado el valor del registro $s0. Se realizan, seguidamente, las operaciones y se restaura el contexto (valores de $s0, $fp, $ra y $sp) y se vuelve al programa principal.

Una vez hemos vuelto al programa principal se restaura, aquí también, el contexto devolviendo el valor que tenía el registro $t0 y actualizando el valor del puntero de pila. Se termina el programa haciendo uso de la llamada al sistema exit.


Para ilustrar el diseño de programas recursivos usaremos el ejemplo del cálculo del factorial de un número entero. El pseudocódigo asociado a la rutina que calcula esta función puede ser algo similar al siguiente:

FUNCIÓN factorial (n: entero): entero;
Empezar
Si (n != 0) entonces factorial := n * factorial (n-1);
Sino factorial := 1;
Fin;

Todo programa recursivo está compuesto por una serie de elementos característicos:

Un elemento base de la recursión, es decir, que detenga el proceso: en este caso que se alcance el valor 0.
Y una serie de llamadas al mismo procedimiento, de ahí el calificativo de recursivo, es decir, que se llama a si mismo.
Suponiendo que se realiza la llamada a la función factorial con el argumento n valiendo 3, en total se harán 3 llamadas a dicha función, tal y como se refleja en la siguiente figura.



Para diseñar la rutina que calcula el factorial debemos hacer uso de la estructura de pila de manera que cada llamada tenga la suya propia. Aunque en realidad las estructuras de pila deben tener un tamaño mínimo de 32 bytes, nosotros no asumiremos tal restricción y utilizaremos el espacio requerido por nuestros programas. Denominaremos fact a la subrutina recursiva que calcula el factorial. A esta rutina le pasaremos el argumento (n) a través del registro $4. El código del programa es el siguiente:

.data 0x10000000
cad1: .asciiz “Cálculo del factorial de: ”
cad2: .asciiz “El factorial calculado es: ”
.text
.globl __start

__start: li $2, 4 # código de la impresión de cadena
la $4, cad1 # dirección primera cadena
syscall # impresión de cadena
li $2, 5 # código lectura de entero
syscall # lectura de entero

add $4, $2, 0 # argumento de la función factorial
jal fact

add $8, $2, 0 # guardamos el resultado en $8

li $2, 4 # código de impresión de cadena
la $4, cad2 # dirección primera cadena
syscall # impresión de cadena
li $2, 1 # código de escritura de entero
add $4, $8, $0 # resultado a imprimir
syscall # escritura de entero

addi $2, $0, 10 # código de finalización de programa
syscall # fin de programa

fact: addi $29, $29, -8 # estructura pila: 2 palabras
sw $4, 4($29) # almacena el argumento n
sw $31, 0($29) # almacena dirección de retorno

lw $2, 4($29) # lectura del argumento
bgtz $2, seguir # si n>0 se prepara nueva llamada
addi $2, $0, 1 # 1 en $2 para deshacer la recursión
j volver # No preparar siguiente llamada

seguir: lw $3, 4($29) # lectura del argumento n
addi $4, $3, -1 # se pasa n-1 como argumento
jal fact # se hace la llamada recursiva

lw $3, 4($29) # lectura del argumento n
mult $2, $3 # se calcula n*factorial(n-1)
mflo $2 # $2 almacena el resultado parcial

volver: lw $31, 0($29) # Leemos la dirección de retorno
addi $29, $29, 8 # restaura valor del stack pointer
j $31 # Retorna de la subrutina


El programa comienza pidiendo al usuario el número entero del cual se va a calcular el factorial. Las cadenas utilizadas para esta comunicación (cad1 y cad2) se sitúan en las posiciones iniciales del segmento de datos. Una vez leído este número entero se copia en el registro $4 para pasárselo como argumento a la función fact. Cuando ésta ha calculado el factorial deja el resultado en el registro $2 y se imprime en pantalla, tras lo cual finaliza el programa.

Veamos ahora cómo está diseñada la subrutina fact. En primer lugar hay que construir la estructura de pila de la subrutina. Para ello se reserva un espacio en pila para dos palabras: una para almacenar la dirección de vuelta ($31) y otra para almacenar el argumento de la subrutina (n), cuyo valor dependerá del nivel de anidamiento en un instante dado y estará comprendido entre el valor introducido por el usuario y 0.

Una vez creada la estructura de pila, la rutina lee el argumento que le pasan y lo deja en el registro $2. Si este argumento es distinto de 0 entonces significa que no se ha llegado al final de la recursión y que deberá prepararse para realizar una nueva llamada a la función fact pero con argumento n-1. Si éste es el caso, el programa bifurca a la etiqueta seguir, donde se vuelve a leer el argumento, se le resta uno y se deja en el registro $4 para pasárselo como argumento a la rutina encargada de calcular el factorial de n-1. Después de esto realiza el salto mediante la instrucción jal fact.

Este proceso seguirá hasta que se alcance el valor base de la recursión (n = 0). Cuando esto ocurra, la rutina no bifurcará a la etiqueta seguir; en su lugar dejará en el registro $2 el valor 1 puesto que el factorial de 0 vale 1, y a continuación bifurcará a la etiqueta volver (no realiza ninguna llamada más a fact), donde leerá su dirección de retorno, restaurará el puntero de pila y volverá a la rutina que la llamó (factorial(1)). Asumiendo que se quiere calcular el factorial de 3, el estado del segmento de pila cuando se da el caso base de la recursión viene reflejado en la siguiente figura.



Bibliografía y referencias adicionales

Patterson y Hennesy. Organización y diseño de computadores. La interfaz hardware - software. Editorial McGraw - Hill. Primera edición, 1995

De Miguel Anasagasti. Fundamentos de computadores. Paraninfo. Cuarta edición, 1994

Larus, J.R. SPIM S20: A MIPS R2000 Simulator. Computer Sciences Departament. University of Wisconsin - Madison. 1993

Kane. RISC Microprocessors VR 3000/ VR 3010. MIPS RISC Architecture. NEC. User’s manual. 1988.

Campelo, Molero, Rodríguez. Principios de Computadores. Ref. 031. Departamento de Sistemas Computadores y automática. Universidad Politécnica de Valencia. Servicio de Publicaciones. 1996

Prácticas de Introducción a los computadores / Fundamentos de computadores. Práctica 2. Programación en ensamblador MIPS R2000 con el simulador PCSpim. Departamento de Ingeniería y Tecnología de computadores. Facultad de Informática. Universidad de Murcia.1999. (http://ditec.um.es/ficomp/teoprac2.pdf)



Ejercicios sobre programación en ensamblador del procesador MIPS R2000

¿Cómo almacenarías los siguientes datos en memoria?

La cadena de caracteres: “El resultado de la suma es: ”
El número en coma flotante simple precisión: 5.46e-2
El byte: 255 (0xff)
La media palabra: 0x74af
El carácter ‘a’:


Escribir el código en ensamblador que realiza las siguientes acciones:

Imprime el siguiente mensaje: “La media es: 55.63”
Lee tu nombre por teclado: “Introduzca su nombre: ”
Crear un procedimiento que imprima una cadena y un entero que se pase como parámetro
Crear un procedimiento que realice la suma de dos números en coma flotante y devuelva el resultado en $v0.


¿Cuál es la etiqueta que marca el comienzo de nuestro programa en caso de que Ö load trap file? ¿y si load trap file no está seleccionado?


Si la directiva .data se utiliza sin ningún argumento. ¿Dónde se almacenan los datos que lleven asociados otras directivas que le acompañen? ¿A partir de que dirección?

Para un programa de usuario, es seguro en algún momento usar los registros $k0 y $k1.


Indíquese la instrucción MIPS o la mínima secuencia de instrucciones para implementar la sentencia x := y * 100. Supóngase que x se encuentra en el registro $11 e y en el $12.


Indíquese la instrucción MIPS o la mínima secuencia de instrucciones que realiza la sentencia a[23]:= a[24] + x; supóngase que a es un vector de 100 elementos que comienza en la dirección de memoria 0x1000A000 y la variable x se encuentra en el registro $15.


El siguiente programa trata de copiar palabras desde la dirección de memoria que indica el registro $4 en la dirección que indica el registro $5; el registro $2 lleva la cuenta de las palabras copiadas. El programa se detiene cuando se encuentra una palabra igual a cero. No se han de guardar los contenidos de los registros $3, $4 y $5. La palabra de terminación (que estará a cero) debe ser leída pero no copiada.

bucle: lw, $3, 0($4) # lee siguiente palabra fuente
addi $2, $2, 1 # incrementa número de palabras copiadas
sw $3, 0($5) # Copia la palabra
addi $4, $4, 1 # Avanza puntero a siguiente palabra fuente
addi $5, $5, 1 # Avanza puntero a siguiente palabra destino
bne $3, $0, bucle # Va a bucle si palabra copiada no es cero

En el programa anterior hay multitud de fallos (bugs). Determínense estos fallos y cámbiese el programa para que funcione perfectamente.


Utilizando el programa anterior (tal y como se proporciona, es decir con los errores), determínese el formato de instrucción para cada una de las instrucciones que lo componen, así como los valores decimales (o hexadecimales) de cada campo del formato. ¿Qué tamaño ocupa en memoria?


Escríbase una subrutina en lenguaje MIPS que implemente el procedimiento siguiente:

PROCEDIMIENTO máximo (a, b: entero; var max: entero)
Empezar
Si (a>=b) entonces max := a;
Sino max := b;
Fin_procedimiento

Téngase en cuenta las siguiente suposiciones:

Los argumentos se pasan a la subrutina utilizando los registros destinados al efecto $4, ..., $7.
El resultado de la subrutina hay que devolverlo a través del registro $2.
No se origina desbordamiento en las operaciones, y su resultado nunca excede de 32 bits.
La preservación de registros en la pila se lleva a cabo mediante el convenio de guardar invocada (callee save).


Escríbase una subrutina en lenguaje MIPS que implemente una función que retorne el valor del cubo de un número introducido por teclado.

Implementar una función que diga si un valor introducido por teclado es par o impar.

Implementar un programa que escriba los números del 1 al 10.

Implementar un programa que implemente la funcionalidad de una contraseña.

Implementar la siguiente función recursiva:

Función suma (n: entero)
Empezar
Si (n = 1) entonces suma := 1;
Si no suma := n + suma (n - 1)
Fin_funcion

Escribir un programa que lea tu nombre, y tu primer apellido y los escriba en la consola de salida.

Entradas relacionadas: