UNIVERSIDAD TECNICA FEDERICO SANTA MARIA DEPARTAMENTO DE ELECTRONICA ELO311 Estructuras de Computadores



Documentos relacionados
Funciones. Diseño de funciones. Uso de instrucción jal y retorno de subrutina.

Introduccion al Lenguaje C. Omar Andrés Zapata Mesa Grupo de Fenomenología de Interacciones Fundamentales, (Gfif) Universidad de Antioquia

Tema 2: Arquitectura del repertorio de instrucciones. Visión del computador que tiene el programador en bajo nivel.

Estructura de Computadores

Modulo 1 El lenguaje Java

Centro de Capacitación en Informática

Guía Laboratorio Número 1

Ilustrar el mecanismo de llamadas al sistema para solicitar servicios al sistema operativo.

SISTEMA InfoSGA Manual de Actualización Mensajeros Radio Worldwide C.A Código Postal 1060

Instrucción IrA (GoTo). Saltos no naturales en el flujo normal de un programa. Pseudocódigo y diagramas de flujo. (CU00182A)

Capitulo V Administración de memoria

PROPUESTAS COMERCIALES

Manual de usuario para Android de la aplicación PORTAFIRMAS MÓVIL

Tema 6. Reutilización de código. Programación Programación - Tema 6: Reutilización de código

En cualquier caso, tampoco es demasiado importante el significado de la "B", si es que lo tiene, lo interesante realmente es el algoritmo.

MATERIAL 2 EXCEL 2007

GUÍA RÁPIDA DE TRABAJOS CON ARCHIVOS.

Organización Básica de un Computador y Lenguaje de Máquina

Práctica 0. Emulador XENON de la computadora CESIUS

Para crear formularios se utiliza la barra de herramientas Formulario, que se activa a través del comando Ver barra de herramientas.

Este programa mueve cada motor de forma independiente, y cuando termina una línea pasa a la siguiente.

Pipelining o Segmentación de Instrucciones

APUNTES DE WINDOWS. Windows y sus Elementos INSTITUTO DE CAPACITACIÓN PROFESIONAL. Elementos de Windows

LABORATORIO Nº 2 GUÍA PARA REALIZAR FORMULAS EN EXCEL

Fundamentos de la Programación

Fundamentos de Investigación de Operaciones Investigación de Operaciones 1

Matemática de redes Representación binaria de datos Bits y bytes

Los elementos que usualmente componen la identidad digital son:

Práctica 3: Programación con subrutinas

1 La Resolución de Problemas utilizando la Computadora

Para ingresar a la aplicación Microsoft PowerPoint 97, los pasos que se deben seguir pueden ser los siguientes:

Tutorial de UML. Introducción: Objetivos: Audiencia: Contenidos:

Preliminares. Tipos de variables y Expresiones

PRACTICA #1. Aprender a programar una interrupción software empleando C y/o Ensamblador.

Unidad III El lenguaje de programación C

Tema 5 Repertorios de instrucciones: Modos de direccionamiento y formato

SOLUCION EXAMEN junio 2006

Introducción. Ciclo de vida de los Sistemas de Información. Diseño Conceptual

La memoria principal. Los subsistemas de E/S. Los buses del sistema

TEMA 5. CONTROL DE FLUJO DEL PROGRAMA. Sentencia Instrucción Expresión Operadores + Operandos Sintaxis: Sentencia ;

Organización de Computadoras

Documentación del simulador SPIM.

PROGRAMACIÓN ORIENTADA A OBJETOS

Arquitectura de Computadores

LEER Y ESCRIBIR ARCHIVOS O FICHEROS EN C. FOPEN, FCLOSE, MODOS DE ACCESO READ, WRITE Y APPEND (CU00536F)

INVENTARIO INTRODUCCIÓN RESUMEN DE PASOS

Módulo II - PowerPoint

Memoria La memoria es la parte del ordenador en la que se guardan o almacenan los programas (las instrucciones y los datos).

Tecnólogo Informático- Estructuras de Datos y Algoritmos- 2009

Sea el siguiente programa de nombre "c0p1" para copiar archivos (por simplicidad se ha eliminado todo control de errores): Se pide:

Es un software de simulación que ejecuta programas en lenguaje de ensamblador para procesadores con arquitectura MIPS32.

Capítulo 1 Introducción a la Computación

Guía Corta: Alcance y Asociaciones. 1. Preliminares: Nombres y Asociaciones

Capítulo 4 Procesos con estructuras de repetición

Ejercicios. 5.2 [5] < 5.3> Este ejercicio es similar al 5.1, pero ahora considere los fallos causados por permanecer en 1 (la señal es siempre 1).

Unidad I. 1.1 Sistemas numéricos (Binario, Octal, Decimal, Hexadecimal)

Un puntero no es más que una variable estática cuyo contenido es una dirección de memoria.

3.2 Operaciones aritmético-lógicas en Pascal

INSTITUTO TECNOLOGICO de la laguna Programación Orientada a Objetos en C++

8. Sentencia return y métodos

Generación de código para funciones. Generación de código para funciones. Generación de código para funciones. Generación de código para funciones

USO DEL COMANDO. Fdisk. Autor :. Alejandro Curquejo. Recopilación :. Agustí Guiu i Ribera. Versión :.. 1.0

Microsoft Office XP Excel XP (I)

Manual para Empresas Prácticas Curriculares

6.1. Conoce la papelera

SISTEMA DE BECAS AL EXTERIOR

Pontificia Universidad Católica de Chile Escuela de Ingeniería Departamento de Ciencia de la Computación

Comente: Los bancos siempre deberían dar crédito a los proyectos rentables. Falso, hay que evaluar la capacidad de pago.

Ejercicio 1. Desarrollar un pequeño juego para practicar mecanografía.

