0% encontró este documento útil (0 votos)
50 vistas43 páginas

Tema04 GIC

Este documento presenta un tema introductorio sobre programación en ensamblador, centrándose en los conceptos básicos de los lenguajes de bajo nivel como el repertorio de instrucciones y los modos de direccionamiento, así como la estructura de los programas en ensamblador para procesadores MIPS. Explica la traducción de programas de alto nivel a bajo nivel, destacando la importancia de comprender este proceso para desarrollar software de forma eficiente.

Cargado por

japenin
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
50 vistas43 páginas

Tema04 GIC

Este documento presenta un tema introductorio sobre programación en ensamblador, centrándose en los conceptos básicos de los lenguajes de bajo nivel como el repertorio de instrucciones y los modos de direccionamiento, así como la estructura de los programas en ensamblador para procesadores MIPS. Explica la traducción de programas de alto nivel a bajo nivel, destacando la importancia de comprender este proceso para desarrollar software de forma eficiente.

Cargado por

japenin
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 43

ESTRUCTURA DE COMPUTADORES

Tema 4: Programación en ensamblador: conceptos básicos

GRADO EN INGENIERÍA DE COMPUTADORES

Luis Rincón

Conocer la naturaleza y características del lenguaje ensamblador y el lenguaje máquina es


fundamental para todo estudiante que curse asignaturas de la materia de Ingeniería de
Computadores. Por un lado, nos permite ver cuáles son las instrucciones básicas ejecutadas por los
computadores. Por otro, el análisis del funcionamiento de dichas instrucciones ayuda a comprender
las decisiones de los arquitectos de computadores y su influencia en el diseño del hardware de los
computadores. Finalmente, comprender el proceso de traducción de un programa en alto nivel a su
correspondiente en bajo nivel ayuda a todo estudiante de cualquier titulación de Informática a
desarrollar programas mejores y más eficientes tanto en espacio como en tiempo de ejecución.
Como modelo de procesador para ilustrar todo ello se ha elegido la familia de microprocesadores
MIPS32, debido a la claridad y sencillez de su arquitectura, tanto desde el punto de vista de su
lenguaje ensamblador como del hardware que soporta el repertorio de instrucciones. Esta elección
determina el desarrollo de conceptos futuros centrados en el diseño de unidades aritméticas y
unidades de control y caminos de datos, que serán objeto de estudio en temas posteriores de la
asignatura.
Hay que entender este tema como una introducción a los lenguajes de programación en general y
al lenguaje ensamblador en particular. El próximo tema tratará más en detalle los aspectos
prácticos de la programación en ensamblador.

1
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

Nos encontramos ante un tema introductorio que presenta algunas generalidades sobre los
lenguajes de programación, para pasar a centrarse en cuestiones relacionadas con los lenguajes de
bajo nivel, como el repertorio de instrucciones y los modos de direccionamiento utilizados, la
estructura de los programas en ensamblador y su sintaxis, etc.
Este curso se centra en los procesadores MIPS. Por ello, es en este tema donde se presenta una
breve introducción a las características de los procesadores de esta familia.
Con objeto de facilitar la comprensión de todos estos conceptos, y para enlazar con conocimientos
impartidos en asignaturas de programación, se hace un cierto énfasis en el proceso de traducción
de programas, desde el programa fuente escrito en alto nivel hasta el montaje del programa
ejecutable, pasando por la traducción a lenguaje ensamblador. Esto también permitirá conocer en
una primera aproximación cómo se ejecutan y cómo funcionan los programas en el procesador.

2
Bibliografía
[PAT] D.A. PATTERSON, J.L. HENNESSY. Computer Organization and Design, 5th ed. Morgan
Kaufmann, 2014 (o la traducción española más reciente: Estructura y Diseño de
Computadores, 4ª ed. Reverté, 2011).

[SWE] D. SWEETMAN. See MIPS Run. Morgan Kaufmann, 2002.

[PAR] B. PARHAMI. Arquitectura de Computadores. McGraw-Hill, 2007.

[CAR] F. GARCÍA CARBALLEIRA y otros. Problemas resueltos de Estructura de


Computadores. Paraninfo, 2009.

[BER] J.A. ÁLVAREZ BERMEJO. Estructura de Computadores: procesadores MIPS y su


ensamblador. Ra-Ma, 2008.

[MIPS32-I] MIPS32 Architecture For Programmers – Volume I: Introduction to the MIPS32


Architecture. MIPS Technologies Inc., 2003.

[MIPS32-II] MIPS32 Architecture For Programmers – Volume II: The MIPS32 Instruction Set.
MIPS Technologies Inc., 2003.

[MIPS32-III] MIPS32 Architecture For Programmers – Volume III: The MIPS32 Privileged
Resource Architecture. MIPS Technologies Inc., 2003.

Para este tema y el siguiente se recomienda leer el capitulo 2 y el apéndice A de [PAT], que es el
texto base seleccionado y está muy bien explicado.

[PAR] es un texto (con traducción latinoamericana) que tiene una estructura parecida a [PAT] en
cuanto a los conceptos que trata, aunque la distribución de temas es diferente. En [PAR] se trata el
lenguaje ensamblador de MIPS en la parte 2, que incluye los capítulos del 5 al 7.

El texto [BER] puede ser adecuado, ya que está íntegramente dedicado a la programación en
ensamblador de MIPS.

[CAR] puede servir como referencia rápida, ya que en los capítulos 3 y sobre todo el 4 incluye un
resumen sobre MIPS y su ensamblador, junto con algunos ejercicios resueltos. El capítulo 9 ofrece
una guía de referencia al ensamblador de MIPS32.

[SWE] es un texto con un elevado nivel de complejidad, y debe ser consultado sólo cuando se
quiera obtener información muy avanzada sobre MIPS.

Las tres últimas referencias corresponden con sendos manuales oficiales de MIPS. El primero de
ellos es una introducción general a la arquitectura; el segundo es una referencia muy amplia del
repertorio de instrucciones completo de MIPS32, que deberá ser consultada sólo puntualmente
para conocer detalles acerca de instrucciones concretas; y el tercero, incluido aquí para mantener
la referencia completa a los tres manuales oficiales, describe la arquitectura de recursos
privilegiados implementada en MIPS. Existen otros tres manuales similares que describen la
arquitectura MIPS de 64 bits.

3
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

El único lenguaje comprensible para la circuitería del computador es el lenguaje máquina. El


código máquina está formado por instrucciones que realizan tareas simples (copiar información,
sumar o restar dos cantidades numéricas, etc) codificadas en binario con secuencias de ceros y
unos. El lenguaje máquina es un lenguaje de bajo nivel, porque está íntimamente ligado a la
arquitectura del computador. Programar en bajo nivel da al programador un control directo sobre
las instrucciones ejecutadas en el programa. Por ello, el lenguaje máquina resulta conveniente para
comunicarse con el computador y especificar órdenes que éste será capaz de ejecutar.
Sin embargo, los lenguajes binarios resultan poco comprensibles para las personas. Además, la
programación en bajo nivel obliga a conocer las características de la máquina (las operaciones que
es capaz de realizar, los datos que puede usar, la organización de la memoria, etc.), y es muy
tediosa y poco productiva para programadores humanos, a quienes resulta más sencillo escribir
programas utilizando letras, dígitos, signos de puntuación, paréntesis, comillas, etc. Estos
lenguajes, denominados lenguajes simbólicos, son más legibles y fáciles de interpretar, y por
tanto:
• Los programadores son más productivos si emplean lenguajes simbólicos.
• El código escrito en un lenguaje simbólico es mucho más fácil de depurar y mantener.
Para facilitar la escritura de programas en bajo nivel se inventó el lenguaje ensamblador
(assembly language, lenguaje de ensamble), que es un lenguaje simbólico cuyas
instrucciones se codifican con nemotécnicos, y que permite al programador nombrar las direcciones
de memoria donde están los datos y las instrucciones del programa mediante nombres simbólicos
llamados etiquetas. El lenguaje ensamblador también cuenta con facilidades adicionales, como
son las directivas, que sirven para indicar al programa traductor que reserve espacio para
variables, para definir secciones en el programa, etc.
(CONTINÚA EN LA PÁGINA SIGUIENTE)
4
Traducción de programas
Lenguajes de bajo nivel y alto nivel / lenguajes binarios y lenguajes simbólicos

High-level swap(int v[], int k)


La programación en lenguaje language
program
{int temp;
temp = v[k];
ensamblador es un tanto tediosa  (in C) v[k] = v[k+1];
v[k+1] = temp;
LENGUAJES DE ALTO NIVEL. }

C compiler

Assembly swap:
Los humanos preferimos los lenguajes language muli $2, $5,4
program add $2, $4,$2
simbólicos  LENGUAJE (for MIPS) lw $15, 0($2)
lw $16, 4($2)
ENSAMBLADOR. sw $16, 0($2)
sw $15, 4($2)
jr $31

Assembler

La circuitería de un computador sólo Binary machine


language
00000000101000010000000000011000
00000000100011100001100000100001
es capaz de comprender y manejar program
(for MIPS)
10001100011000100000000000000000
10001100111100100000000000000100
señales binarias  LENGUAJE 10101100111100100000000000000000
10101100011000100000000000000100
MÁQUINA. 00000011111000000000000000001000

(VIENE DE LA PÁGINA ANTERIOR)


Hay una correspondencia directa entre las instrucciones en ensamblador y código máquina
(machine code), y por tanto el lenguaje ensamblador es también un lenguaje de bajo nivel, lo
cual limita la productividad de los programadores, ya que las instrucciones son muy simples y es
preciso conocer la arquitectura de la máquina. Los programas escritos en lenguajes de bajo nivel
no pueden transportarse entre computadores no compatibles entre sí.
Enfrente están los lenguajes de alto nivel, que facilitan la tarea de los programadores, pues su
sintaxis es más próxima a la forma de pensar de los humanos. Son lenguajes simbólicos que
permiten realizar un control estructurado del flujo de ejecución del programa, así como definir y
utilizar diferentes tipos de datos. Programar en un lenguaje de alto nivel es mucho más productivo
y fácil, pues su sintaxis es mucho más próxima a los humanos, se dispone de mayor potencia
expresiva, los programas tienen menos líneas de código, y no es necesario un conocimiento
exhaustivo de las características de la máquina concreta que se está usando. Además, los
programas escritos en lenguajes de alto nivel son en gran medida transportables entre diferentes
máquinas, sobre todo si comparten el mismo sistema operativo. Hoy en día la práctica totalidad de
los programadores trabaja con lenguajes de alto nivel.
Para ejecutar un programa no escrito en código máquina hay que realizar una traducción previa
mediante un programa traductor. Existen tres tipos de traductores:
• Ensambladores (assemblers): traducen programas escritos en lenguaje ensamblador a
código máquina, generando un fichero con código objeto.
• Compiladores (compilers): traducen programas escritos en lenguajes de alto nivel a
lenguaje de bajo nivel, generando un fichero con código ensamblador o código objeto.
• Intérpretes: traducen programas en alto nivel a bajo nivel, intercalando la traducción del
código fuente con la ejecución de las instrucciones de máquina que van generando.
5
Traducción de programas
Proceso de traducción

A continuación vamos a estudiar el proceso de traducción de programas, partiendo de un código


