Tema04 GIC
Tema04 GIC
Luis Rincón
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).
[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.
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
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.
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
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
$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)
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.
12
Características principales de MIPS
Alineamiento y accesos a memoria
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.
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
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
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
} li $v0,1
lw $a0,suma
syscall
li $v0,17
li $a0,0
syscall
19
Estructura de un programa en ensamblador
Etiquetas y tabla de símbolos
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)
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
• 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
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
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
• Notación: d16
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
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
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
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
• 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)
0x10010008
0x10010004
0x1000FFFC
Memoria
sw registro_origen,desplazamiento16(registro_base)
Ejemplo: sw $s1,-8($at)
0x10010008 ↔ C ???
$1 0x10010000
0x10010004 ↔ B XXXX
0x00000004
0x10010000 ↔ A ???
Memoria 0x10010004
$s2 XXXX
$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
35 1 18 4
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
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
39
Operaciones de ramificación
Direccionamiento relativo a PC con desplazamiento
PC + Word
• Antes de sumar:
– Concatenar dos ceros por el final (multiplicar por 4).
– Extender en signo a 32 bits.
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
Operación Etiqueta
Primer registro Segundo registro
4 18 19 3
PC | Word
• 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
2 0x00400038
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