Arreglos. // Incluir E/S y Librerías Standard #include <stdlib.h> #include <stdio.h>

Instructivo de Microsoft Excel 2003

CAPÍTULO I. Sistemas de Control Distribuido (SCD).

A continuación, se detalla el uso de los accesorios más comunes:

7. Manejo de Archivos en C.

UNIDADES DE ALMACENAMIENTO DE DATOS

Ingeniería de Software I

Guía para la migración de asignaturas de grado y másteres al nuevo espacio docente para el curso 2015/2016

Soporte lógico de computadoras

Tema 2. Diseño del repertorio de instrucciones

Programa Presupuestos de Sevillana de Informática.

GUÍA PARA MANEJAR GOOGLE DRIVE

SISTEMA ETAP en línea Estándares Tecnológicos para la Administración Pública

La ventana de Microsoft Excel

TEMA 3. EL PROCESO DE COMPILACIÓN, DEL CÓDIGO FUENTE AL CÓDIGO MÁQUINA

Guía para El Proveedor **********

MANEJANDO FICHEROS Y CARPETAS

CASO PRÁCTICO DISTRIBUCIÓN DE COSTES

Tema 8 Procesos. * Definición informal: un proceso es un programa en ejecución

MACROS Y FORMULARIOS

UTILIDAD DE EXPORTACIÓN DE ASIENTOS A CONTAPLUS

Operación de Microsoft Word

HP Backup and Recovery Manager

Capítulo 4 Gestión de memoria

HERRAMIENTAS DE ACCESS ACCESS Manual de Referencia para usuarios. Salomón Ccance CCANCE WEBSITE

ISTP CIDET COMPUTACION E INFORMATICA ARREGLOS EN JAVA

Para ingresar a la aplicación Microsoft Word 97, los pasos que se deben seguir pueden ser los siguientes:

Sesión 3 - Movimiento Diferencial

CÓMO CREAR NUESTRO CATÁLOGO

Programación Orientada a Objetos con Java

Transcripción:

6.5. Comunicación de las funciones con su entorno. Hasta el momento se ha conceptualizado una subrutina como un segmento de código que ocurre a menudo en un programa. La idea puede flexibilizarse y aumentar el poder de las subrutinas, pasándole parámetros y retornando valores; de esta manera, el código de una subrutina modela al concepto de función. Las subrutinas permiten darle estructura a las acciones, facilitando la comprensión del algoritmo. Es decir, permiten descomponer un todo complejo, en partes más simples que tienen un nombre abstracto que recuerda la subacción que realizan. Un motivo frecuente de errores es que las subrutinas tengan efectos laterales, es decir que modifiquen otras variables, generalmente por alcance de nombres. Pare evitar esto se adoptó la política de pasarle los datos de entrada como argumentos y que la función retorne el resultado, ojalá en registros. La mayor ventaja de esto es que el código interno de la función puede cambiarse y no afectará al resto del programa. Además de permitir el reusar las funciones en otros programas. La metodología anterior también generó dificultades, ya que si se cambian las estructuras de los datos, deben rescribirse, o modificarse las funciones que los manipulan. Actualmente los datos y las funciones que los procesan están encapsulados en un objeto, ésta es la idea básica en la programación orientada a objetos, que fue una refinación del concepto de módulos, que ya se empleaba en programación. Una función, entonces, efectúa el cálculo de un valor en términos de sus argumentos; por ejemplo, una función y, con argumentos x1, x2 y x3 puede anotarse: y = f(x1, x2, x3). En el lenguaje C, sólo se tienen funciones a las que se les pasa argumentos por valor. Es decir, los argumentos de la función son números (técnicamente son expresiones). Y también la función sólo retorna un valor; de este modo una función es una abstracción de una expresión. Esta aparente limitación, es debida a que el lenguaje fue desarrollado para ser fácilmente compilado en lenguajes de máquina. Como veremos este modelo, tiene una implementación simple en assembler. Se estudiarán tres casos típicos; el primero, una función sin argumentos; el segundo, paso de argumentos por valor; y el tercero, paso de argumentos por referencia. Prof. Leopoldo Silva Bijit. 15-07-2004 106

6.5.1. Comunicación por variables globales. En este caso, la función no requiere argumentos y tampoco un valor de retorno. Entonces puede leerse valores, dentro de la función, desde las variables ya existentes; y escribir los resultados en una o varias de las variables externas. Éstas pueden estar en registros o en la zona estática de datos. Esta forma puede provocar, lo que se denomina efectos laterales (por escribir en una variable que empleará otra función más adelante, o por leer una variable que otra función modificó con otro sentido), y no es recomendada. Sin embargo su utilidad es reducir el código de entrada y salida de una subrutina. Descripción en C. int i, j, k; /*definición de variables globales*/ /*Definición de la función */ void f1(void) /*argumentos y resultados en globales*/ { k = i + j; } main( ) {... f1( ); /*aquí se invoca a la subrutina o función f1 sin argumentos */... /*aquí debería retornar a ejecutar la siguiente sentencia */ } La primera ocurrencia de la palabra void(vacío en español) indica que la función f1 no retorna un valor; cuando void aparece en lugar de los argumentos, indica que a la función no se le pasan parámetros. Representación en assembler:.data i:.word 0x0 #definición de variables globales (estáticas) j:.word 0x0 k:.word 0x0 main:.text.globl main #se especifica tipo de rótulo... jal f1 # invocación de la función (por su dirección).... # se guarda en ra, la dirección de la instrucción después del jal. j main # se vuelve a repetir la función principal. Observación. La decisión del salto a main es para simplificar el empleo del simulador. El texto en C indica que en este lugar debería retornarse de la función main, y volver al sistema operativo, desde donde se ejecutó el programa. De esta manera se impide la ejecución de instrucciones después de la última ensamblada; ya que siempre habrá algo en la memoria, después de la última instrucción cargada. Esto vuelve a repetir el programa. Prof. Leopoldo Silva Bijit. 15-07-2004 107