fuente escrito en un lenguaje de alto nivel como C, hasta llegar a la construcción del fichero
ejecutable final y su posterior carga en memoria principal para ser ejecutado.
Los programadores normalmente trabajan en algún lenguaje de alto nivel, pero para poder ejecutar
sus programas en un computador, es preciso realizar un proceso de traducción que acabe
produciendo un programa en lenguaje máquina, ya ejecutable y equivalente al original. Existen
diferentes tipos de programas traductores, si bien nos vamos a centrar en los compiladores.
El programador escribe el código (programa) fuente en un lenguaje de su elección,
normalmente de alto nivel. El código fuente puede contener errores sintácticos o de escritura. El
código fuente sirve para alimentar al compilador (compiler), que es un traductor de lenguaje de
alto nivel a lenguaje de bajo nivel (ensamblador o directamente lenguaje máquina). Si el fuente
tiene errores sintácticos, es preciso corregirlos y compilar de nuevo.
Si el compilador ha producido un programa escrito en lenguaje ensamblador (lenguaje de
ensamble, assembly language), es preciso traducirlo a lenguaje máquina, utilizando un
programa llamado ensamblador (assembler), creándose entonces el código (programa)
objeto, que aún no será ejecutable, pues normalmente le faltarán otros módulos de biblioteca
(library routines) con funciones adicionales y que ya están traducidos a código máquina.
La unión del código objeto y el código de biblioteca es realizada por el montador de enlaces o
enlazador (linker), encargado de resolver las referencias entre módulos (referencias cruzadas o
referencias externas) y crear el código (programa) ejecutable final, en código máquina, y que,
si está mal diseñado, puede contener errores lógicos (de funcionamiento).
Para ejecutar el programa, invocaremos al programa cargador (loader), perteneciente al sistema
operativo, que cargará el código en memoria y lo dejará listo para su ejecución.

6
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

El repertorio o juego de instrucciones de un computador es conjunto de instrucciones de


máquina que es capaz de ejecutar. Estas instrucciones realizan operaciones sencillas sobre unos
pocos operandos (a lo sumo tres). No todas las instrucciones de un repertorio tienen igual número
de operandos, pues hay operaciones unarias, binarias e incluso hay instrucciones sin operandos.
El repertorio de instrucciones de una máquina es uno de los elementos más importantes de la
ARQUITECTURA DEL REPERTORIO DE INSTRUCCIONES (ISA: instruction set architecture,
también conocida como la ARQUITECTURA DE LA MÁQUINA), que marca la frontera entre el
hardware y el nivel más bajo de la programación. Además del propio repertorio de instrucciones, la
ISA incluye los modos de direccionamiento, los tipos y tamaños de datos utilizados, la organización
de la memoria, los elementos para el tratamiento de excepciones, las tablas hardware, y muchos
otros detalles. El código de bajo nivel no es transportable entre máquinas diferentes, salvo que
compartan la ISA. La ISA oculta la materialización concreta de un computador. Programar en bajo
nivel requiere un conocimiento profundo de la ISA del computador.
Las instrucciones se codifican en binario, dando lugar a los formatos de instrucción. En un
computador coexisten varios formatos de instrucción diferentes, dependiendo del número de
operandos, la ubicación de los mismos, etc.
El código binario de las instrucciones se divide en campos:
• Código de operación: identifica unívocamente a la instrucción leída. A veces incorpora
campos modificadores que lo matizan, indicando alguna peculiaridad de la operación o de
los operandos.
• Operandos fuente: permiten conocer la ubicación de los datos de entrada que utiliza la
instrucción para realizar el cálculo requerido.
• Operando destino: permite saber dónde se guardará el resultado de la operación.
(CONTINÚA EN LA PÁGINA SIGUIENTE)
7
Repertorios de instrucciones

• Repertorio o juego de instrucciones de un computador:


conjunto de instrucciones de máquina que es capaz de ejecutar.

• ARQUITECTURA DEL REPERTORIO DE INSTRUCCIONES (ISA:


instruction set architecture): frontera entre el hardware y el nivel más
bajo de la programación.

INSTRUCCIÓN DE MÁQUINA (EN BINARIO)


Dirección de Código de Campos de operando
memoria operación (tantos como sea preciso)

• Modos de direccionamiento: mecanismos para acceder a los


datos.
 Operandos inmediatos: en las propias instrucciones.
 Operandos en registros.
 Operandos en memoria.

(VIENE DE LA PÁGINA ANTERIOR)


Los operandos pueden residir en memoria (es el caso de las variables), en registros visibles para
el programador (se usan para mantener datos temporales, o para copiar temporalmente variables
en ellos para operar) o dentro de la propia instrucción. Este es el caso de las constantes, también
llamadas datos inmediatos, que pueden usarse sólo como operandos fuente.
Los campos de operando indican cómo encontrar los datos, pero no contienen los datos como
tales, salvo en el caso de los datos inmediatos. Por ello, los campos de operando incluyen
exclusivamente la información necesaria para poder aplicar los modos de direccionamiento, que
son los mecanismos con los que cuenta el procesador para localizar los datos empleados por las
instrucciones. Así, en el caso de un dato residente en un registro, la instrucción contendrá el
número que lo identifica, mientras que en el caso de una variable en memoria, la instrucción
contiene su dirección efectiva, o bien la información necesaria para calcularla (el número
identificativo de un registro base y un desplazamiento constante que se le suma, etc).
Los repertorios de instrucciones incluyen múltiples instrucciones de diferentes tipos.
Las instrucciones de transferencia se emplean para copiar información de un elemento de
almacenamiento a otro. Pueden ser cargas (load, copia memoria  registro o constante 
registro), almacenamientos (store o copia registro  memoria) o movimientos (move o copia
registro  registro o memoria  memoria).
Las instrucciones ariméticas con enteros (suma, resta, producto, división) tienen dos operandos
fuente y un operando destino (o dos en la división: cociente y resto). Los datos son todos de igual
ancho menos el resultado del producto, que es el doble de ancho que los operandos fuente. El
cambio de signo o el cálculo del valor absoluto son operaciones unarias (con un operando fuente).
(CONTINÚA EN LA PÁGINA SIGUIENTE)
8
Repertorios de instrucciones
Tipos de operaciones

• Transferencia de datos: cargar, almacenar, mover.


• Aritméticas para enteros: sumar, restar, multiplicar, dividir, etc.
• Aritméticas para coma flotante: sumar, restar, multiplicar, dividir,
raíz cuadrada, logaritmo, exponencial, etc.
• Operaciones lógicas bit a bit: or, and, not, xor, etc.
• Activación condicional: si r1 es menor que r2, entonces activar r3.
• Desplazamiento y rotación: desplazamiento lógico o aritmético a la
izquierda, a la derecha o circular.
• Control de programa: bifurcación condicional o incondicional, salto,
salto con enlace, etc.
• Control de sistema: llamar al sistema operativo, producir excepción,
etc.
• Otras: no operación, etc.

(VIENE DE LA PÁGINA ANTERIOR)


Las instrucciones aritméticas en coma flotante incluyen la suma, la resta, el producto, la
división, la raíz cuadrada, el logaritmo, la función exponencial, funciones trigonométricas, etc.
Las instrucciones lógicas bit a bit (bitwise) toman dos operandos fuente a y b de n bits de
ancho, y realizan la operación lógica ai op bi para cada uno de los bits, produciendo un resultado
de n bits de ancho. Las operaciones típicas son or, and, nor, xor, not (unario), etc.
Las instrucciones de activación condicional (set) o comparación (compare) comparan dos
operandos fuente según una cierta condición, y si resulta cierta, ponen un 1 en dato (activación) o
en uno o varios biestables de estado (comparación).
Las instrucciones de desplazamiento y rotación realizan desplazamientos lógicos o aritméticos a
la izquierda, a la derecha o circulares (rotaciones) de una longitud dada por uno de los operandos.
Las instrucciones aritméticas para enteros o coma flotante, lógicas, de activación condicional, los
desplazamientos y las rotaciones son instrucciones de proceso.
Las instrucciones de control de programa pueden alterar la secuencia de ejecución de
instrucciones para crear estructuras de selección, bucles y subprogramas. Existen diferentes tipos:
bifurcaciones condicionales (bifurcan sólo si se cumple una cierta condición) frente a
incondicionales (siempre bifurcan); ramificaciones (la dirección destino se calcula sumando el
PC más un desplazamiento) frente a saltos (se da una dirección completa); y bifurcaciones con
retorno o con enlace (link) (retienen la dirección de la siguiente instrucción, llamada dirección
de retorno) frente a bifurcaciones sin retorno o sin enlace (no retienen ninguna dirección).
Otras instrucciones son las de control de sistema (llamada al sistema operativo, control de caché,
etc.) o las misceláneas (no operación, etc).

9
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

Antes de continuar en el tema, vamos a dedicar un apartado a describir las características básicas
de los procesadores de la familia MIPS, centrándonos en los procesadores de 32 bits.

10
Características principales de MIPS
Modelo de programación de MIPS R2000

• Familia de máquinas RISC.


Memory
• MIPS32: ancho de palabra de 32 bits.
• Ancho de las instrucciones: 32 bits.
• Ancho de las direcciones: 32 bits.
CPU Coprocessor 1 (FPU)
• Tamaño de los datos en instrucciones:
Registers Registers
 Byte (8 bits): sufijo “b”.
$0 $0
 Halfword (16 bits): sufijo “h”.
 Word (32 bits): sufijo “w”.
$31 $31  Doubleword (64 bits): sufijo “d”.
Arithmetic Multiply • Coprocesadores en MIPS.
unit divide
 Hasta 4 coprocesadores con hasta 32
Arithmetic registros cada uno.
unit
Lo Hi
 Coprocesador 0: control de sistema
(System Control Coprocessor).
 Coprocesador 1: coma flotante.
Coprocessor 0 (traps and memory)
Registers  Coprocesador 2: reservado.
BadVAddr Cause  Coprocesador 3: coma flotante en
Status EPC ciertas arquitecturas de 64 bits.

Las máquinas MIPS componen una familia de procesadores RISC (computador con repertorio de
instrucciones “reducido”). Nosotros nos vamos a centrar en las arquitecturas MIPS32, con ancho
de palabra de 32 bits, que fueron las primeras de la familia. En ellas, el ancho de las
direcciones es de 32 bits. Por contra, existen otros procesadores de la familia con ancho de
palabra y direcciones de 64 bits. En todo caso, las instrucciones tienen un ancho de 32 bits.
Las instrucciones de MIPS32 admiten cuatro tamaños para los operandos:
• Byte (8 bits): las instrucciones tienen el sufijo “b”.
• Halfword (16 bits): instrucciones llevan el sufijo “h”.
• Word (32 bits): las instrucciones a veces llevan el sufijo “w”. Es el tamaño natural.
• Doubleword (64 bits): las instrucciones ensamblador llevan el sufijo “d”.
Los coprocesadores en MIPS son partes del procesador destinadas a cumplir funciones
específicas, y que cuentan con registros propios. Puede haber hasta 4 coprocesadores, con un
máximo de 32 registros cada uno.
El coprocesador 0 (System Control Coprocessor) es obligatorio, y realiza el control de
sistema. Este coprocesador controla el subsistema de memoria caché, soporta la memoria virtual y
la traducción de direcciones virtuales a físicas, el manejo de excepciones, los cambios en el modo
de ejecución (usuario, núcleo y supervisor) y proporciona control de diagnóstico y recuperación
ante errores de sistema, entre otras funciones.
Los demás coprocesadores son optativos, si bien el coprocesador 1 es muy frecuente, ya
contiene una unidad de coma flotante (Floating Point Unit, FPU) que realiza operaciones con
datos expresados en dicho sistema de representación. El coprocesador 2 está reservado para
implementaciones específicas, y el coprocesador 3 sirve para incorporar una nueva unidad de
coma flotante en ciertas arquitecturas MIPS de 64 bits.

