Sei sulla pagina 1di 6

Direcciones en el código de destino

 Un área de Código determinada en forma estática, la cual contiene el código destino


ejecutable. El tamaño del código destino se puede determinar en tiempo de
compilación.
 Un área de datos Estática determinada en forma estática, para contener constantes
globales y demás datos que genera el compilador. El tamaño de las constantes globales
y de los datos del compilador también puede determinarse en tiempo de compilación.
 Un área Montículo administrada en forma dinámica, para contener objetos de datos
que se asignan y se liberan durante la ejecución del programa. El tamaño del Montículo
no puede determinarse en tiempo de compilación.
 Un área Pila administrada en forma dinámica, para contener los registros de activación
a medida que se crean y se destruyen, durante las llamadas a los procedimientos y sus
retornos. Al igual que el Montículo, el tamaño de la Pila no puede determinarse en
tiempo de compilación.

Asignación Estática
 Los nombres se enlazan a las posiciones de memoria durante la compilación del
programa.
 No requiere de un programa run-time
 Cada procedimiento tiene asignado un registro de activación, cuya posición de memoria
es asignada en la compilación
 Los valores de las variables locales permanecen entre diferentes activaciones del mismo
procedimiento
 Esto implica que:
 El tamaño de los objetos de datos debe ser conocido en tiempo de compilación.
o No es posible declarar procedimientos recursivos.
o No pueden usarse estructuras de datos dinámicas.
 Ejemplo: Fortran
 Implementa la pila de control: el almacenamiento es manejado como una pila, y los
registros de activación entran a la pila cuando inicia la ejecución del procedimiento, y
salen de ella cuando termina.
 Los valores de las variables locales se pierden cuando la activación termina.
 Un registro marca el tope de la pila, cuando se activa el procedimiento, se asigna el
espacio para su registro de activación.

En la asignación estática se fija la posición de un registro de activación en la memoria de


ejecución.

La generación de código para las llamadas y retornos simplificados de procedimientos, vamos a


enfocamos en las siguientes instrucciones de tres direcciones:
La asignación estática de memoria se utiliza cuando el programa fuente define un identificador
al que se debe asignar espacio una sola vez durante el programa. Por ejemplo, una variable con
ámbito global o bien un valor estático de una rutina (como el tipo static de C).

Los lenguajes que utilizan asignación estática de memoria como Fortran necesitan conocer el
tipo y el tamaño de memoria de todos los objetos en compilación: Es decir, hay que declarar
los nombres antes de utilizarlos.

En estos lenguajes no se permiten:

o Llamadas recursivas a subrutinas


o Estructuras de datos dinámicas.

 CALL RECEPTOR
RETURN
 HALT
 ACTION

Por ejemplo, el código de tres direcciones para los procedimientos c y p de la figura 9.4 contiene
precisamente estas clases de proposiciones. El tamaño y la disposición de los registros de
activación se comunican al generador de código a través de la información de la tabla de
símbolos sobre los nombres. Para verlo con claridad, en la figura 9,4 se muestra la disposición
en lugar de la forma de las entradas de la tabla de símbolos.

Figura 1 Entrada para un generador de código

Primero vamos a considerar el código necesario para implementar el caso más simple, la
asignación estática. Aquí, una instrucción call receptor en el código intermedio puede
implementarse mediante una secuencia de dos instrucciones de máquina destino:

 ST receptor. areaEstatica, #aqui + 20


 BR receptor. areaCodigo

L a instrucción ST guarda la dirección de retomo al principio del registro de activación para


receptor, y la instrucción BR transfiere el control al código de destino para el procedimiento
receptor al que se llamó. El atributo antes de receptor.areaEstatica es una constante que
proporciona la dirección del inicio del registro de activación para receptor, y el atributo receptor.
areaCodigo es una constante que se refiere a la dirección de la primera instrucción del
procedimiento receptor al que se llamó en el área Código de la memoria en tiempo de ejecución.
El operando #aqui + 20 en la instrucción ST es la dirección de retomo literal; e s la dirección de
la instrucción que sigue después de la instrucción BR. Suponemos que #aqui es la dirección de
la instrucción actual, y que las tres constantes más las dos instrucciones en la secuencia de
llamada tienen una longitud de 5 palabras, o 20 bytes. E l código para un procedimiento termina
con un retomo al procedimiento que hizo la llamada, sólo que el primer procedimiento no tiene
emisor, por lo que su instrucción final es HALT, la cual devuelve el control al sistema operativo.
Una instrucción return receptor puede implementarse mediante una instrucción sim ple de
salto: BR * receptor. areaEstatica la cual transfiere el control a la dirección guardada al principio
del registro de activación para receptor.