La codificación que traduce el programa en C, es ejecutar un llamado al sistema que retorne al programa que invocó a main (syscall #10). Esto viene incorporado en el trap handler. Si lo que se desea es detener la ejecución, a pesar de seguir ejecutando paso a paso, una alternativa es colocar: stop: j stop # esto cuelga al procesador. (hang-up) Definición de la función. Veamos ahora el código de la función en assembler. #definición de la función f1: la $t0, i # t0 apunta a variable i (se usa una macro). t0 = &i lw $s1, 0($t0) # s1 = i lee desde la memoria las variables lw $s2, 4($t0) # s2 = j add $t1, $s1, $s2 # t1 = i + j sw $t1, 8($t0) # k = t1 escribe en variable en memoria jr $ra # retorna a la instrucción siguiente al jal. Estrategias para el uso de registros. En máquinas con registros numerosos de propósito general se intenta mantener en registros las variables de uso más frecuente, y se colocan en memoria el resto de las variables (si son más numerosas que los registros disponibles). Entonces se emplea load para mover esas variables a registros, y luego de manipularlas, mediante store almacenarlas en la memoria. Esto implica mayor tiempo de proceso, por esto es conveniente tener una estrategia para ocupar de manera eficiente los registros. Por otro lado, si están ocupados los registros, es preciso pasar alguna variable a memoria, para disponer de éstos; se denomina derramar registros (spilling registers) a este proceso. Y éste es uno de los usos del stack. En MIPS, se pasan los argumentos en los registros $a0, $a1, $a2, $a3, y las funciones pueden retornar resultados en los registros $v0 y $v1, es decir, son funciones expandidas que pueden retornar dos valores. Si se requieren más argumentos debe empujárselos (derramarlos) al stack antes de la invocación. Es importante entender que los valores almacenados en registros, por el texto que llama a un procedimiento (llamador), deben contener lo mismo después del retorno del procedimiento (llamado) si se vuelven a emplear en el texto que invocó a la subrutina. Una forma primitiva para evitar corromper el estado de los registros es: llamador: salvar en stack todos los registros(derrame total) Prof. Leopoldo Silva Bijit. 15-07-2004 108

invocar restaurar todos los registros. Pero esto es muy ineficiente, ya que habrán registros que pueden no emplearse dentro de la función. Una alternativa, en algunos procesadores es emplear varios bancos de registros (solución que emplea, por ejemplo el 8051, y que consiste en disponer de diversos grupos de todos los registros de propósito general), mediante una instrucción se cambia de banco, y el texto llamado dispone de todos los registros. Al retornar, se vuelve a activar el banco original. La siguiente descripción conceptual destaca este hecho: llamador: se usan registros con argumentos se escribe en ciertos registros deben salvarse registros que se escriban en el texto del llamado deben salvarse argumentos que se ocupen después del retorno del llamado y que se escriban en la siguiente fase. se prepara invocación cargando argumentos se invoca se almacena valores de retorno se recuperan argumentos que vuelvan a ser leídos en fases siguientes se restauran contenidos de registros que sean leídos luego en el llamador pueden volver a ocuparse los argumentos originales pueden volver a ocuparse registros en los que se hubieran depositado valores. Para organizar el manejo de los registros, en MIPS, se tienen dos grupos de registros, los t, por temporales ($t0 hasta $t9), y los s por salvados ($s0 hasta $s7). Y se emplea el siguiente convenio: El llamado puede emplear los temporales; es decir escribir en ellos. Y no es preciso que los salve y restaure. El que llama debe salvar, al inicio, los registros s en que escriba. Y antes de salir debe restaurar dichos registros. El que llama debe salvar, antes de invocar, los registros t que tengan valores que ocupe después del llamado, y que se hayan modificado en la función llamada. Luego del retorno debe restaurar dichos registros. 6.5.2. Comunicación por argumentos. Paso por valor. En este caso se coloca como ejemplo una función que retorna un valor, y a la cual se le pasan argumentos en forma de números (por valor). Prof. Leopoldo Silva Bijit. 15-07-2004 109

Descripción en C. int i, j, k; /* globales */ register int f2(register int a0, register int a1) /* a0 y a1 son parámetros formales */ { return (a0+a1); } main() {... k = f2(i, j); /* invocación a f2 con parámetros actuales i y j. Pueden ser expresiones*/... } A la función f2 se le pasan como argumentos, los valores de las variables globales i y j. La palabra register le indica al compilador que los argumentos deben pasarse vía registros. El resultado de la función, que debe devolverse en un registro, se almacena en k. Nótese que una función es una expresión, con valor perteneciente al tipo de la función. En invocaciones por valor, puede pasarse una expresión, por ejemplo f2(i+5, j-2). Codificación en assembler..data i:.word 0x0 j:.word 0x0 k:.word 0x0.text.globl main main:... la $t0, i # lw $a0,0($t0) # a0 = i Carga de valores. lw $a1,4($t0) # a1 = j jal f2 # invocación de función sw $v0,8($t0) # k = v0 Almacenar resultado retornado.... j main Observaciones. La pareja de instrucciones: la $t0, i # a0 = i lw $a0, 0($t0) Prof. Leopoldo Silva Bijit. 15-07-2004 110

Puede reemplazarse por la macro: lw $a0, i(0) También la pareja de instrucciones: la $t0, i # sw $v0,8($t0) # k = v0 Puede reemplazarse por la macro: sw $v0,k(0) Definición de la función f2 # Argumentos en registros a0 y a1. # Resultado de la función en registro v0. # v0 = f2(a0, a1) f2: add $v0, $a0, $a1 #La función comienza en la dirección f2. jr ra Notesé que antes y después del jal, en la secuencia de instrucciones que implementan el llamado a la función, existe código para pasar argumentos y procesar el resultado de la función. Todo esto involucra un costo en tiempo y espacio en memoria para procesar funciones. En los argumentos se colocan valores, y la función también retorna un valor. 6.5.3. Comunicación por argumentos. Paso por referencia. En el ejemplo que veremos en este caso los argumentos son direcciones de variables. Por esta razón se definen como variables de tipo puntero. El modelo sigue siendo de paso por valor, pero lo que se pasa ahora es el valor de un puntero; es decir, una dirección. El siguiente es un ejemplo que ilustra los conceptos anteriores. Código en C. int i, j, k; register int f3(register int *a0, register int *a1) { return (*a0+*a1); } /*argumentos por referencia*/ main() {... k= f3(&i,&j); /*Invocación. Paso de las direcciones de las variables*/... } Prof. Leopoldo Silva Bijit. 15-07-2004 111

Codificación en assembler. Variables.data i:.word 0x0 j:.word 0x0 k:.word 0x0 Invocación..text.globl main main:... la $a0,i #carga punteros. Se pasa una referencia a las variables. la $a1,j #En los argumentos se pasan valores de punteros. jal f3 #invocación de función sw $v0,k(0) # k = v0 (es una macro)... j main Definición. f3: lw $t1,0($a0) # t1 = i lw $t2,0($a1) # t2 = j add $v0,$t1,$t2 jr ra Observación. La utilidad de pasos por referencia es que se puede escribir en las variables externas, y a la vez no se crea espacio en el stack, ni es necesario copiar los valores de los argumentos en el stack. En el ejemplo, sólo se leen variables pasadas por referencia. Se puede modificar el valor de una variable pasada por referencia; por ejemplo si se desea escribir el registro t5 en la variable i, puede escribirse, dentro de la subrutina: sw $t5, 0($a0) # i = t5 o bien, con notación de punteros: *a0 = t5 siempre y cuando no se altere el puntero a0, desde que se le cargó la dirección de i. Suele también utilizarse pasos por referencia, si la función debe retornar más de un valor. 6.6. Administración de la Memoria. Se definen, al menos, cuatro segmentos de memoria. Algunos registros del procesador se emplean para apuntar a dichos segmentos. Prof. Leopoldo Silva Bijit. 15-07-2004 112

En uno de ellos se almacenan las instrucciones, es el segmento de texto. Este segmento puede estar en Eprom. O en la misma memoria RAM en que se almacenan los datos. El registro PC apunta a la instrucción que se está ejecutando. En otro segmento se almacena datos estáticos y constantes. Es el segmento de datos. Las constantes (mensajes, prompts, etc., pueden estar en Eprom) no cambian durante la ejecución del programa. Las variables que estén visibles durante toda la ejecución del programa suelen estar en RAM. Aquí se almacenan variables tipificadas como estáticas en C. En el caso del MIPS, se dispone de un registro que apunta a esta zona, para facilitar el direccionamiento de estas variables y/o constantes (se denomina $gp global pointer). En general todas las variables externas a las funciones de C, ocupan esta zona. Todas las variables, en esta zona, son conocidas en el momento de la compilación. Otro segmento usual es el de datos dinámicos o heap. Esta zona se utiliza mediante llamados a malloc y free en C. Las variables a esta zona están referenciadas por punteros existentes en la zona estática. Finalmente se define un segmento de Stack. En él se almacenan las variables automáticas de C, es decir los argumentos (que no puedan pasarse en registros) y las variables locales (que requieran estar en memoria por no haber registros disponibles o por ser locales de funciones recursivas). En general las locales suelen ser registros temporales. Cada función tiene un espacio donde almacena dichas variables, que se denomina frame. En este espacio también suele guardarse la dirección de retorno de la función. Una vez terminada la ejecución de una función, el frame se desactiva, y el espacio queda disponible para ser reutilizado. Las variables, que se almacenan en el stack, existen sólo durante el momento en que está activa la función que las creó (por esto se llaman automáticas). Este espacio es referenciado mediante dos registros: sp (stack pointer) y fp (frame pointer). En el caso de microcontroladores existen más segmentos. Ya que éstos pueden tener ROM y RAM interna al chip, y externa a éste. En estos casos es importante especificar claramente en qué segmento se tendrá los elementos que se ocuparán. En el caso del procesador MIPS, se dispone del siguiente esquema de memoria: El simulador asume por defecto que el texto comienza en 0x00400000, el segmento de datos estático, comienza en 0x10010000. Y el stack comienza en 0x7fffffff, y crece hacia direcciones menores. Prof. Leopoldo Silva Bijit. 15-07-2004 113

La zona desde 0x00000000 hasta la zona de texto, no puede ser empleada por datos o instrucciones del usuario. Están reservadas para el sistema operativo. Las direcciones: 0xffff000c, 0xffff0008, 0xffff0004, 0xffff0000 se emplean en un dispositivo de entrada salida, orientado al carácter, en el simulador SPIM. E/S mapeada en memoria Zona de Stack $fp 0x7fffffff $sp heap Segmento de Datos 0x10010000 las direcciones aumentan 0x00400000 Segmento de Texto Reservado. Sistema Operativo $gp $pc En el esquema de memoria las direcciones crecen hacia arriba. A continuación se desarrolla un ejemplo que ilustra el uso del stack. 6.7. Desarrollo de programas. Primer programa del apéndice A. (del texto guía) Generar la suma de los cuadrados de los números de 0 a 100. Primero escribiremos un algoritmo en lenguaje C, luego traduciremos el texto C a assembler; esta operación se denomina compilar. Se describe una rutina para imprimir en la consola del simulador, a través de llamados al sistema. El ejemplo muestra que una manera eficiente de describir un programa assembler Prof. Leopoldo Silva Bijit. 15-07-2004 114

es a través de un lenguaje de alto nivel. Esta manera es mejor que emplear un diagrama de flujo. El programa en C, que se coloca como descripción del algoritmo, puede probarse en un computador que tenga un compilador C y genere un ejecutable que corra en ese computador, para de este modo asegurar que cumple las especificaciones, antes de comenzar el proceso de traducción. Programación assembler. #void main(void) # { register unsigned int i; # register unsigned int suma=0; # for(i=0; i<=100; i++) suma=suma+i*i; # printf("\nla suma de los cuadrados de los números de 0 a 100 es %d",suma); # } # #************************************************************************.data mensaje:.asciiz "\nla suma de los cuadrados de los números de 0 a 100 es: " main:.text.globl main addiu $sp, $sp,-4 #push ra sw $ra, 0($sp) #salva dirección de retorno. Main invoca a subrutina. #variables locales en registros. addu $t0, $zero, $zero # $t0 = i = 0 addu $t1, $zero, $zero # $t1= suma = 0 for: sltiu $t2, $t0,101 # i <101 => $t2 =1 beq $t2, $zero, fin multu $t0, $t0 # HI-LO = i*i mflo $t2 add $t1, $t1, $t2 # $t3 = suma + i*i addiu $t0, $t0, 1 # i = i+1 j for fin: la $a0, mensaje # printd(mensaje,suma) addu $a1, $zero, $t1 jal printd lw $ra, 0($sp) # restaura ra Prof. Leopoldo Silva Bijit. 15-07-2004 115

addiu $sp, $sp, 4 # pop jr $ra # retorna desde main. El programa principal (main) se trata como una subrutina, que es invocada por el sistema operativo, o un cargador de programas. Se asume que existe una zona de stack, previamente definida. El puntero apunta al último ocupado. Por esta razón la operación push primero decrementa el stack pointer, y luego almacena el registro ra en la memoria; salvando así la dirección de retorno. De esta forma se posibilita que una subrutina llame a otras. Note la existencia de la instrucción jal, dentro del código de main. Al ingresar a la subrutina se salva la dirección de retorno en el stack, antes de salir de la subrutina se restaura el valor del registro ra, y se reposiciona el stack pointer. #**********************************************************************# # printf( $a0 %d,$a1); # #**********************************************************************# #imprime mensaje apuntado por $a0, seguido de número decimal en $a1 printd: addiu $sp, $sp,-12 #crea espacio del frame de 12 bytes. sw $ra, 0($sp) #salva dirección de retorno #debug para ver argumento decimal de printd en stack sw $a0, 4($sp) #salva arg sw $a1, 8($sp) #salva a1 en frame li $v0, 4 #imprime string en $a0 syscall move $a0, $a1 #macro li $v0, 1 #imprime decimal en $a1 syscall lw $ra, 0($sp) addiu $sp, $sp, 12 jr $ra #y retorna. La subrutina printd, reserva un espacio o frame para mantener en direcciones adyacentes los valores de los argumentos con que trabajará. Es usual que los argumentos y variables locales de una subrutina se almacenen en el stack, en procesadores que no tienen un número elevado de registros de propósito general. En el ejemplo no hay variables locales, sólo argumentos. Prof. Leopoldo Silva Bijit. 15-07-2004 116

Esta forma de proceder crea una localidad espacial de los datos; lo cual, como se verá más adelante, es el fundamento para el empleo de memoria caché. Los siguientes diagramas, ilustran el stack, antes del llamado a printd y después de haber creado el frame y haber almacenado los valores de los argumentos. sp dirección de retorno de main disponible sp dirección de retorno de main valor de a1 valor de a0 dirección de retorno de printd disponible En la subrutina printd, no se emplean los valores almacenados en el stack. Sólo se ilustra la creación de un frame. Debe notarse el código para la creación del frame, y su recuperación antes de salir. Después de salir de printd, main vuelve a encontrar su frame en iguales condiciones que antes de invocar a printd. Compilación. La misma tarea de traducir el texto fuente en C a assembler de MIPS, se puede lograr con un compilador cruzado en un computador que no tenga un procesador MIPS. El siguiente es el código generado por el compilador lcc. Se le han agregado algunos comentarios para hacerlo más legible. Debe notarse el uso de directivas assembler (comienzan con un punto); y también que es un assembler numérico. Los rótulos son generados automáticamente, algunos no se emplean (como L.3). El compilador lcc no emplea el frame pointer..set reorder.globl main.text.text.align 2.ent main main:.frame $sp,32,$31 #especifica tamaño del frame addu $sp,$sp,-32 #crea espacio para el frame.mask 0xc0800000,-8 #indica los registros que deben salvarse. Usa máscara binaria(32 bits) sw $23,16($sp) #salva registros 23, 30 y 31 Prof. Leopoldo Silva Bijit. 15-07-2004 117

sw $30,20($sp) sw $31,24($sp) move $23,$0 move $30,$0 b L.5 L.2: mul $24,$30,$30 #inicia en cero el 23 y lo usa para local suma #inicia en cero el 30 y lo usa para local i #L.5 es un rótulo de un instrucción # temporal = i*i addu $23,$23,$24 # suma = suma + temporal L.3: la $30,1($30) # le suma 1 a i y lo deja en $30 L.5: la $24,100 #temporal = 100 bleu $30,$24,L.2 la $4,L.6 move $5,$23 jal printf L.1: lw $23,16($sp) lw $30,20($sp) lw $31,24($sp) addu $sp,$sp,32 j $31.end main.rdata.align 0 L.6:.byte 10.byte 76.byte 97.byte 115.byte 117.byte 109.byte 97.byte 100.byte 101.byte 108.byte 111.byte 115.byte 99.byte 117 #carga en a0 dirección de inicio del string #carga en a1 el valor entero #recupera registros #retorno # código ascii de \n expresado con valor decimal # código ascii de L # código ascii de a Prof. Leopoldo Silva Bijit. 15-07-2004 118

.byte 97.byte 100.byte 114.byte 97.byte 100.byte 111.byte 115.byte 100.byte 101.byte 108.byte 111.byte 115.byte 110.byte 250.byte 109.byte 101.byte 114.byte 111.byte 115.byte 100.byte 101.byte 48.byte 97.byte 49.byte 48.byte 48.byte 101.byte 115.byte 37.byte 100.byte 0 #fin de string Nótese la falta de legibilidad del string generado por lcc para el mensaje: mensaje:.asciiz "\nla suma de los cuadrados de los números de 0 a 100 es: " Prof. Leopoldo Silva Bijit. 15-07-2004 119

6.8. Función recursiva. Manejo y administración de frames, en el stack. Hasta el momento se han visto los siguientes usos para el stack: Guardar direcciones de retorno de subrutinas, salvar y restaurar registros, almacenar los valores de los argumentos de las subrutinas. Se verá a continuación otro uso para el stack, que es almacenar las variables locales de las subrutinas (variables automáticas en C); esto permite crear subrutinas recursivas. Un programa es recursivo, si dentro de su bloque de acciones se llama a sí mismo. Como veremos, las variables locales y argumentos deben crearse en el stack. Si por ejemplo las variables estuvieran en el segmento estático de datos, sólo podría existir una copia de una variable. Cada vez que se invoca a una rutina recursiva, se crea un frame asociado que contiene espacio para las variables locales; de este modo las diversas ejecuciones del único código que implementa la rutina operan sobre sus propias variables. En el ejemplo, la invocación de f(5) crea una variable n, en el stack; pero esta invocación genera el llamado f(4), que a su vez crea otra variable n, en el stack. A las sucesivas instancias de n, en el stack, se las denomina encarnaciones. Y la re-ejecución del texto de la función recursiva se podría llamar clonación. Una de las principales características de los programas recursivos, es que debe existir una alternativa codificada en términos de los argumentos de la función que terminen las invocaciones recursivas. En el caso del ejemplo en C, si n es menor o igual que cero, no se produce un llamado recursivo, y la función termina. Al terminar desaparecen las variables automáticas (las guardadas en el stack). También se denominan funciones reentrantes, a las que mantienen sus propias variables en su frame. Esto posibilita que dos procesos invoquen a una misma función, cuyo código es compartido. Las funciones del sistema operativo se diseñan como reentrantes. Se emplea como ejemplo el cálculo de n!. Código en C. # #include <stdio.h> # int fact(int n) # { if (n>0) return( n*fact(n-1) ); else return( 1); } # void main(void) # { printf("\nel factorial de 3 es: %d", fact(3)); } Prof. Leopoldo Silva Bijit. 15-07-2004 120

Código en assembler..data mensaje:.asciiz "\nel factorial de 3 es: ".text.globl main main: subu $sp, $sp, 4 #push ra sw $ra, 0($sp) #acciones de main li $a0, 3 # pasa constante 3 como argumento de fact jal fact move $a1, $v0 # el retorno de fact se lleva al argumento a1 de printd la $a0, mensaje # argumento a0 de printd. Imprime salida de fact jal printd lw $ra,0($sp) #restaura ra addu $sp,$sp,4 jr $ra #retorna de main El programa principal salva en el stack la dirección de retorno solamente. La rutina fact, guarda en el stack el argumento n y la dirección de retorno. ################################################################# # $v0 = fact( $a0) # ################################################################# fact: subu $sp, $sp, 8 #crea espacio para n y ra (8 bytes) sw $ra, 0($sp) #acciones de fact sw $a0, 4($sp) # salva a0 en frame. Inicia argumento. lw $v0, 4($sp) bgtz $v0, ifpos # salta si positivo li $v0, 1 # f(0)=1 j salirfact ifpos: lw $v1, 4($sp) # v1 = n subu $v0, $v1, 1 # v0 = n - 1 move $a0, $v0 jal fact # fact(n-1) lw $v1, 4($sp) # v1 = n Prof. Leopoldo Silva Bijit. 15-07-2004 121

mul $v0, $v0, $v1 # v0 = fact(n-1)*n salirfact: lw $ra, 0($sp) #restaura ra addu $sp, $sp,8 jr $ra #retorna de fact No se muestra el código de printd, ya que es la misma subrutina vista antes. Se reutiliza el código. La siguiente tabla muestra el estado del stack, después del llamado fact(0). La última columna muestra hacia donde apunta el stack pointer, cuando está ejecutando el código de una subrutina. Cuando se sale de una subrutina, y se vuelve a la que llamó, se reajusta el puntero sp. Dirección Contenido Observaciones valor de sp 0x7fffefd8 0x00400074 PC después del jal fact en fact(0). dentro de fact(0) 0x7fffefdc 0x00000000 salva valor de a0 cuando invoca fact(0) 0x7fffefe0 0x00400074 PC después del jal fact en fact(1). dentro de fact(1) 0x7fffefe4 0x00000001 salva valor de a0 cuando invoca fact(1) 0x7fffefe8 0x00400074 PC después del jal fact en fact(2). dentro de fact(2) 0x7fffefec 0x00000002 salva valor de a0 cuando invoca fact(2) 0x7fffeff0 0x00400030 PC después del jal fact en main. fact(3) dentro de fact(3) 0x7fffeff4 0x00000003 salva valor de a0 cuando invoca fact(3) 0x7fffeff8 0x00400018 PC después de jal main. (código de arranque) dentro de main 0x7fffeffc 0x00000000 SP apunta aquí antes de invocar a main 6.9. Sobre los argumentos de main y el código de arranque. El trap handler carga en las primeras posiciones del segmento de texto el siguiente código (de arranque):.text 0x00400000.globl start start: lw $a0, 0($sp) # a0 = argc addiu $a1, $sp, 4 # a1 = sp + 4 addiu $a2, $a1, 4 sll $v0, $a0, 2 # v0 = argc*4 addu $a2, $a2, $v0 # a2 = argc*4 + sp+8 jal main li $v0,10 # retorno al monitor syscall Prof. Leopoldo Silva Bijit. 15-07-2004 122

Su finalidad es pasarle a main sus argumentos. En el lenguaje C, se le pueden pasar argumentos en la línea de comandos a un programa. Esto se logra con los argumentos de main, que pueden describirse según: int main(int argc, char *argv[ ] ) Cuando el programa es compilado y ligado, se genera un ejecutable con un nombre que el programador elige (en ambiente PC sea nprog.exe el nombre asignado; a.out por defecto en Unix). Se lo carga en memoria y se lo ejecuta llamándolo por su nombre; y podrían agregarse algunos string que se denominan argumentos. Un ejemplo con dos argumentos es: nprog argumento1 argumento2 Por convenio argc (cuenta el número de argumentos, incluido el nombre del programa), en el caso del ejemplo toma valor 3. Si no hay argumentos argc vale 1. El nombre del programa es reconocido como argv[0], en el ejemplo argv[1] = "argumento1", y argv[2] es igual al string "argumento2". Note que argv[1] es un puntero a char. Además en este caso, argv[argc] es un puntero nulo a carácter; esto por convenio. Note que argv es un arreglo (con dimensión fijada automáticamente) de punteros a char. Entonces, dentro de main, puede efectuarse un procesamiento de los argumentos. Un uso frecuente es pasar nombres de archivos. Por ejemplo, puede imprimirse el primer argumento mediante: if (argc >1 ) printf("%s", argv[1]); El loader, cargador de programas, lee el nombre del programa y los argumentos, solicita memoria en forma dinámica y almacena los argumentos en strings terminados en nulo; y va empujando en el stack la dirección de los strings, partiendo por el último argumento. Lo primero que se empuja es un puntero nulo. Visualmente, la estructura del arreglo de punteros a char, puede representarse por: argv nprog\0 argumento1\ argumento2\ En el simulador SPIM se empujan sólo las direcciones de los strings argumentos propiamente tales (no el nombre del programa) y la cuenta de éstos. En el menú simulator, Prof. Leopoldo Silva Bijit. 15-07-2004 123

activando go, se despliega una ventana con la dirección de inicio y una casilla para entrar los argumentos. Se inicia el stack del modo siguiente: Prof. Leopoldo Silva Bijit. 15-07-2004 124

argc dirección string primer argumento dirección string segundo argumento... dirección string último argumento 0x00000000 tope stack antes de llamar a main Direcciones aumentan. sp a1 a2 Stack después de jal main. $a0 contiene argc. $a1 apunta al puntero al primer argumento. $a2 apunta al tope del stack antes de llamar a main. Los argumentos a0, a1 y a2 son iniciados por el código de arranque. El siguiente programa en C, emplea los argumentos de main, y los imprime. Código en C. ##include <stdio.h> #int main (int argc, char * argv[]) # { int i; # if (argc==1) {printf("\nno hay argumentos"); return(1);} # for( i=0; i<argc; i++) printf("\n%s", argv[i]); # return(0); # } Se traduce a assembler, contemplando que los argumentos se introducen empleando el comando go. Código en assembler..data noargs:.asciiz "\nno hay argumentos" newline:.asciiz "\n".text.globl main main: subu $sp, $sp, 4 #push ra sw $ra, 0($sp) #a0 = argc a1 = argv[0] move $s0,$a0 # salva a0 move $s1,$a1 # salva a1 #acciones de main Prof. Leopoldo Silva Bijit. 15-07-2004 125

li $t0,0 beq $t0,$s0, nohay li $t0,0 #t0 = i lazo: slt $t1,$t0,$s0 #t1 =1 si i<argc beq $t1,$zero,salir lw $a0,0($s1) # a0 = argv[t0] jal prints addiu $s1,$s1,4 addi $t0,$t0,1 j lazo nohay: la $a0, noargs # argumento a0 de prints. jal prints li $v0,1 j fuera salir: li $v0,0 fuera: lw $ra,0($sp) #restaura ra addu $sp,$sp,4 jr $ra #retorna de main #**********************************************************************# # printf("\n%s",$a0); # #**********************************************************************# prints: move $a1,$a0 #salva a0. la $a0,newline # # li $a0,0x0a li $v0, 4 #imprime \n syscall move $a0,$a1 #recupera a0 li $v0, 4 #imprime string en $a0 syscall jr $ra #y retorna. Prof. Leopoldo Silva Bijit. 15-07-2004 126

6.10. Convenio de llamadas a funciones. Preservación de los registros. Un grado de flexibilidad adicional, se logra disponiendo un registro denominado fp(por frame pointer), además de los registros: pc, sp, ra. La finalidad de este puntero es facilitar, a los compiladores, el direccionamiento de los argumentos y variables locales de una función. Además permite direccionar variables de rutinas, que tengan su frame activo, y que se hayan invocado antes. No todos los compiladores emplean dicho registro. En caso de no hacerlo, deben llevar la cuenta si dentro de la subrutina varía el stack pointer, para el direccionamiento de las variables locales y argumentos allí almacenadas. El compilador lcc, no emplea el frame pointer. A continuación veremos algunos conceptos generales sobre los frames de funciones, empleando un puntero al frame, o registro de activación de una función. Se denomina frame, al espacio contiguo de variables y argumentos de una función. Todas las variables almacenadas en el frame, existen mientras esté activa la función. Son las variables automáticas de C. Direcciones altas +4($fp) 0($fp) 32($sp) a4-4($fp) 28($sp) variable local 1-8($fp) 24($sp) variable local 2-12($fp) 20($sp) variable local 3-16($fp) 16($sp) variable local 4-20($fp) 12($sp) para salvar registros s -24($fp) 8($sp) para salvar registros s -28 ($fp) 4($sp) fp -32($fp) 0($sp) ra relativas a fp relativas a sp Direcciones Contenidos Direcciones más bajas $fp Tope Stack $sp Antes de crear el frame, se asume que el sp apunta al tope. La última palabra del frame activo, del que llama. El frame pointer, después de creado el frame apunta al último concepto empujado en el stack antes de invocar. Prof. Leopoldo Silva Bijit. 15-07-2004 127

Se ilustra un frame de 8 palabras (32 bytes) después de fijar el nuevo tope de stack y el nuevo frame pointer. Lo cual permite guardar 6 enteros, que pueden ser registros s o variables locales; además se almacenan ra y fp. En el frame de ejemplo que se ilustra, se considera salvar dos registros s y disponer de 4 variables locales. Las variables locales son necesarias si no alcanzan los registros t y los s que se salvaron; en este caso el acceso a las locales en memoria es más lento que a las locales en registros. También son indispensables en caso de funciones recursivas. En la columna de direcciones, se establecen las referencias respecto del fp, y también respecto de sp. No suelen plantearse respecto de sp, ya que éste puede variar durante la ejecución de la función. Por ejemplo, puede utilizarse el stack, para evaluar expresiones o condiciones complejas, o para salvar algún recurso. Debe cuidarse, en caso de usar el stack, durante las acciones de la función, de emparejar los push con los pop, dentro de la subrutina. En el momento de la compilación, pueden ubicarse todas las variables locales y los argumentos (almacenados en el stack), con direccionamiento relativo a fp. Dentro del frame, suelen guardarse fp y ra en posiciones fijas. Facilitando de este modo el acceso a variables locales de frames anteriores, mediante un direccionamiento indirecto. Por ejemplo, si uno desea usar la primera variable local del frame anterior y depositarla en el registro temporal t1, puede efectuar: lw $t1,-28($fp) # en t1 copia valor de fp anterior lw $t1,-4($t1) Cada compilador tiene una estructura determinada del frame, y ésta debe conocerse si se desea escribir una función en assembler que sea compatible. Es decir, que pueda invocarse desde el programa en C. Un esquema general para rutinas que llaman a otras rutinas, es el siguiente: laquellama: subu $sp, $sp, 32 # crea espacio de 32 bytes (8 palabras) sw $ra, 0($sp) sw $fp, 4($sp) addu $fp, $sp, 32 # fija $fp #acciones de la que llama. lw $t0,... #modifican temporales add $t1,... # salva temporales que quiera usar después del llamado. addiu $sp, $sp,-8 # push sw $t0, 0($sp) # salva valores vivos en sitio del que llama. sw $t1, 4($sp) # caller-save Prof. Leopoldo Silva Bijit. 15-07-2004 128

# puede salvar argumentos actuales move $a0,... #paso de argumentos vía registros move $a1,... move $a2,... move $a3,... addiu $sp,$sp,-4 # push argumento sw valor argumento4, 0($sp) jal llamado #aquí pueden usarse los valores de retorno. addiu $sp, $sp, 4 # pop argumento lw $t0, 0($sp) #recupera conceptos lw $t1, 4($sp) #recupera argumentos actuales, si los guardó. addiu $sp, $sp, 8 # libera espacio #siguen acciones del que llama. Puede seguir usando $t0 y $t1 lw $fp, 4($sp) # restaura frame pointer anterior. lw $ra, 0($sp) # recupera ra addu $sp,$sp,32 # desaparecen automáticas. jr $ra # retorna de laquellama De acuerdo al convenio caller-save, una rutina que llama a otra (laquellama) tiene la responsabilidad de salvar los registros temporales que desee usar después del llamado; así también los valores corrientes de los argumentos, si desea usarlos después del llamado. Luego debe pasar los argumentos, de preferencia en registros; pero si se requieren más de cuatro (a0, a1, a2, a3) debe pasarlos en el stack. Después del llamado, están a disposición los valores retornados (v0, v1). También tiene la responsabilidad de recuperar los registros temporales y argumentos, y ajustar el registro sp. De acuerdo al convenio calle-save, la rutina que es llamada tiene la responsabilidad de salvar los registros s que ella emplee, y también la de restaurarlos. Si la rutina que hemos denominado llamado, a su vez llama a otra, debe cumplir el convenio caller-save. Ambas rutinas deben crear y desarmar el frame. Guardando las direcciones de retorno y el frame pointer. llamado: subu $sp,$sp,tf #tf=4*palabras del frame. Crea frame. sw $ra, 0($sp) sw $fp, 4($sp) addu $fp, $sp, tf #fija nuevo fp Prof. Leopoldo Silva Bijit. 15-07-2004 129

#salva registros s que use el llamado sw $s0, 8($sp) sw $s1, 12($sp) #acciones del llamado. Puede usar libremente registros $ti # pueden emplearse variables locales con referencias a fp #en este ejemplo se usan s0 y s1(se los sobreescribe) add $s0,... lw $s1,... #escribe resultados add $v0,... lw $v1,... lw $s0, 8($sp) # restaura callee-save lw $s1, 12($sp) lw $ra, 0($sp) # restaura ra lw $fp, 4($sp) # recupera frame anterior addu $sp, $sp, tf # desarma frame jr $ra # retorna de llamado En compiladores que usen efectivamente el frame pointer, las referencias a las variables locales y argumentos, pueden realizarse con direccionamiento relativo a $fp. El compilador también puede calcular el tamaño del frame. Prof. Leopoldo Silva Bijit. 15-07-2004 130