11
Características principales de MIPS
Registros de la UCP

Número Alias Uso


$0 $zero Constante con valor 0 (no se puede escribir sobre él)

$1 $at Usado en pseudoinstrucciones

$2 – $3 $v0 – $v1 Valores devueltos en subrutinas

$4 – $7 $a0 – $a3 Argumentos en subrutinas (argument registers)

$8 – $15 $t0 – $t7 Registros para datos temporales (temporary registers)

$16 – $23 $s0 – $s7 Registros para variables de larga duración (saved registers)

$24 – $25 $t8 – $t9 Registros para datos temporales (temporary registers)

$26 – $27 $k0 – $k1 Reservados para sistema operativo (kernel registers)

$28 $gp Puntero global a variables estáticas (global pointer)

$29 $sp Puntero de pila (stack pointer)

$30 $fp Puntero de marco (frame pointer)

$31 $ra Dirección de retorno en subrutinas (return address)

Hi y Lo son registros de propósito específico de la UCP destinados a contener el


resultado de las instrucciones de producto y división en coma fija.

Los registros de la UCP se numeran desde el 0 hasta el 31, y para usarlos como operandos se
escribirá su número precedido del símbolo $. El registro $0 es especial, pues su contenido está
permanentemente a 0. El registro 31 tiene la peculiaridad de que se usa como operando implícito
en ciertas instrucciones de salto a subrutina. Los demás registros son todos iguales, y existe un
convenio que indica cómo usarlos (cada uno tiene un alias o sobrenombre indicativo):
• $1: no debe usarse por los programadores, ya que lo usa el traductor de ensamblador a la hora
de traducir pseudoinstrucciones.
• $2-$3: se usan para que las subrutinas graben en ellos el valor de retorno.
• $4-$7: se emplean para introducir los cuatro primeros argumentos de las subrutinas.
• $8-$15, $24-$25: se usan para mantener datos temporales.
• $16-$23: se utilizan para mantener copias de las variables de memoria. Muchas instrucciones
exigen que sus operandos estén en registros. Por tanto, cuando queremos usar variables
residentes en memoria, tenemos que copiarlas (cargarlas) en registros antes de operar con ellas,
y estos registros suelen utilizarse para ello.
• $26-$27: utilizados por las rutinas del sistema operativo. Los programas de usuario nunca
deberían utilizarlos.
• $28: puntero a una región de variables estáticas en memoria.
• $29: puntero de pila. La pila es una región de memoria esencial para las llamadas a subrutina.
• $30: puntero de marco. Se usa también en llamadas a subrutina, para apuntar a los datos
locales de la misma.
• $31: registro de dirección de retorno. Se usa en subrutinas, para volver al punto de llamada.

Hi y Lo son registros especiales utilizados en productos y divisiones en coma fija. En el producto, el


par Hi-Lo contiene el resultado completo, dado que éste tendrá el doble de bits de ancho que los
operandos fuente. En la división, Hi contendrá el resto y Lo el cociente.

12
Características principales de MIPS
Alineamiento y accesos a memoria

• Unidad direccionable mínima: octeto.


• Normalmente se exige que los accesos a memoria estén alineados.
– Octeto: sin alinear (= en cualquier posición de memoria).
– Media palabra: dirección par.
– Palabra: dirección múltiplo de 4.
– Doble palabra: dirección múltiplo de 8.
• Datos de más de un octeto:
– Little endian : el octeto menos significativo primero.
– Big endian : el octeto más significativo primero.
Ejemplo: dato 0x12345678 en la posición 0x10000000 de memoria en una memoria
organizada en octetos (NOTA: 0x indica que se usa base hexadecimal).
Little endian Big endian

0x10000000 0x78 0x10000000 0x12


0x10000001 0x56 0x10000001 0x34
0x10000002 0x34 0x10000002 0x56
0x10000003 0x12 0x10000003 0x78

Aunque la memoria de los sistemas MIPS32 está organizada en palabras de 32 bits, la mínima
unidad direccionable es el octeto. Así, cada objeto de tamaño n*8 bits ocupa n posiciones de
memoria consecutivas.
En casi todas las instrucciones de MIPS32 se exige que los accesos a memoria estén alineados.
Esto significa que existen restricciones a la hora de ubicar un dato en memoria en función del
tamaño que ocupe. Así, un dato de tamaño palabra deberá forzosamente comenzar en una
posición cuya dirección deberá ser múltiplo de 4 (terminada por dos ceros en binario), dado que
ocupa 4 octetos. Del mismo modo, un dato de tamaño doble palabra comenzará en una posición de
memoria con dirección múltiplo de 8 (terminada por tres ceros en binario), y un dato de tamaño
media palabra comenzará en una posición con dirección par (terminada por un único 0 en binario).
No existen restricciones de alineamiento a la hora de ubicar datos de tamaño octeto.
Con datos que ocupan más de un octeto surge un nuevo problema: ¿cómo interpretar la
información? ¿Cuál es el octeto más significativo del dato? Hay dos posibles criterios:
• Big endian, o extremo más significativo primero: se ubica el octeto más significativo al
principio del todo, en la dirección más baja, dejando el menos significativo para la última
posición ocupada por el dato, es decir, la de la dirección más alta.
• Little endian, o extremo menos significativo primero: se pone el octeto menos
significativo en la dirección más baja, al principio del todo, y el más significativo en la
última posición ocupada por el dato, en la dirección más alta.
En los procesadores x86 y x64 de Intel se usa el criterio little endian, mientras que en los
procesadores de la familia 68K de Motorola se usaba big endian. En los procesadores MIPS están
implementados ambos criterios, y en un momento dado se puede elegir uno de ellos mediante un
bit en un registro de control del coprocesador 0.

13
Características principales de MIPS
Modos de direccionamiento y repertorio de instrucciones

• Modos de direccionamiento:
 Datos: direccionamiento directo a registro / direccionamiento inmediato /
direccionamiento indirecto a registro con desplazamiento.
 Instrucciones destino de bifurcación: direccionamiento relativo a PC con
desplazamiento / direccionamiento pseudodirecto.
 “Pseudodireccionamientos” no soportados por la circuitería.

• Operaciones en MIPS = instrucciones + pseudoinstrucciones.


 Transferencia: cargas, almacenamientos, movimientos entre registros.
 Proceso: aritméticas, lógicas, coma flotante, activación condicional,
desplazamientos, etc.
 Bifurcación: condicional / incondicional, ramificación / salto con o sin enlace.
Otras: sistema, no operación, etc.

• Directivas: .data, .text, .space, .word, .asciiz, .eqv, etc.

• Máquina virtual: pseudoinstrucciones + pseudodireccionamientos + alias


de registros.

MIPS32 es un procesador RISC con muy pocos modos de direccionamiento. Para acceder a datos
hay tres modos, uno por cada ubicación posible: en registro (directo a registro), en la instrucción
(inmediato) o en memoria (indirecto a registro con desplazamiento). Para designar
instrucciones hay dos opciones: dando una dirección absoluta (pseudodirecto) o dando una
dirección relativa al PC (relativo al PC con desplazamiento). El ensamblador de MIPS permite
dar direcciones mediante etiquetas, aunque esto no esté soportado por la circuitería (por lo que es
un pseudodireccionamiento): en estos casos, el traductor de ensamblador se encarga de
generar una o varias instrucciones que usan direccionamientos reales soportados por el hardware.
MIPS32 presenta instrucciones y pseudoinstrucciones de diferentes tipos:
• Transferencia: cargar (lw, lh, lhu, lb, lbu: memoria  registro; li: constante  registro; la:
carga dirección en registro), almacenar (sw, sh, sb), copiar registro  registro (move), etc.
• Aritméticas con enteros: suma (add, addu, addi, addiu), resta (sub, subu), producto (mul,
mult, multu), división (div, divu), resto (rem, remu), cambio de signo (neg, negu), etc.
• Activación condicional: activar si menor que (slt, slti, sltu, sltiu), etc.
• Lógicas: and, andi, or, ori, xor, xori, not, nor.
• Desplazamiento: lógico (sll, sllv, srl, srlv), aritmético (sra, srav), circular (rol, ror).
• Coma flotante: l.s (cargar), s.s (almacenar), mov.s (copiar registro  registro), add.s, sub.s,
mul.s, div.s, abs.s, neg.s, cvt (convertir), c (comparar), etc (.s: precisión simple, .d: doble).
• Control de programa: ramificaciones (beq, bne, etc), saltos (j, jr; con enlace: jal, jalr), etc.
• Otras: llamada a sistema (syscall), no operación (nop), etc.
Las instrucciones aritméticas y lógicas con sufijo i tienen un segundo operando inmediato de 16
bits que se extiende en signo (en operaciones aritméticas) o con ceros (en operaciones lógicas).
Las que llevan el sufijo u usan datos en binario puro, y las demás usan datos en complemento a 2.
Las directivas tienen nemotécnicos que comienzan por el carácter “.” .
14
Características principales de MIPS
Espacios de direcciones de memoria

0x80000000
0xFFFFFFFF 0x7FFFFFFF Espacio para pila

Espacio traducido (kseg2) Memoria


1 GB
para el SO
0xC0000000
Espacio no traducido y no
ubicable en caché (kseg1)
0xA0000000 512 MB
Espacio no traducido y ubicable
en caché (kseg0)
0x80000000 512 MB
Espacio para datos dinámicos
0x10010000 (heap)
Memoria de (como máximo)

Espacio de memoria de usuario


usuario gp = 0x10008000
Espacio para datos estáticos
(64 kbits como máximo)
(kuseg) (totalmente 0x10000000

2 GB ubicada en Segmento de texto


memoria (instrucciones del programa)
virtual) 0x00400000
0x00000000 Reservado
0x00000000

$gp: puntero que apunta a las variables estáticas.

Con un ancho de direcciones de 32 bits, el mapa de memoria de MIPS32 alcanza un tamaño total
de 4 GB. Los 2 GB inferiores constituyen la memoria de usuario (kuseg), destinada a contener los
programas de usuario con sus datos. Los 2 GB superiores son para el sistema operativo. El espacio
de la memoria de usuario está dividido en una serie de zonas o segmentos:
• 0x00000000 – 0x003FFFFF: esta zona está reservada.
• 0x00400000 – 0x0FFFFFFF: constituye el segmento de texto, que contiene la sección de
código del programa, es decir, las instrucciones ejecutables del mismo.
• 0x10000000 – 0x1000FFFF: es la zona para datos estáticos.
• 0x10010000 – 0x7FFFFFFF: es la zona para datos dinámicos, que incluye el montículo
(heap) y la pila (stack). La pila crece desde la dirección 0x7FFFFFFF hacia abajo, y el
montículo va desde la dirección 0x10010000 hacia arriba.
Las direcciones de memoria de usuario son direcciones virtuales, y se gestionan mediante la unidad
de gestión de memoria (MMU, Memory Management Unit) del procesador.
El espacio de memoria para el sistema operativo se reparte del siguiente modo:
• 0x80000000 – 0x9FFFFFFF (kseg0): es una zona con direcciones reales, no virtuales, y
por tanto no está gestionada por la MMU, pero sí puede ubicarse en memoria caché. Por
ello, esta zona de memoria es apta para contener código crítico del sistema operativo.
• 0xA0000000 – 0xBFFFFFFF (kseg1): es una zona con direcciones reales (no gestionada
por la MMU) y que no puede ubicarse en caché. Por tanto, es una zona apta para ubicar
los puertos de E/S.
• 0xC0000000 – 0xFFFFFFFF (kseg2): es una zona con direcciones virtuales, es decir, que
está gestionada por la MMU. Esta zona es apta para código no crítico del sistema
operativo.
El registro $gp es un puntero global que normalmente apunta a la zona de variables estáticas.
15
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