Por conveniencia, vamos a usar desplazamientos positivos, manteniendo en un registro SP un


apuntador al inicio del registro de activación en la parte superior de la pila. Cuando ocurre una
llamada a un procedimiento, el procedimiento que llama incrementa a SP y transfiere el control
al procedimiento al que llamó. Una vez que el control regresa al emisor, decremento el SP, con
lo cual se desasigna el registro de activación del procedimiento al que se llamó.

El código para el primer procedimiento inicializa la pila, estableciendo SP al inicio del área de la
pila en la memoria:

LD SP , #inicio Pila //inicializa la pila

código para el primer procedimiento

HALT / / termina la ejecución

La secuencia de la llamada a un procedimiento incrementa a SP, guarda la dirección de retorno


y transfiere el control al procedimiento a l que se llamó:

ADD SP, SP, # emisor.tamRegistro / / incrementa el apuntador de la pila

ST *SP, # aquí + 16 / / guarda la dirección de retomo

BR receptor.areaCodigo // regresa al emisor

El operando # emisor. tamRegistro representa el tamaño de un registro de activación, por lo que


la instrucción ADD hace que SP apunte al siguiente registro de activación. E l operando #aquí +
16 e n la instrucción ST es la dirección de la instrucción que va después de BR; se guarda en la
dirección a la que apunta SP.

La secuencia de retomo consiste en dos partes. El procedimiento al que se llamó transfiere el


control a la dirección de retomo, usando lo siguiente:

BR * 0 (SP) / / regresa al emisor

L a razón de usar * 0 (SP) en la instrucción BR se necesita dos niveles de indirección: O (SP) e s


la dirección de la primera palabra en el registro de activación, y *0 (SP) es la dirección de retorno
que está guardada ahí.
La segunda parte de la secuencia de retomo está en el emisor, el cual decremento a SP, con lo
cual SP se restaura a su valor anterior. Es decir, después de la resta SP apunta al inicio del registro
de activación del emisor:

SUB SP, S P, R emisor.tamRegistro / / decremento el apuntador de la pila

La estrategia de asignación de almacenamiento y la distribución de los datos locales en un


registro de activación para un procedimiento son los que determinan cómo se accede al
almacenamiento para los nombres.

Este método tiene una ven taja considerable; hace que el compilador sea más portable, ya que
el front-end no tiene que modificarse, ni siquiera cuan do el compilador se mueve a una

Máquina distinta, e n donde se requiere una organización distinta en tiempo de ejecución. Por
otro lado, la acción de generar la secuencia específica de pasos de acceso mientras se gen era el
código intermedio puede representar un a ventaja considerable en un compilador optimizador.

ENTRADA: Una secuencia de instrucciones de tres direcciones.

SALIDA: Una lista de los bloques básicos para la secuencia en la que cada instrucción se asigna
exactamente a un bloque básico.

MÉTODO: En primer lugar, determinamos las instrucciones en el código intermedio que son
líderes; es decir, las primeras instrucciones en algún bloque básico. La instrucción que va justo
después del programa intermedio no se incluye como líder. L as reglas para buscar líderes son:

1. La primera instrucción de tres direcciones en el código intermedio es líder

2. Cualquier instrucción que sea el destino de un salto condicional o incondicional es líder.

3. Cualquier instrucción que siga justo después de un salto condicional o incondicional es líder.

Es esencial saber cuándo se usará el valor de una variable para generar buen código. Si el valor
de una variable que s e encuentra en un registro nunca se utilizará más adelante, entonces ese
registro puede asignarse a otra variable.

El uso de un nombre en una instrucción de tres direcciones se define a continuación. Suponga