El lenguaje ensamblador no es un lenguaje de formato libre, sino que los programas se estructuran
en líneas. En principio, en cada línea habrá una instrucción, que constará de:
• Etiqueta (opcional).
• Nemotécnico.
• Operandos.
• Comentarios.
Cada campo del formato binario de la instrucción se corresponde con un campo en la instrucción
ensamblador.
Así, el código de operación se expresa mediante un nemotécnico, nombre corto que indica qué
operación realiza la instrucción.
Los campos de operando y resultado en ensamblador van en un cierto orden y están separados
por comas. Lo normal es que el operando destino vaya en primer lugar, con los operandos fuente a
continuación.
Cuando el programa se está ejecutando, cada instrucción se ubica en una posición de memoria, y
el campo de etiqueta, que es opcional, contiene un nombre simbólico que servirá al programador
de ensamblador para recordar esa dirección y utilizarla en otras instrucciones.
En ensamblador, tras los operandos puede incluirse un campo de comentarios, también opcional,
y que a menudo comienza por un carácter especial. A veces hay comentarios que ocupan la línea
completa: en estos casos, es imprescindible que el comentario comience por dicho carácter
especial.
(CONTINÚA EN LA PÁGINA SIGUIENTE)

16
Estructura de un programa en ensamblador
Formatos de instrucción y líneas de código en ensamblador

INSTRUCCIÓN DE MÁQUINA (EN BINARIO)


Dirección de Código de Campos de operando
memoria operación (tantos como sea preciso)

Etiqueta: Nemotécnico Res,Op1,Op2 # Comentarios


LÍNEA DE CÓDIGO EN ENSAMBLADOR

• Líneas en un programa ensamblador:


 Instrucciones.
 Pseudoinstrucciones.
 Directivas.
 Macroinstrucciones.
 Comentarios.

(VIENE DE LA PÁGINA ANTERIOR)


Las pseudoinstrucciones son operaciones aceptadas por el traductor de ensamblador, pero no
soportadas directamente por la circuitería. Tienen apariencia de instrucciones normales, con
nemotécnico y operandos. Pero cuando el traductor de ensamblador se encuentra con una
pseudoinstrucción en el programa, sustituye por una instrucción o una secuencia de instrucciones
reales soportadas por la circuitería que realiza la operación pedida.
Las directivas son órdenes procesadas por el traductor de ensamblador. Tienen aspecto de
instrucciones, con su nemotécnico y a veces con operandos y etiqueta, pero no lo son, y no
generan código ejecutable. Se utilizan para indicar determinadas acciones al traductor de
ensamblador, como delimitar secciones de texto y código, reservar espacio en memoria para
variables, etc.
Las macroinstrucciones son secuencias parametrizables de operaciones definidas por el
programador, que es quien las define a través de directivas e instrucciones. La definición de una
matroinstrucción comienza por una directiva especial que va acompañada de una etiqueta (el
nombre de la macroinstrucción) y de unos operandos que sirven como parámetros. A continuación
se escriben las instrucciones que forman la secuencia de operaciones indicada por la
macroinstrucción. En dichas instrucciones pueden aparecer los parámetros como operandos.
Finalmente, la definición de la macroinstrucción termina con otra directiva especial. Más adelante,
cuando el traductor de ensamblador se encuentra con un nemotécnico que coincide con el nombre
de la macroinstrucción acompañado de sus correspondientes operandos, realiza la expansión de la
misma, reemplazando dicho nemotécnico por la secuencia de instrucciones indicadas en la
definición, y sustituyendo los parámetros debidamente.

17
Estructura de un programa en ensamblador
Estructura de un programa en ensamblador de MIPS

.data
Sección de datos

.text

tabulaciones

Sección de código

li $v0,17
fin de programa li $a0,0
syscall

Un programa en ensamblador de MIPS consta de al menos dos secciones:


• Sección de datos: contiene directivas que permiten reservar espacio de memoria para
variables (.space, etc), en ocasiones rellenando dicho espacio con una serie de valores
iniciales (.word, .half, .byte, .asciiz, etc). Además en esta sección también puede haber
directivas para definir constantes en el código (.eqv). La sección de datos comienza justo
detrás de la directiva .data.
• Sección de texto: aquí van las instrucciones ejecutables del programa, tanto las escritas
por el programador como las generadas por el traductor a partir de las
pseudoinstrucciones y de la expansión de las macroinstrucciones existentes en el código.
La sección de código comienza donde encontramos la directiva .text.
Un fichero fuente puede contener varias secciones de datos y código intercaladas. Al ensamblar el
fichero, el traductor junta las secciones de código por un lado y las de texto por otro.
Para finalizar la ejecución del programa cuando se genere el ejecutable, es preciso incluir en algún
punto del código una secuencia de fin de programa, que constará de una llamada a sistema
(syscall) precedida de un escritura del número de servicio en el registro $v0 (li $v0,17),
eventualmente acompañada de la asignación de argumentos del servicio en los registros de
argumento (li $a0,0).
En este ejemplo, para terminar el programa se invoca el servicio 17, indicando en el registro $a0 el
código de error de salida. Por convenio, si el código es 0, el programa se ha ejecutado
correctamente, y si el código es distinto de 0, significa que se ha producido algún error en la
ejecución del mismo.
Al ensamblar un fichero fuente se crea un fichero objeto con los datos y las instrucciones
traducidos a binario, junto con otra información necesaria para que el montador pueda realizar su
labor resolviendo las referencias externas y generando el código ejecutable final.
18
Estructura de un programa en ensamblador
C frente a ensamblador MIPS

#include <stdio.h> suma:


.data
.space 4
int suma; cont: .space 4
n: .space 4
int cont; tira1: .asciiz "Introduzca el valor límite: "
tira2: .asciiz "El valor de la suma es "
int n; .text
int main (void) { main: li
la
$v0,4
$a0,tira1
printf(“Introduzca el valor límite: “); syscall
li $v0,5
scanf(“%d”,&n); syscall
sw $v0,n
suma = 0; li $s0,0
sw $s0,suma
cont = 1; li $s1,1
while (cont <= n) { while:
sw
lw
$s1,cont
$s2,n
suma = suma + cont; bgt
lw
$s1,$s2,endwhile
$s0,suma
cont = cont + 1; lw $s1,cont
add $s0,$s0,$s1
} sw $s0,suma
addi $s1,$s1,1
printf(“El valor de la suma es %d“, sw $s1,cont
suma); endwhile:
j
li
while
$v0,4
return 0; la
syscall
$a0,tira2

} li $v0,1
lw $a0,suma
syscall
li $v0,17
li $a0,0
syscall

En esta diapositiva se muestra un ejemplo de traducción de un programa escrito en un lenguaje de


alto nivel (C) a lenguaje ensamblador de MIPS. La sintaxis de C es más próxima al programador, y
sus construcciones son mucho más potentes que las de ensamblador. En ensamblador las
operaciones son muy simples: copiar un dato de un lugar a otro, sumar dos datos y guardar el
resultado en un tercero, saltar a una cierta instrucción cuya dirección viene dada por una etiqueta,
etc.
Vemos arriba del todo el comienzo de la sección de datos, con tres directivas de creación de
espacio para sendas variables de tamaño palabra sin valor inicial (.space 4). Cada hueco
corresponderá con una variable, cuyo nombre corresponde con la etiqueta que acompaña a la
directiva (suma, cont y n). Detrás de ellas tenemos dos directivas de creación de tiras de
caracteres con valor inicial y terminadas en carácter nulo (.asciiz). El contenido de las tiras
aparece entre comillas dobles.
A continuación aparece la sección de texto (a partir de la directiva .text). Se observa que la
primera instrucción ejecutable del programa lleva asociada la etiqueta main. Este es un hecho
bastante habitual, ya que la función principal en los programas en C se llama precisamente main.
Detrás se encuentran las instrucciones y pseudoinstrucciones que implementan las operaciones del
programa original en C, con la salvedad de que las llamadas a las funciones printf y scanf han
sido reemplazadas por llamadas a servicios del sistema operativo (syscall) que realizan las
operaciones de impresión por pantalla. Como puede verse, la sección de texto termina generando
un código de ejecución correcta e invocando al servicio de terminación.

19
Estructura de un programa en ensamblador
Etiquetas y tabla de símbolos

• Etiquetas: representación simbólica de direcciones en el programa.


 Direcciones de datos: cuando acompañan a directivas.
 Direcciones de instrucciones: cuando acompañan a instrucciones o
pseudoinstrucciones.

• Tabla de símbolos: contiene todas las etiquetas del programa.


 Creada por el traductor de ensamblador.
 Usada y completada por el montador.
Nombre Valor
main 0x00400000
while 0x00400038
endwhile 0x00400074
suma 0x10010000
cont 0x10010004
n 0x10010008
tira1 0x1001000c
tira2 0x10010029

En la traducción de un programa fuente se genera una tabla de símbolos, que contiene los
símbolos definidos por el usuario y el valor de los mismos. En nuestro ejemplo los símbolos de
usuario se definen mediante etiquetas, que representan la dirección del objeto al que acompañan,
ya sea dato o instrucción. En realidad, cada módulo genera valores para las etiquetas partiendo de
una cierta dirección base, común a todos ellos. Cuando el montador va a generar el código final,
asigna las direcciones de comienzo de cada uno de los módulos y reubica los símbolos (es decir,
modifica su valor en función de la dirección base asignada al módulo) para que el código generado
sea correcto. Aquí se muestra la tabla de símbolos final, una vez realizada la operación de montaje
del ejecutable.
Como puede verse, en cada entrada de la tabla de símbolos va un símbolo con su nombre y valor.
En ocasiones, las entradas de la tabla incorporan más campos (el tamaño del símbolo, el tipo, si el
símbolo es redefinible, etc).

20
Estructura de un programa en ensamblador
Montaje del código ejecutable

Object file

sub:

Object file ꞏ Executable file

Instructions main: main:
jal ??? jal printf
ꞏ ꞏ
ꞏ ꞏ
ꞏ ꞏ
jal ??? jal sub
printf:
call, sub Linker ꞏ
Relocation call, printf ꞏ
records ꞏ
sub:

C library ꞏ

printf:


Cuando un programa se divide en varios módulos, el montador de enlaces tiene que unirlos todos
para formar el fichero ejecutable, colocando el código y los datos de todos ellos uno detrás de otro,
incluyendo los módulos de biblioteca, y resolviendo las referencias cruzadas entre ellos.
En la figura tenemos un código objeto con un programa principal que contiene llamadas a dos
subrutinas cuyo código objeto reside en otros módulos: en concreto, la rutina sub reside en un
módulo objeto creado de forma independiente, mientras que la rutina printf forma parte de la
biblioteca estándar de C.
El código objeto del módulo principal incluye la información de todas las referencias a símbolos no
resueltas en la compilación, así como la tabla de símbolos. Al unir todos los módulos, si consigue
resolver todas las referencias externas, el montador asigna las direcciones de memoria que
corresponderán a los distintos módulos en el ejecutable final, y después realiza la reubicación de
todas las referencias a memoria presentes en el código, con objeto de que apunten a las
direcciones adecuadas.
Si alguna referencia externa no puede ser resuelta por el montador, se produce un error y el código
ejecutable no puede ser generado.

21
Estructura de un programa en ensamblador
Software MARS: http://courses.missouristate.edu/KenVollmar/MARS/

Zona de edición
de código fuente

Registros
•UCP
Zona de mensajes: •Coprocesadores 0 y 1
•Mensajes de MARS (Mars Messages)
•Mensajes de ejecución y E/S por terminal (Run I/O)

Para realizar las actividades prácticas de la asignatura se ha elegido el software MARS, que es un
IDE (entorno integrado de desarrollo, integrated development environment) para escribir
programas en ensamblador de MIPS32 e incluso simular la ejecución de los mismos. Este software
está escrito en Java, con lo cual se puede ejecutar en múltiples plataformas, sin más que tener
correctamente instalado y configurado el runtime de la máquina virtual de Java (JRE: Java Runtime
Environment).
Si pinchamos en la pestaña Edit veremos la pantalla de edición de MARS, que desde la versión 4.0
permite mantener cargados para edición múltiples archivos fuente, cada uno en una pestaña. En la
zona inferior hay una zona de mensajes con dos pestañas, una para los mensajes que se generan
en la traducción (Mars Messages) y otra para los generados durante la ejecución de los
programas (Run I/O).
En la zona derecha aparecen los bancos de registros de la UCP (Registers), del coprocesador de
coma flotante (Coproc 1) y del coprocesador de control del sistema (Coproc 0).
En la zona superior se muestra un menú de opciones y una serie de iconos que sirven como atajo a
ciertas opciones seleccionadas del menú.
Si escribimos un programa en ensamblador que contiene errores, o bien se produce un fallo de
montaje, en la zona de mensajes se nos mostrará información acerca del error o errores
cometidos. Si el ensamblaje y montaje se realizan correctamente, automáticamente se activa la
pestaña de ejecución (Execute).

22
Estructura de un programa en ensamblador
Software MARS: http://courses.missouristate.edu/KenVollmar/MARS/

Segmento
de código
(segmento
de texto)

Etiquetas

Segmento
de datos
(estáticos +
dinámicos +
pila)

La pestaña de ejecución se activa automáticamente al ensamblar y montar código que no contiene


errores, mostrando una serie de ventanas que corresponden con:
• El segmento de texto (Text Segment) con la correspondencia entre el código fuente,
las instrucciones reales generadas, el código máquina (escrito en hexadecimal para
facilitar su lectura) y la dirección de memoria en la que se almacena cada instrucción.
• El segmento de datos (Data Segment) con el contenido de las variables del programa,
la pila, el heap, etc. Se puede seleccionar la región de memoria de datos que queramos
mediante un desplegable, y mediante sendos tics podemos mostrar los datos en decimal o
hexadecimal. En las últimas versiones aparece un tic adicional que permite visualizar los
contenidos de la memoria en formato de caracteres ASCII.
• La tabla de símbolos (Labels) con las etiquetas definidas por el usuario y las direcciones
que le corresponden. Esta ventana no aparece por defecto, siendo necesario activarla a
partir de la opción Show Labels Window (symbol table) del menú Settings.
A la derecha seguimos teniendo a la vista los registros, y abajo la zona de mensajes.
Desde esta pantalla de simulación podemos simular la ejecución del programa de un golpe o paso a
paso, y podemos definir puntos de ruptura (breakpoints) para detener la simulación en puntos
seleccionados (activando el tic Bkpt junto a la instrucción en la que queremos que la simulación se
pare). También podemos retroceder la simulación paso a paso.
MARS permite modificar el contenido de los registros y las variable de memoria antes de comenzar
a simular o en mitad de la simulación, siempre que esté detenida, sin más que hacer doble click
sobre el registro o variable de memoria seleccionada.
MARS ofrece múltiples llamadas a sistema que se pueden invocar mediante syscall:
entrada/salida por terminal o por fichero, generación de sonidos MIDI, hora del sistema, petición
de bloques de memoria dinámica, terminación del programa, etc.
23
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

Los programas en bajo nivel utilizan datos que pueden encontrarse en tres lugares diferentes:
• En memoria principal: es el caso de las variables.
• En registros de la UCP.
• En la propia instrucción: es el caso de las constantes que aparecen en el programa.
Al acceder a un dato utilizaremos un modo de direccionamiento, cuya naturaleza depende de:
• La ubicación del dato en cuestión (registro, memoria, constante en instrucción).
• La información utilizada para encontrarlo cuando es un dato residente en memoria.
En este apartado estudiaremos los modos de direccionamiento para acceder a datos, y lo haremos
partiendo de ejemplos de programas en C y analizando su posible traducción a ensamblador de
MIPS. Esto nos servirá para estudiar algunas instrucciones habituales en ensamblador, e incluso
para presentar los formatos binarios más comunes utilizados en MIPS.

24
Acceso a datos residentes en registros
Direccionamiento directo a registro

• Código C y ensamblador MIPS para sumar dos variables:


.data .data
int A, B, C;
A: .space 4 A: .space 4 # Copia en $s1
int main (void) {
B: .space 4 B: .space 4 # Copia en $s2

C: .space 4 C: .space 4 # Copia en $s3
A = B + C;
.text .text

… …
}
add A,B,C add $s1,$s2,$s3
… …

• Problema: ¡no accedemos a las variables en memoria!


• Direccionamiento directo a registro: operando residente en un
registro de propósito general de la UCP.
Register addressing
op rs rt rd ... funct Registers
Register

• Notación: $n

Los programas utilizan variables residentes en memoria. Por tanto, las instrucciones ensamblador
deben ser capaces de acceder a datos residentes en memoria para realizar cálculos. A la izquierda
de la diapositiva se muestra un pequeño fragmento de programa en C que suma dos variables
estáticas globales B y C, y guarda el resultado en una tercera variable A. La traducción lógica de
esta operación a ensamblador sería add A,B,C. Sin embargo, al ensamblar esta línea de código
obtendríamos un error. ¿Por qué?
Sabemos que las direcciones en MIPS32 ocupan 32 bits. Por tanto, la codificación binaria de esta
instrucción debería ocupar 6 (código de operación) + 6 bits (funct: modificador del código de
operación) + 32 (dirección del destino A) + 32 (dirección del primer sumando B) + 32 (dirección
del segundo sumando C) bits, o sea, que en total debería ocupar 108 bits. Dado cada instrucción
MIPS ocupa únicamente 32 bits, está claro que no es posible sumar dos variables en memoria y
guardar el resultado en otra variable.
En realidad la suma (y otras operaciones aritméticas y lógicas) usan datos residentes en registros.
Como en el banco de registros hay 32, basta con 5 bits para dar el identificador de cada uno de
ellos. Por tanto, la instrucción add $s1,$s2,$s3 ocuparía 6+6+5+5+5=27 bits, que ya sí nos
caben en una palabra de memoria (incluso nos sobran bits). Pero entonces con esta operación
no podemos usar directamente variables residentes en memoria. Este asunto lo
resolveremos más adelante.
El modo de direccionamiento directo a registro se emplea cuando en instrucciones en las que
tenemos uno o varios operandos residentes en registros de propósito general de la UCP. Este
direccionamiento se usa mucho, ya que el acceso a registros se realiza de forma muy rápida. En la
codificación binaria de la instrucción, cada campo de registro ocupa 5 bits. En los programas en
ensamblador, cuando usamos un dato residente en un registro anteponemos el símbolo $ al
número del registro utilizado (o a su alias o sobrenombre).
25
Suma con registros

add registro_destino,registro_fuente1,registro_fuente2
addu registro_destino,registro_fuente1,registro_fuente2

Genera excepción si
Ejemplo: add $s1,$s2,$s3
hay desbordamiento
addu $s1,$s2,$s3

$s2 -125
$s3 257

Resultado 132 $s1 132

Los datos forzosamente están en registros, y no se accede a memoria.


El resultado en ambas instrucciones es el mismo.

La instrucción add se utiliza para sumar dos datos residentes en registros, y pone el resultado en
un tercero. En ensamblador MIPS, el operando destino aparece en primer lugar, y los sumandos en
segundo y tercero. Al ejecutar add, se accede al banco de registros con los dos identificadores
numéricos de los operandos fuente (se necesita un banco de registros con dos puertos de salida)
para después enviar sus contenidos a las entradas de operando de la UAL, seleccionando en ella la
operación de suma. El resultado producido por la UAL se encamina hacia el puerto de entrada del
banco de registros, y dando el identificador de registro destino, se graba en él.
Para sumar dos datos residentes en registros también puede usarse la instrucción addu, que tiene
un formato similar a la anterior. add y addu funcionan igual, pero add genera una excepción por
desbordamiento si el resultado de la suma está fuera del rango de cantidades representables
(complemento a 2), mientras que addu no genera ninguna excepción. Las instrucciones sub y
subu son similares a add y addu, salvo que realizan una resta. La instrucción sub genera una
excepción si hay desbordamiento, mientras que subu no lo hace.
Hay instrucciones lógicas que también tienen sus tres operandos en registros: and, or, nor y xor.
Estas instrucciones realizan una operación lógica entre los operandos fuente bit a bit (bitwise), y
producen un resultado de 32 bits que se graba en el registro destino. Su codificación binaria es
similar a la de add, y todas ellas pertenecen al grupo de instrucciones de tipo R.
Hay instrucciones de tipo R que realizan desplazamientos lógicos (sll, srl, sllv, srlv) o aritméticos
(sra, srav) a izquierda o derecha. El primer operando fuente es el dato que vamos a desplazar, y
el segundo da la longitud del desplazamiento. Las instrucciones con nemotécnico terminado en v
tienen su segundo operando en un registro, y las demás tienen dicho operando en una constante
de 5 bits que va en el campo de 5 bits que sobraba en el formato. En los desplazamientos lógicos,
los bits que van entrando por la izquierda o la derecha son siempre 0, mientras que en los
aritméticos, que sólo son hacia la derecha, se realiza extensión de signo.
26
Formato de instrucción tipo R: add
Registro Registro Registro
Cód. Op. Funct
fuente 1 fuente 2 destino
Tipo R
xxxxxx rs rt rd shamt funct
(shamt: shift
amount en 6 5 5 5 5 6
instrucciones de 31-26 25-21 20-16 15-11 10-6 5-0
desplazamiento)
Ejemplo: add $s1, $s2, $s3
Operación Registro fuente 2
Registro destino Registro fuente 1

Código
Operando 1 Operando 2 Destino Shamt Función
operación
0 18 19 17 0 32

000000 10010 10011 10001 00000 100000

Notación compacta hexadecimal: 0x02538820

Las instrucciones con tres operandos en registros se codifican en lenguaje máquina mediante el
formato de instrucción tipo R, que tiene:
• Un campo de 6 bits (rs) para el código de operación, con todos sus bits a 0.
• Un campo de 5 bits (rt) para el identificador del primer registro fuente.
• Un campo de 5 bits (rd) para el identificador del segundo registro fuente.
• Un campo de 5 bits para el identificador del registro destino.
• Un campo de 5 bits, que vale 0, excepto en ciertas instrucciones de desplazamiento, en
que da la longitud del mismo (shamt viene de shift amount).
• Un campo de 6 bits llamado campo de función (funct), que es una extensión del
código de operación, y sirve para distinguir unas instrucciones de otras.
Además de add (funct=32), otras instrucciones de tipo R con tres operandos en registros son
addu (33), sub (34), subu (35), and (36), or (37), xor (38) y nor (39), o los desplazamientos de
longitud variable sllv (4), srlv (6) y srav (7). Las instrucciones slt (42) y sltu (43) comparan dos
operandos fuente rs y rt y ponen a 1 el registro destino rd si rs<rt (lt=less than), dejándolo a 0
en caso contrario. slt maneja operandos en complemento a 2, y sltu usa operandos en binario
puro. Las instrucciones sll (0), srl (2) y sra (3) dan la longitud del desplazamiento con un tercer
operando constante dado en los 5 bits del campo shamt.
Las instrucciones mult (funct=24) y multu (25) tienen dos operandos fuente. El campo rd vale 0,
porque el resultado se guarda en la pareja de registros hi-lo. Lo mismo sucede con div (26) y
divu (27), que ponen el cociente en lo y el resto en hi. Las instrucciones mult y div tienen
operandos en complemento a 2, mientras que multu y divu manejan operandos en binario puro.
Las instrucciones mfhi (10) y mthi (11) copian el contenido de hi en un registro o viceversa, y
mflo (12) y mtlo (13) hacen lo propio con lo. En estas cuatro instrucciones, todos los campos
están a 0 menos el de función y el de primer registro fuente o registro destino, según corresponda.