que la instrucción de tres direcciones i asigna un valor a x. S i la instrucción j tiene a x como
operando, y el control puede fluir de la instrucción i hacia j a través de una ruta que no tiene
instrucciones en las que intervenga x, entonces decimos que la instrucción j utiliza el valor de x
calculado en la instrucción i. Además, decimos que x está viva en la instrucción i.

Deseamos determinar para la instrucción de tres direcciones x — y + z cuáles van a ser los
siguientes usos de( x, y) y 2. Por e l momento, no nos preocuparemos por los usos fuera del
bloque básico que contiene esta instrucción de tres direcciones.

Nuestro algoritmo para determinar la información sobre la vida y el siguiente uso realiza una
pasada invertida sobre cada bloque básico.
ENTRADA: Un bloque básico B de instrucciones de tres direcciones. Suponemos que, al
principio, la tabla de símbolos muestra que todas las variables no temporales en B están vivas
al salir.

SALIDA: En cada instrucción i: x = y + 2 en B, adjuntamos a ¿la información sobre la vida y el


siguiente uso de x, y y z.

MÉTODO: Empezamos en la última instrucción en B, y exploramos en forma invertida hasta el


inicio de B. En cada instrucción i: x = y + 2 en B, hacemos lo siguiente:

1. Adjuntar a la instrucción i la información que se encuentra en ese momento en la tabla de


símbolos, en relación con el siguiente uso y la v id a d e x, y y 2.

2. En la tabla de símbolos, establecer x a “no viva” y “sin siguiente uso”.

3. En la tabla de símbolos, establecer y y 2 a ‘Viva” y los siguientes usos de y y 2 a i.

Una vez que un programa de código intermedio se particiona en bloques básicos,


representamos el flujo de con trol entre ellos mediante un grafo de flujo. Los nodos del grafo
de flujo so n los nodos básicos. Hay una flecha del bloque B al bloque C sí, y sólo si es posible
que la primera instrucción en el bloque C vaya justo después de la última instrucción en e l
bloque B. H ay dos formas en las que podría justificarse dicha fecha:

 Hay un salto condicional o incondicional desde el final de B hasta el inicio de C.


 C sigue justo después de B en e l orden original de las instrucciones de tres direcciones,
y B no termina en un salto incondicional.
 Decimos que B e s un predecesor de C, y que C es un sucesor de B.

A menudo agregamos dos nodos, conocidos como entrada y salida, los cuales no corresponden
a instrucciones intermedias ejecutables. H ay u n a flecha que va desde la entrada hasta el primer
nodo ejecutable del grafo de flujo; es decir, al bloque básico que surge de la primera instrucción
del código intermedio. Hay una flecha que va a la salid a desde cualquier bloque básico que con
tenga una instrucción, que pudiera ser la última instrucción ejecutada del programa.

Si la instrucción final del programa no es un salto incondicional, entonces el bloque que contiene
la instrucción final del programa es un predecesor de la salida, p ero también lo es cualquier b
lo que básico que tenga un salto hacia código que no forme parte del programa.

En primer lugar, observe en la figura 8.9 que en el grafo de flujo, es normal sustituir los saltos
hacia números de instrucción o etiquetas, por saltos hacia bloques básicos. Recuerde q u e
todo salto condicional o incondicional va hacia la instrucción líder de algún bloque básico, y
que el salto ahora hará referencia a este bloque. La razón de este cambio es que después de
construir el grafo de flujo, es común realizar cambios considerables a las instrucciones e n los
diversos bloques básicos. Si los saltos fueran a instrucciones, tendríamos que corregir los
destinos de éstos cada vez que se modificara una de las instrucciones de destino.

Siendo grafos bastante ordinarios, los grafos de flujo pueden representarse mediante una de
las estructuras de datos apropiadas para grafos. El contenido de los nodos (bloques básicos)
necesita su propia representación. Podríamos representar el contenido de un nodo mediante
un apuntador a la instrucción líder en e l arreglo de instrucciones de tres direcciones, junto
con un conteo del número de instrucciones, o un segundo apuntador a la última instrucción.
Sin embargo, como tal vez cambiemos el número de instrucciones en un bloque básico con
frecuencia, es probable que sea más eficiente crear una lista enlazada de instrucciones para
cada bloque básico.

Potrebbero piacerti anche