27
Acceso a constantes
Direccionamiento inmediato

• Código en C y ensamblador MIPS para sumar una variable y una


constante:
.data
int A, B;
A: .space 4
int main (void) {
B: .space 4

C: .space 4
A = B + 25;
.text


}
addi $s1,$s2,25

• Direccionamiento inmediato: operando constante representable


con 16 bits.
 Se extiende a 32 bits al operar (extensión de signo o con ceros).
Immediate addressing
op rs rt Immediate

• Notación: d16

El lenguaje ensamblador permite la inclusión de constantes en las instrucciones. Como el valor de


una constante no cambia a lo largo del programa, no es necesario reservar un espacio para
almacenarla en memoria. Por ello, tienen un tratamiento especial: se incluyen dentro de la
instrucción, en un campo específico. Por ello, cuando usamos constantes se dice que utilizamos
direccionamiento inmediato.
Veamos un ejemplo de suma de una variable con una constante. Ya hemos visto que en
ensamblador no pueden usarse variables directamente en operaciones aritméticas, así que será
preciso sumar la constante con un registro. La instrucción MIPS para ello es addi, que tiene como
operando destino un registro ($s1), y como primer operando fuente otro registro ($s2), mientras
que el otro (el dato inmediato) está en un campo de la propia instrucción.
La instrucción ensamblador sería addi $s1,$s2,25. Por tanto, la codificación binaria de esta
instrucción debería ocupar 6 (código de operación) + 5 (código del registro destino) + 5 (código
del registro fuente) + 32 (constante), lo cual totalizaría 48 bits. Dado que las instrucciones de MIPS
ocupan únicamente 32 bits en total, está claro que hay que reducir espacio. En este caso sólo se
puede reducir el campo de la constante, limitándose entonces su tamaño a 16 bits.
Los dos datos que sumaremos con addi deben tener el mismo tamaño. Como el registro tiene 32
bis de ancho, entonces es necesario realizar extensión de signo de la constante a 32 bits para
igualar su ancho con el del registro.
Entonces, el direccionamiento inmediato se puede usar cuando uno de los operandos es un
valor constante representable con 16 bits. El dato inmediato se almacena dentro de la instrucción
tal cual, y en ensamblador se codifica directamente con la constante numérica, sin más.

28
Suma con registros y constantes
addi registro_destino,registro_fuente1,constante16
addiu registro_destino,registro_fuente1,constante16
Genera excepción si
Ejemplo: addi $s1,$s2,-25
hay desbordamiento
addiu $s1,$s2,-25

$s2 125
inmediato -25

Resultado 100 $s1 100

El resultado en ambas instrucciones es el mismo.

La instrucción addi se utiliza para sumar un dato residente en registros con una constante, y
almacena el resultado en un tercero. En ensamblador MIPS, el operando destino aparece en primer
lugar, y los sumandos en segundo y tercero. Al ejecutar addi, se accede al banco de registros con
el identificador numérico del primer operando fuente, y para el segundo se accede al campo
inmediato de la instrucción, cuyo contenido pasa por un circuito de extensión de signo. Ambos
datos se envían a la UAL, seleccionando en ella la operación de suma. El resultado producido por la
UAL se encamina hacia el puerto de entrada del banco de registros, y se graba en el registro cuyo
identificador numérico corresponde con el del registro destino de la instrucción.
La instrucción addiu es similar a addi, pero addi genera una excepción por desbordamiento si el
resultado de la suma está fuera del rango de cantidades representables (complemento a 2),
mientras que addiu no genera ninguna excepción. Se da la circunstancia de que el dato inmediato
puede ser positivo o negativo (está representado en complemento a 2), haciendo innecesario
contar con una operación de resta con inmediato.
Hay instrucciones lógicas que también tienen un campo de operando inmediato de 16 bits (andi,
ori y xori). En este caso, la extensión a 32 bits de los mismos se realiza rellenando con ceros a la
izquierda (extensión con ceros).
Existen instrucciones condicionales que ponen un registro destino
Las instrucciones con campo de datos inmediatos de 16 bits tienen un formato binario llamado
formato de instrucciones de tipo I.

29
Formato de instrucción tipo I: addi
Registro Registro
Tipo I (carga o Cód. Op. Desplazamiento
base destino
almacenamiento,
ramificación xxxxxx rs rt Inmediato
condicional, 6 5 5 16
operación con 31-26 25-21 20-16 15-0
inmediato)
Ejemplo: addi $s1,$s2,-25
Operación Dato inmediato (constante)
Registro destino Registro fuente

Código Operando 1 Destino Inmediato


operación
8 18 17 -25

001000 10010 10001 1111111111100111

Notación compacta hexadecimal: 0x2251FFE7

Las instrucciones MIPS con campo para dato inmediato se codifican en binario según el formato
de instrucción tipo I, que tiene:
• Un campo de 6 bits para el código de operación.
• Un campo de 5 bits (rs) para el identificador del primer registro fuente.
• Un campo de 5 bits (rt) para el identificador del registro destino.
• Un campo de 16 bits para el dato inmediato.
Con este formato se codifican instrucciones como addi, addiu o slti, que asumen que la constante
está en complemento a 2, con lo que la circuitería realiza extensión de signo antes de operar, e
instrucciones como sltiu, andi, ori o xori, para las que la circuitería realiza extensión con ceros.
El formato I no lleva campo de función, y por tanto unas instrucciones se distinguen de otras por
los 6 bits del código de operación.

30
Acceso a datos residentes en memoria
Variables estáticas

• Variable: espacio en memoria para un dato de valor cambiante.


int V; V: .space 4 int X = 50; X: .word 50
Direcciones Memoria Direcciones Memoria
... ...
Dirección ↔ V ???? 4 octetos Dirección ↔ X 50 4 octetos
... ...

int vector[N]={val0,val1,…,valN-1} vector: .word val0,val1,...,valN-1


Direcciones Memoria
...
Dirección ↔ vector+4*(N-1) valN-1 4 octetos Otras directivas:
Dirección ↔ vector+4*(N-2) valN-2 4 octetos .byte
... ... ... .half
Dirección ↔ vector+8 val2 4 octetos
Dirección ↔ vector+4 val1 4 octetos
Dirección ↔ vector val0 4 octetos
...

Una variable es un espacio en memoria utilizado para almacenar un dato cuyo valor puede ser
modificado a lo largo de la ejecución de un programa. Hay tres tipos de variables: estáticas, locales
y dinámicas. Nos ocuparemos ahora de las variables estáticas, que existen mientras el programa
se encuentra en ejecución. En lenguajes como Pascal o C, las variables globales son estáticas.
Cuando en C definimos una variable estática, le damos un nombre, que luego utilizamos para
referenciarla. En realidad, el nombre de una variable es una dirección simbólica, es decir, es una
forma de referenciar la dirección de la variable mediante un nombre simbólico.
En ensamblador de MIPS las directivas para crear espacio para una variable estática son:
etiqueta: .space n # Reserva n bytes sin poner ningún valor inicial
etiqueta: .byte N # Reserva un byte y pone el valor inicial N
etiqueta: .half N # Reserva 16 bits y pone el valor inicial N
etiqueta: .word N # Reserva 32 bits y pone el valor inicial N
La etiqueta hace las veces del nombre de la variable, y es una representación simbólica de la
dirección de la misma, si bien en ensamblador la etiqueta es opcional.
Con .space podemos reservar tantos octetos de memoria como queramos, así que puede utilizarse
generar hueco para variables simples o para vectores u otras estructuras de datos, eso sí, sin
rellenarlos con ningún valor definido.
Las directivas .byte, .half y .word permiten crear espacio para variables simples con un valor
inicial dado. También se puede poner una secuencia de valores separados por comas. En tal caso,
se creará espacio para tantos datos como valores hayamos indicado, y la etiqueta queda asociada a
la dirección del primero. Los demás quedarán almacenados a continuación, uno tras otro.
Las variables locales se relacionan con la ejecución de subrutinas, y las variables dinámicas se
crean y destruyen en el heap mediante funciones de gestión de memoria dinámica.
31
Acceso a datos residentes en memoria
Direccionamiento indirecto a registro con desplazamiento

• Código en C y ensamblador MIPS para sumar dos variables:


.data
int A, B, C; .data
A: .space 4 # Copia en $s1
int main (void) { A: .space 4
B: .space 4 # Copia en $s2
… B: .space 4
C: .space 4 # Copia en $s3
A = B + C; C: .space 4
.text
… .text

} …
lw $s2,B
add A,B,C
lw $s3,C

add $s1,$s2,$s3
sw $s1,A

MIPS: máquina con arquitectura de carga/almacenamiento.
Direccionamiento indirecto a registro con desplazamiento:
Base addressing
op rs rt Address Memory

Register + Byte Halfword Word

• Notación: d16($n)

Volvamos al ejemplo de suma de dos variables. Como add exige que sus operandos residan en
registros, para trabajar con variables residentes en memoria tendremos que copiarlas previamente
en registros. En nuestro ejemplo, copiaríamos la variable B en un registro y la variable C en otro.
Además, como el resultado de add queda almacenado en un registro, si queremos que se grabe en
memoria tendremos que copiar el contenido del registro $s1 en la variable A.
La operación de copia del contenido de una variable de memoria en un registro de la UCP se
denomina carga (load), mientras que la operación que copia el contenido de un registro de la UCP
en una variable de memoria se denomina almacenamiento (store). Entonces, para trabajar con
variables residentes en memoria haremos lo siguiente:
1) Cargar las variables en registros.
2) Operar con los registros.
3) Almacenar el contenido del registro resultado en memoria.
Por ello, se dice que MIPS es una máquina con arquitectura de carga/almacenamiento. La
operación de carga de un dato de tamaño palabra es lw (load word), mientras que la operación
de almacenamiento de un dato de tamaño palabra es sw (store word).
Pero si lw o sw tuviesen un campo para la dirección absoluta de las variables, no cabrían en una
palabra: su tamaño sería de 6 (código de operación) + 5 (identificador del registro) + 32 (dirección
de la variable) = 43 bits. Sin embargo, el código ensamblador de la parte superior derecha de la
diapositiva, que usa etiquetas para referenciar las variables, es correcto. Esto es porque el
traductor de ensamblador realiza una transformación en el código, de modo que la dirección
efectiva del operando residente en memoria no se proporciona directamente en la instrucción, sino
que se calcula como la suma del contenido de un registro (registro base) más un
desplazamiento constante.
(CONTINÚA EN LA PÁGINA SIGUIENTE)
32
Acceso a datos residentes en memoria
Carga y almacenamiento de variables en MIPS

lw registro_destino,desplazamiento16(registro_base)
Ejemplo: lw $s0,12($at)

24311 0x1001000C $s0 24311

0x10010008

0x10010004

$at 0x10010000 0x10010000

0x1000FFFC

$s1 2325 2325 0x1000FFF8

Memoria

sw registro_origen,desplazamiento16(registro_base)
Ejemplo: sw $s1,-8($at)

(VIENE DE LA PÁGINA ANTERIOR)


Hemos dicho que, para obtener la dirección de las variables residentes en memoria, las
instrucciones de lw y sw calculan su dirección efectiva sumando el contenido de un registro base
más un desplazamiento de 16 bits (que se extenderá en signo antes de sumar), o lo que es lo
mismo, mediante direccionamiento indirecto a registro con desplazamiento. Para utilizarlo
en ensamblador, escribiremos el desplazamiento constante (positivo o negativo) y después el
nombre del registro base encerrado entre paréntesis: offset($reg_base).
Ahora podemos ver que el tamaño de las instrucciones lw y sw es de 6 (código de operación) + 5
(identificador del registro de carga o almacenamiento) + 5 (identificador del registro base) + 16
(desplazamiento) = 32 bits, así que ya caben en una palabra de memoria.
Esta diapositiva muestra sendos ejemplos de una operación de carga y una de almacenamiento.
En la carga, el dato origen reside en memoria, y su dirección efectiva se calcula mediante la suma
del contenido del registro $at (que actúa como puntero) más un desplazamiento de 12, es decir,
de 3 palabras más arriba de donde apuntaba el registro base. Así, se lee el contenido de dicho dato
en memoria, y se copia en el registro $s0, que es el destino.
En el almacenamiento, el dato origen reside en el registro $s1, y se pretende grabar en memoria
en una posición cuya dirección efectiva se calcula mediante la suma del contenido del registro $at
(que actúa como puntero) más un desplazamiento de -8, o sea, de 2 palabras más abajo del lugar
donde señala el puntero. Así, en dicha ubicación de memoria se copia el contenido de $s1.
Las instrucciones lw y sw realizan transferencias de tamaño palabra. MIPS permite realizar
transferencias de 8 bits (lb, lbu, sb) y de 16 bits (lh, lhu, sh). Como las operaciones de carga
tienen siempre como destinatario un registro de 32 bits, lb y lh hacen extensión de signo del dato
copiado, mientras que lbu y lhu rellenan la parte superior del registro con ceros.
33
Acceso a datos residentes en memoria
Pseudodireccionamiento directo

lw $s2,B lui $1,0x1001


lw $s2,4($1)

0x10010008 ↔ C ???
$1 0x10010000
0x10010004 ↔ B XXXX
0x00000004
0x10010000 ↔ A ???

Memoria 0x10010004
$s2 XXXX

lui registro_destino,constante16 0x1234

Ejemplo: lui $s1,0x1234

$s1 0x12340000

Referenciar las variables mediante direccionamiento indirecto a registro con desplazamiento resulta
muy incómodo. Por ello, el traductor de ensamblador proporciona el pseudodireccionamiento
directo o absoluto no soportado por la circuitería (dando la dirección numérica o simbólica
de la variable), y es el propio traductor quien se encarga de incluir el código que carga un cierto
registro con una dirección base, para después calcular el desplazamiento idóneo.
A las personas nos puede resultar incómodo manejar direcciones de memoria, pero debemos
recordar que una de las tareas del traductor es crear una tabla de símbolos con todas las etiquetas
definidas y sus valores. Así, para el traductor es tarea sencilla dar la dirección base y el
desplazamiento necesarios para acceder a cada variable. Supondremos que la zona de variables
estáticas comienza en la dirección 0x10010000. Así, en el momento de realizar la traducción de lw
$s2,B, el traductor ya conoce la dirección de B (0x10010004) y, por tanto, puede calcular la
dirección base que cargará en el puntero y el valor del desplazamiento sin ninguna dificultad. Lo
más sencillo es hacer que la dirección base sea 0x10010000, y así el desplazamiento sería igual a 4.
Así, cuando escribimos lw $s2,B, en realidad el código que genera el traductor de ensamblador es:
lui $1,0x1001 # Carga $1 con el valor 0x10010000
lw $s2,4($1) # Lee el dato de memoria y lo carga en $s2
La instrucción lui tiene como operando fuente una constante de 16 bits, y como destino un
registro. La función de lui es cargar la constante en el registro, pero lo hace de un modo un tanto
especial: la constante es copiada en la mitad superior del registro, quedando la mitad inferior del
mismo con todos sus bits a 0 (lui: load upper immediate).
El registro escogido por el traductor como puntero base es $1, cuyo alias es $at. Este registro es
utilizado por el traductor para realizar la traducción de pseudoinstrucciones, con lo cual no debería
ser utilizado por los programadores en ningún caso.

34
Formato de instrucción tipo I: lw
Registro Registro
Cód. Op. Desplazamiento
Tipo I (carga o base destino
almacenamiento, xxxxxx rs rt Inmediato
ramificación 6 5 5 16
condicional) 31-26 25-21 20-16 15-0

Ejemplo: lw $s2, 4($at)


Operación Registro base
Registro destino Desplazamiento

Código operación Base Destino Desplazamiento

35 1 18 4

100011 00001 10010 0000000000000100

Notación compacta hexadecimal: 0x8C320004

Las instrucciones MIPS de carga y almacenamiento se codifican en binario con el formato de


instrucción tipo I, ya estudiado antes. En este caso, el campo de 16 bits da el desplazamiento
aplicado al registro base para calcular la dirección efectiva del dato en memoria, y que se extiende
en signo a la hora de realizar dicho cálculo.
La instrucción lui estudiada antes también se codifica con este formato, aunque tiene el campo rs
a 0 puesto que no cuenta con ningún operando fuente en registro.

35
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

El sistema operativo ofrece una serie de servicios que permite a los programas de usuario utilizar
los periféricos (teclado, ratón, discos, etc) de una forma ordenada, ocultando buena parte de sus
peculiaridades. De este modo, los programadores no necesitan conocer las características de las
controladoras de los periféricos ni la forma de acceder a las mismas para realizar transferencias de
información.
Hay otras operaciones propias del sistema operativo, como la petición de bloques de memoria
dinámica, la creación y terminación de procesos, etc., que no tienen que ver con el acceso a
periféricos, y que también se ofrecen a los programas de usuario como servicios del sistema.
Los servicios del sistema están numerados, y pueden ser invocados desde los programas de
usuario a través de una instrucción de llamada a servicio del sistema. Esta instrucción en MIPS
es syscall. Para invocar un servicio, basta con dar el número del mismo, y colocar los parámetros
del servicio en lugares determinados donde la rutina de servicio espera encontrarlos, para después
ejecutar la instrucción de llamada. En MIPS el programa de usuario copia los parámetros en los
registros $a0, $a1, etc., y el número de servicio en el registro $v0 antes de invocar a syscall.
Cuando la rutina de servicio completa la operación requerida, devuelve un valor de retorno en un
lugar donde el programa de usuario espera encontrarlo, y después retorna al programa de usuario,
que puede continuar ejecutándose normalmente. En MIPS las rutinas de servicio suelen copiar el
valor de retorno en $v0, aunque a veces lo copian en $f0 (si el valor retornado es un dato en
coma flotante) o en los registros de argumento $a0, $a1, etc.
MARS no emula ningún sistema operativo, sino que emula un sistema con un pequeño monitor
ROM. Los servicios disponibles con syscall pueden consultarse en la tabla ofrecida en Help 
MIPS  Syscalls. Se incluyen servicios de petición de datos de entrada por teclado, escritura de
datos por pantalla, acceso a ficheros, petición de memoria dinámica, terminación de programa, etc.

36
Llamadas a sistema

• Sirven para realizar operaciones de E/S, terminar procesos, etc.


#include <stdio.h> # #include <stdio.h> # scanf(“%d”,&C);
int A, B, C; .data li $v0,5
int main (void) { # int A,B,C; syscall
A: .space 4 # Copia en $s1 sw $v0,C
printf(“Valor de B: “);
B: .space 4 # Copia en $s2 # A = B + C;
scanf(“%d”,&B); C: .space 4 # Copia en $s3 lw $s2,B
printf(“Valor de C: “); # Tiras auxiliares para E/S por terminal lw $s3,C
scanf(“%d”,&C); tiraB: .asciiz "Valor de B = " add $s1,$s2,$s3
A = B + C; tiraC: .asciiz "Valor de C = " sw $s1,A
printf(“B + C = %d\n“,A); tiraA: .asciiz "B + C = " # printf(“B + C = %d\n“,A);
tiraeoln: .asciiz "\n" li $v0,4
return 0;
.text la $a0,tiraA
} # int main(void); syscall
main: li $v0,1
# printf(“Valor de B: “); lw $a0,A
li $v0,4 syscall
la $a0,tiraB li $v0,4
syscall la $a0,tiraeoln
# scanf(“%d”,&B); syscall
li $v0,5 # return 0;
syscall #}
sw $v0,B li $v0,17
# printf(“Valor de C: “); li $a0,0
li $v0,4 syscall
la $a0,tiraC
syscall

Retomando nuestro ejemplo de suma de dos enteros, sería una buena idea pedir los datos B y C
por teclado, y presentar el resultado en pantalla. Para ello se recurre a los siguientes servicios:
• Servicio 4: escritura de una tira de caracteres en pantalla.
• Servicio 5: lectura de un dato entero por teclado.
• Servicio 17: terminación ordenada del programa devolviendo un código de retorno.
En todos los casos se copian previamente el número de servicio y los parámetros en los registros
pertinentes, y después se recoge el valor de retorno, cuando existe.
Para conocer más detalles sobre los servicios indicados, consultar la ayuda de MARS.
Como novedad, para escribir el número de servicio en un registro se está empleando la
pseudoinstrucción li, que tiene dos operandos:
• El primero es el registro destino.
• El segundo es la constante que vamos a copiar en el registro destino indicado como
primer operando.
Por simplicidad, en este ejemplo se ha invocado directamente los servicios de E/S. Sin embargo,
printf y scanf son funciones de la biblioteca de E/S estándar de C, y el programa debería haber
invocado a dichas funciones. Serían printf y scanf quienes deberían contener las llamadas a
servicio, pero ambas funciones son bastante más complejas, y su implementación en ensamblador
queda fuera de los objetivos de la asignatura.

37
Índice

1. Traducción de programas.
2. Repertorios de instrucciones.
3. Características principales de MIPS.
4. Estructura de un programa en ensamblador en MIPS.
5. Modos de acceso a los datos en MIPS.
6. Llamadas a sistema en MIPS.
7. Operaciones de ramificación y salto en MIPS.

Los programas están formados en principio por instrucciones que se ejecutan una detrás de otra en
orden. La secuenciación de instrucciones se realiza automáticamente, ya que el registro contador
de programa (PC: program counter) siempre contiene la dirección de la próxima instrucción que se
va a leer, de modo que, cuando cada instrucción se encuentra en la fase de lectura (fetch), al
mismo tiempo el PC se incrementa para pasar a apuntar a la siguiente.
Sin embargo, este mecanismo impide realizar estructuras de selección (if-else, switch) o
iterativas (while, do, for) o implementar subprogramas, por ejemplo. Por ello, es preciso
complementar la secuenciación automática de instrucciones con algún otro mecanismo que permita
implementar tales estructuras.
De aquí surge un grupo de instrucciones de máquina que permiten cambiar a voluntad el valor del
PC y así romper la secuencia de ejecución de tres modos diferentes:
• De forma incondicional.
• Dependiendo de una condición.
• Guardando la dirección de retorno (con enlace).
En el presente apartado analizaremos estas instrucciones.

38
Operaciones de ramificación y salto

• Código en C, y posible equivalencia en ensamblador MIPS:


Ramificación .data
int A, B, C; condicional A: .space 4 # Copia en $s1
B: .space 4 # Copia en $s2
int main (void) {
C: .space 4 # Copia en $s3
… CIERTO
¿B = C? .text
if (B = C)

A = A + B;
FALSO if: lw $s2,B
else A = B - C;
lw $s3,C

beq $s2,$s3,then
}
A=B-C else: sub $s1,$s2,$s3
sw $s1,A
j endif
then: lw $s1,A
add $s1,$s1,$s2
Ramificación
sw $s1,A
incondicional A=A+B
endif:
o salto

• Branch = ramificar
• Jump = saltar

Partimos de un código C que presenta una estructura de selección if-else. En el centro de la


diapositiva se muestra el diagrama de flujo de este código. En el diagrama, los rectángulos
representan bloques de operaciones que se realizan en secuencia. Las flechas representan el flujo
de ejecución. Las flechas que se saltan bloques corresponden con instrucciones de ruptura de
secuencia, y ésta ruptura puede ser incondicional (cuando procede de un rectángulo) o
condicional (cuando la flecha procede de un rombo). Precisamente, el rombo representa una
toma de decisiones, y por tanto irá seguido de al menos dos flechas, una que representa el flujo
secuencial de ejecución y otra que representa la ruptura condicional de secuencia.
Las instrucciones ensamblador que dan soporte a esta estructura condicional normalmente rompen
la secuencia cuando la condición es cierta, y no la rompen cuando la condición es falsa. Por ello,
inmediatamente después de la toma de la decisión se encuentra el bloque else, mientras que el
bloque then está más abajo. De este modo, si la condición resultase cierta, entonces romperíamos
la secuencia e iríamos al bloque then, y al terminar éste continuaríamos con la ejecución normal
de instrucciones. Si la condición resultase falsa, continuaríamos la ejecución secuencial de las
instrucciones, entrando directamente en el bloque else.
Se puede observar que, si no rompemos la secuencia de ejecución al final del bloque else,
entonces entraríamos en el bloque then, que se encuentra justo debajo, y que obviamente
deberíamos evitar. Para ello realizaremos una ruptura incondicional de secuencia que nos
permitirá ir al final de la estructura if-else, para así poder continuar con la ejecución normal de
instrucciones.

39
Operaciones de ramificación
Direccionamiento relativo a PC con desplazamiento

• Campos: desplazamiento de 16 bits.


– Positivo o negativo.
– Se suma al PC.
– Cuenta instrucciones que hay que saltarse (tener el cuenta que el
PC ya apunta a la instrucción siguiente).
PC-relative addressing
op rs rt Address Memory

PC + Word

• Antes de sumar:
– Concatenar dos ceros por el final (multiplicar por 4).
– Extender en signo a 32 bits.

• Notación: oculto en etiqueta.

• Operaciones: beq, bne, bge, bgt, ble, blt, bgeu, bgtu,


bleu, bltu, etc.

En la diapositiva anterior la instrucción beq $s2,$s3,then rompe la secuencia de ejecución cuando


el contenido de los registros coincide, en cuyo caso bifurca a una instrucción cuya dirección viene
dada por la etiqueta then. Para codificarla necesitaríamos 6 (código de operación) + 5
(identificador del primer registro) + 5 (identificador del segundo registro) + 32 bits (dirección de
instrucción destino) = 48 bits, lo cual supera el tamaño límite de una palabra por instrucción.
En realidad, la circuitería calcula la dirección de la instrucción destino como la suma del contenido
del PC más un desplazamiento constante, que codifica la distancia entre el valor actual del PC y la
instrucción destino del salto. El desplazamiento tiene 16 bits, y podrá ser positivo o negativo, en
función de que la instrucción destino se encuentre más adelante o más atrás en el programa. El
ancho del desplazamiento limita la distancia máxima del salto, y por ello a la operación se le
denomina ramificación (branch, de ahí que el nemotécnico de la instrucción comience por b).
Como calcular el desplazamiento resulta incómodo, es el traductor de ensamblador quien lo hace,
restando la dirección de la instrucción destino menos el PC actual. Para ampliar la distancia del
salto, el desplazamiento indica cuántas instrucciones (cada una de las cuales ocupa 4 octetos)
debemos saltar hacia delante o hacia atrás, y por ello la circuitería añade dos ceros al
desplazamiento por el final (lo que equivale a multiplicarlo por 4) antes de sumárselo al PC.
Es preciso recordar que el incremento del PC para apuntar a la dirección de la instrucción siguiente
se produce durante la fase de lectura de la instrucción actual en memoria (fetch). Entonces, al
entrar en la fase de decodificación, el PC ya no apunta a la instrucción de ramificación, sino a la
siguiente. El traductor tendrá esto en cuenta al calcular el valor del desplazamiento.
A este direccionamiento se le denomina direccionamiento relativo a PC con desplazamiento.

40
Formato de instrucción tipo I: beq
Registro Registro
Cód. Op. Desplazamiento
Tipo I (carga o base destino
almacenamiento, xxxxxx rs rt Inmediato
ramificación 6 5 5 16
condicional) 31-26 25-21 20-16 15-0

Ejemplo: beq $s2, $s3, then

Operación Etiqueta
Primer registro Segundo registro

Código operación Primer registro Segundo registro Desplazamiento

4 18 19 3

000100 10010 10011 0000000000000011

Notación compacta hexadecimal: 0x12530003

Las instrucciones de ramificación se codifican con el formato de instrucción tipo I, ya estudiado


antes. Las más utilizadas son beq (ramifica si los dos operandos son iguales; eq = equal) y bne
(ramifica si los dos operandos son distintos; ne = not equal). También es habitual el nemotécnico
b, que realiza una ramificación incondicional y sólo lleva un operando, que es la etiqueta; en
realidad, b es una pseudoinstrucción. Las pseudoinstrucciones beqz y bnez comparan un registro
fuente con 0, siendo su segundo operando la etiqueta que marca el destino de la ramificación.
Para otros tipos de comparaciones es preciso tener en cuenta si los operandos llevan signo o no.
Así, nos encontraremos con las siguientes pseudoinstrucciones, que comparan operandos con signo
(suponiendo operandos en complemento a 2): bgt (ramifica si op1 > op2; gt = greater than),
blt (ramifica si op1 < op2; lt = less than), bge (ramifica si op1 ≥ op2; ge = greater or
equal), ble (ramifica si op1 ≤ op2; ge = greater or equal).
Las siguientes instrucciones comparan el primer operando con 0 suponiendo que tiene signo: bgtz
(ramifica si op1 > 0), bltz (ramifica si op1 < 0), bgez (ramifica si op1 ≥ 0), blez (ramifica si
op1 ≤ 0). Aquí la etiqueta aparece como segundo operando.
Las siguientes pseudoinstrucciones de ramificación toman operandos sin signo (binario puro):
bgtu (ramifica si op1 > op2; gtu = greater than unsigned), bltu (ramifica si op1 < op2; ltu
= less than unsigned), bgeu (ramifica si op1 ≥ op2; geu = greater or equal unsigned),
bleu (ramifica si op1 ≤ op2; geu = greater or equal unsigned).
Las instrucciones de ramificación con enlace sirven para saltar a subrutinas. Estas instrucciones
comparan su primer operando (en complemento a 2) con 0, y si se cumple la condición, ramifican y
además guardan la dirección del PC actual (que apunta a la instrucción siguiente a la de
ramificación) en el registro $ra para poder volver: bltzal (ramifica si op1 < 0), bgezal (ramifica si
op1 ≥ 0 y enlaza en $ra). En estas instrucciones la etiqueta es el segundo operando.
41
Operaciones de salto
Direccionamiento pseudodirecto

• Campos: dirección de 26 bits.

• Para completar la dirección a 32 bits la circuitería:


– Pone dos ceros a la derecha.
– Copia delante los cuatro primeros bits del PC.

• Se utiliza en instrucciones de salto.


Pseudodirect addressing
op Address Memory

PC | Word

• Notación: dirección numérica, u oculto en etiqueta.

• Instrucciones: j, jal.

En la diapositiva anterior hay una instrucción de ruptura incondicional de secuencia (j endif). Para
codificarla necesitaríamos 6 (código de operación) + 32 bits (dirección de instrucción destino) = 38
bits, lo cual supera el tamaño límite de una palabra por instrucción.
En realidad, el campo de la dirección destino tiene 26 bits, que ocupan el hueco disponible en el
formato. Como las direcciones de las instrucciones tienen 32 bits, la circuitería completa el campo
con otros 6 bits del siguiente modo:
• Concatena dos ceros por el final, lo que asegura que la dirección resultante es múltiplo de
4 y está alineada a palabra.
• Concatena los cuatro bits más significativos del PC por delante. Esto limita el alcance del
salto a una cuarta parte del mapa total de direcciones, pero a los programas de usuario
no les afecta, ya que el rango de direcciones ocupado por el segmento de texto es
0x00400000-0x0FFFFFFF, y ahí los cuatro primeros bits valen siempre 0.
Es el traductor quien, conociendo la dirección destino, se encarga de recortarla y guardar en el
campo de dirección los 26 bits necesarios.
A este direccionamiento se le denomina direccionamiento pseudodirecto.

42
Formato de instrucción tipo J
Cód. Op. Dirección destino
Tipo J xxxxxx dirección
(salto 6 26
incondicional) 31-26 25-0
Ejemplo: j endif
Operación Destino

PC actual Destino del salto


0x00400024 0x00400038
0000 0000 0100 0000 0000 0000 0010 0100 0000 0000 0100 0000 0000 0000 0011 1000

Código operación Destino

2 0x00400038

000010 00 0001 0000 0000 0000 0000 1110

Notación compacta hexadecimal: 0x0810000E

Las instrucciones de salto incondicional con etiqueta nos conducen al formato de instrucción
tipo J, denominado así precisamente porque es propio de este tipo de instrucciones. Tiene un
campo de 6 bits para el código de operación, quedando los 26 restantes para la dirección. A la hora
de ejecutar la instrucción, la circuitería le añade dos ceros por el final, y antecede los cuatro bits
superiores del PC.
La instrucción jal (jump and link) se codifica con el mismo formato que j (jump), y realiza un
salto a una etiqueta con enlace en el registro $ra. jal es la instrucción típica para invocar
subrutinas: la etiqueta que aparece como operando es la dirección simbólica de la primera
instrucción ejecutable de la subrutina. La dirección de enlace o dirección de retorno, que es la
de la instrucción siguiente a jal, se guarda en el registro $ra.
Para poder retornar desde la subrutina a la instrucción siguiente al jal de llamada se utiliza la
instrucción jr (jump register). Sin embargo, jr se codifica con el formato tipo R, ya que no tienen
ninguna etiqueta como operando: jr tiene un único operando residente en un registro, que
precisamente indica la dirección de la instrucción destino del salto. Así, jr $ra realiza el retorno a la
instrucción siguiente a la de llamada al salir de la subrutina.
Existe otra instrucción de salto incondicional con enlace denominada jalr, que tiene dos operandos:
el primero es el registro que contiene la dirección de la instrucción destino, y el segundo es el
registro de enlace para poder realizar el retorno a la instrucción siguiente a jalr. La instrucción jalr
se codifica mediante el formato tipo R, igual que jr.

43

También podría gustarte