miércoles, 29 de julio de 2009

martes, 23 de junio de 2009

PRUEBA 2

1. Señale dos ventajas de la generación de código intermedio

1.1 Una ventaja es que el código objeto es abstraído para una maquina virtual. Esta abstracción ayuda a separar operaciones de alto y bajo nivel y realizaciones dependientes de la máquina de igual manera la generación de código y el asignamiento de registros temporales son separados de las RUTINAS SEMANTICAS, las cuales solo trabajan con la abstracción presentada por la representación intermedia. Las dependencias del código objeto son aisladas de las rutinas de generación de código.
1.2 Una ventaja es que la optimización puede ser hecha en el nivel de representación intermedia. Esta organización ayuda a hacer una optimización completamente independiente del código objet, con lo que hace que las trutinas de optimización complejas sean más transportables (Que puedan llevarla de una arquitectura a otra) debido a que las representaciones intermedias son por diseño más abstractas y uniformes, las rutinas de optimización pueden ser más simples.

2. Explique con un ejemplo la optimización de código.

Un ejemplo pequeño de optimización de código es el siguiente caso:
a=1/2; //Código lento no recomendado.
a=0.5; //Código rápido recomendado.
El compilador realiza la división entre 1 y 2 mientras por el contrario el segundo caso es directamente el resultado y no necesita ser procesado.

3. Señale el proceso de compilación de un programa en C#.

Lo primero que le ocurre a un fichero .c de código C es el preprocesado. En este paso se sustituyen todas las macros y se eliminan los comentarios. El resultado, si lo vemos independientemente, es un fichero de código C preprocesado, o .i.
El segundo paso es la compilación propiamente dicha, en el que el código C preprocesado se convierte en código ensamblador, que si lo vemos independientemente es un fichero .s.
El tercer paso es el ensamblado del código ensamblador, lo que lo convierte en código máquina. Un fichero de código máquina también es llamado fichero objeto, y su extensión típica es .o.
Dado que el camino anterior normalmente convierte un fichero en un fichero, se suele hacer en un sólo paso, de código C a fichero objeto.
El último paso es el enlazado de los ficheros objeto en el ejecutable final.


Ejemplo del proceso de compilación
Tomemos el código C más simple posible:
int main(void) /*Ejemplo*/
{
return(0);
}
Al preprocesarlo tendremos:
# 1 "prueba.c"
# 1 ""
# 1 ""
# 1 "prueba.c"
int main(void)
{
return(0);
}
Vemos que el comentario ha desaparecido. En su lugar aparecen comentarios específicos del preprocesador. Al compilarlo tenemos:
.file "prueba.c"
.text
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp
movl $0, %eax
leave
ret
.size main, .-main
.ident "GCC: (GNU) 4.0.3 20051023 (prerelease) (Debian 4.0.2-3)"
.section .note.GNU-stack,"",@progbits
Un precioso código ensamblador que enseguida convertimos en un ilegible código máquina:
ELFØ4( UåäðžÀÀÁèÁà)ÄžÉÃGCC: (GNU) 4.0.3 20051023 (prerelease) (Debian 4.0.2-3).symtab.strtab.shstrtab.text.data.bss.comment.note.GNU-stack4#!XX,X95E@Àñÿ
#prueba.cmain
Ese código máquina es el fichero .o que normalmente obtenemos directamente del código C. Finalmente, ese código objeto es enlazado con las librerías necesarias para formar un ejecutable:
ìÀ£žÿÒ¡žÒuëÆŒÉÃöUå¡ÌÀtžÀtäðPTRhhQVhhè³ÿÿÿôUåSQè[ßüÿÿÿÒtè ÿÿÿX[ÉÃUå=Œt+ën_used__libc_start_mainGLIBC_2.0$ii
hÌÿÐÄÉÃUåäðžÀÀÁèÁà)ÄžÉÃUåWVSì
è[Ãþè²þÿÿ ÿÿÿ ÿÿÿ)ÐÁøEðuÄ
[^_]ÃŽ&1ÿÖ¶¿ÿGÆ;}ðrõÄ
[^_]öŒ'UåWVSì
è[Ã ÿÿÿ ÿÿÿ)ÐÁøEðHøÿt41ÿ¶¿ÿGî9}ðuõèDÄ
[^_]ÃUåSR¡Œøÿt»Œ¶¿ÿÐCüëøÿuóX[]ÃUåSPè[ÃþèVþÿÿX[ÉÃÿÿÿÿÿÿÿÿ$
HÀp \
Y
þÿÿo$ÿÿÿoðÿÿoÐÈGCC: (GNU) 4.0.2 (Debian 4.0.2-2)GCC: (GNU) 4.0.2 (Debian 4.0.2-2)GCC: (GNU) 4.0.3 20051023 (prerelease) (Debian 4.0.2-3)GCC: (GNU) 4.0.3 20051023 (prerelease) (Debian 4.0.2-3)GCC: (GNU) 4.0.2 (Debian 4.0.2-2)GCC: (GNU) 4.0.3 20051023 (prerelease) (Debian 4.0.2-3)GCC: (GNU) 4.0.2 (Debian 4.0.2-2)°",\
Ô$$̪q!y_IO_stdin_used°Ò../sysdeps/i386/elf/start.S/space/debian/glibc/build-area/glibc-2.3.5/build-tree/glibc-2.3.5/csuGNU AS 2.16.1XÔÔ¬F}xgMint\}nŽO-V|/space/debian/glibc/build-area/glibc-2.3.5/build-tree/i386-libc/csu/crti.S/space/debian/glibc/build-area/glibc-2.3.5/build-tree/glibc-2.3.5/csuGNU AS 2.16.1¬f(/space/debian/glibc/build-area/glibc-2.3.5/build-tree/i386-libc/csu/crtn.S/space/debian/glibc/build-area/glibc-2.3.5/build-tree/glibc-2.3.5/csuGNU AS 2.16.1%
$

>
$

>
4:
;
I?

T/û
../sysdeps/i386/elfstart.S°À01:"VWYX û
init.cš^û
/space/debian/glibc/build-area/glibc-2.3.5/build-tree/i386-libc/csucrti.S3,W\#,:Ô
,Wdd,,W^û
/space/debian/glibc/build-area/glibc-2.3.5/build-tree/i386-libc/csucrtn.Sªq /space/debian/glibc/build-area/glibc-2.3.5/build-tree/glibc-2.3.5/csuinit.cshort intlong long intunsigned charlong long unsigned intshort unsigned int_IO_stdin_usedGNU C 4.0.2 (Debian 4.0.2-2).symtab.strtab.shstrtab.interp.note.ABI-tag.hash.dynsym.dynstr.gnu.version.gnu.version_r.rel.dyn.rel.plt.init.text.fini.rodata.eh_frame.ctors.dtors.jcr.dynamic.got.got.plt.data.bss.comment.debug_aranges.debug_pubnames.debug_info.debug_abbrev.debug_line.debug_str#(( 1HH(7
ppP?ÀÀYGÿÿÿo
Tþÿÿo$$ c lD LL
u\\ptt0{°°ä°žžŒ ħÌ̬ÐеºÃ°°
ŒŒÎŒ7×øæp%ö}
v
²0:
'|`!5 Üî(HpÀ$L \
t
°
°žŒÄÌаŒ !Ô
ŒÄ-Ì:ŒIžP
f@
rÀȞ̊`
ŒÐÅŒñÿÖŒñÿéŒñÿúŒñÿ#°*Ž7X
G\

Tc
dŒñÿph#
£ŒñÿªÀñÿ¯ŽŸ°Ë ß call_gmon_start__CTOR_LIST____DTOR_LIST____JCR_LIST__completed.4463p.4462__do_global_dtors_auxframe_dummy__CTOR_END____DTOR_END____FRAME_END____JCR_END____do_global_ctors_aux_DYNAMIC__fini_array_end__fini_array_start__init_array_end_GLOBAL_OFFSET_TABLE___init_array_start_fp_hw__dso_handle__libc_csu_fini_init_start__libc_csu_init__bss_startmain__libc_start_main@@GLIBC_2.0data_start_fini_edata_end_IO_stdin_used__data_start_Jv_RegisterClasses__gmon_start_






4. Generar CI para el código siguiente:

If x>10 and Not(y<0) Then
A+=2
B+=1
End if



for (i=0; i<10; i++){
x += i;
}

PRUEBA 1

1. En el cuadro siguiente escriba dos tareas para cada analizador:
Léxico Sintáctico Semántico





Analizador léxico:
- Lee la secuencia de caracteres del programa fuente, caracter a caracter, y los agrupa para formar unidades con significado propio, los componentes léxicos (tokens).
- Contabilizar el número de líneas y columnas para emitir mensajes de error.

Analizador Sintáctico:

- El analizador sintáctico convierte el texto(código) de entrada en otras estructuras (comúnmente árboles), que son más útiles para el posterior análisis y capturan la jerarquía implícita de la entrada. Un analizador léxico crea tokens de una secuencia de caracteres de entrada y son estos tokens los que son procesados por el analizador sintáctico para construir la estructura de datos, por ejemplo un árbol de análisis o árboles abstractos de sintaxis.
- El análisis sintáctico también es un estado inicial del análisis de frases de lenguaje natural. Es usado para generar diagramas de lenguajes que usan flexión gramatical, como los idiomas romances o el latín. Los lenguajes habitualmente reconocidos por los analizadores sintácticos son los lenguajes libres de contexto.

Analizador Semántico:

- El análisis semántico utiliza como entrada el árbol sintáctico detectado por el análisis sintáctico para comprobar restricciones de tipo y otras limitaciones semánticas y preparar la generación de código.
- el análisis semántico se realiza independientemente de la generación de código, pasándose información a través de un archivo intermedio, que normalmente contiene información sobre el árbol sintáctico en forma linealizada (para facilitar su manejo y hacer posible su almacenamiento en memoria auxiliar).



2. Transformar las siguientes expresiones a notación postfija y prefijo.
a. a * b % c – d * a – d % e


Notación Postfija: a b c % * d a * d e % - -
Notación Prefija: * a b % c * - d a % - d e

b. (a + c) – (d * e) / x % y

Notación Postfija: a c + d e * x y % / -
Notación Prefija: + a c *- d e / % x y

c. (h – i – j * k) % m / x / y

Notación Postfija: h i j k * - - m x y / / %
Notación Prefija: - h i * - j k % / m x / y



3. Dado el árbol de derivación, obtener la expresión en notación infija, postfija y prefija.





En notación infija: ( a * b % c – d ) * ( h – f % e) Depende de cómo se le interpreta el árbol.
En notación Postfija: a b c % * d – h f e % - *
En notación Prefija: * * a b % - c d – h f % e

miércoles, 13 de mayo de 2009

Herramientas para la construcción de Compiladores

Herramientas
Utilidades y Generadores de Compiladores

A continuación se muestran algunas de las herramientas disponibles que pueden utilizarse para la realización del Proyecto de Compiladores. Todas estas herramientas funcionan bajo Windows.
Herramientas para la construcción de compiladores Herramienta Lenguaje Descripción
Bison C Generador de Analizadores Sintácticos Ascendentes tipo YACC
COCO/R C/C++ Generador de Analizadores Léxicos y Sintácticos Descendentes Recursivos
Flex C Generador de Analizadores Léxicos tipo Lex
Lex C Generador de Analizadores Léxicos
SDGLL1 exe Sistema Detector de Gramáticas LL(1)
TS 2006 C/C++ Tipo abstracto de datos Tabla de Símbolos de uso sencillo (beta 0.4)
TS C Tipo abstracto de datos Tabla de Símbolos
TS-OO C++ Tipo abstracto de datos Tabla de Símbolos
YACC C Generador de Analizadores Sintácticos Ascendentes LR(1)

Nota: El uso de estas herramientas de compiladores no es en absoluto obligatorio ni se garantiza su correcto funcionamiento. Se muestran aquí solamente a título informativo. Los profesores de la asignatura no proporcionarán ayuda ni información adicional sobre dichas herramientas.
Ensambladores Simbólicos ENS

Los ensambladores simbólicos ENS permiten ensamblar, ejecutar y depurar el código ensamblador generado por el compilador. Dentro de los ficheros comprimidos que se pueden obtener en la tabla, se encuentra información sobre su uso, su sintaxis y algún ejemplo de funcionamiento. El compilador construido en el Proyecto de Compiladores tiene que generar como código objeto uno de estos ensambladores.
Ensambladores simbólicos Versión ENS S.O. Descripción
ENS 2001 DOS (Consola Windows) Lenguaje ensamblador basado en el estándar IEEE 694. Entorno textual de ensamblado y depuración.
W-ENS 2001 Windows Lenguaje ensamblador basado en el estándar IEEE 694. Entorno gráfico de ensamblado y depuración.
L-ENS 2001 Linux Lenguaje ensamblador basado en el estándar IEEE 694. Entorno textual de ensamblado y depuración. Incluye fuentes.
ASS 1.3 Linux Lenguaje ensamblador sencillo. Entorno textual de ensamblado y depuración. Incluye fuentes.
ENS 96 DOS Lenguaje ensamblador basado en un estándar IEEE 694 reducido. Entorno textual de ensamblado y depuración.

Generador de código intermedio

El analizador sintactico va generando acciones que valida el analizador semántico y que se convierten en tercetos. Esta conversión en tercetos constituye el generador de código intermedio.

Dado que el lenguaje puede presentar distintas funciones anidadas, los tercetos los generamos por orden del parser y son almacenados en un sitio u otro dependiendo del contexto en que nos encontremos. Es decir, se almacenan en una lista de tercetos dependiente de la Tabla de Simbolos. Hay tantas listas de tercetos como funciones haya en el código fuente más una lista de tercetos asociada a la Tabla de Simbolos Global

No obstante una vez finalizado el análisis, todos estos tercetos repartidos en distintas listas se vuelcan a una sola lista de tercetos global. Esta será la que finalmente se optimice y a partir de la que se generará el programa en ensamblador.

El problema de tener que manejar tercetos indirectos fue resuelto modificando el método de inserción sobre la lista de tercetos utilizada en cada momento, de manera que se realiza previamente una búsqueda de algún terceto que sea exactamente igual al que estamos insertando. En caso afirmativo, insertamos en la lista no un terceto nuevo, sino un puntero al ya existente, y marcamos dicho terceto como terceto indirecto. Son tercetos indirectos aquellos marcados con un asterisco despúes del índice en los volcados de la lista de tercetos.

Gestion de memoria en tiempo de ejecucion

Cuando un programa se ejecuta sobre un sistema operativo existe un proceso previo
llamado cargador que suministra al programa un bloque contiguo de memoria sobre el
cual ha de ejecutarse. El programa resultante de la compilación debe organizarse de
forma que haga uso de este bloque. Para ello el compilador incorpora al programa objeto
el código necesario.
Las técnicas de gestión de la memoria durante la ejecución del programa difieren de unos lenguajes a otros, e incluso de unos compiladores a otros.

Para lenguajes imperativos, los compiladores generan programas que tendrán en tiempo
de ejecución una organización de la memoria similar (a grandes rasgos) a la que aparece
en la figura 1.







En este esquema se distinguen las secciones de:
- El Código
- La Memoria Estática.
- La Pila.
- El Montón.



El código:

Es la zona donde se almacenan las instrucciones del programa ejecutable en código
máquina, y también el código correspondiente a los procedimientos y funciones que
utiliza. Su tamaño puede fijarse en tiempo de compilación.




La memoria estática



La forma más fácil de almacenar el contenido de una variable en memoria en tiempo de
ejecución es en memoria estática o permanente a lo largo de toda la ejecución del
programa. No todos los objetos (variables) pueden ser almacenados estáticamente. Para
que un objeto pueda ser almacenado en memoria estática su tamaño ( número de bytes
necesarios para su almacenamiento) ha de ser conocido en tiempo de compilación. Como
consecuencia de esta condición no podrán almacenarse en memoria estática:
• Los objetos correspondientes a procedimientos o funciones recursivas, ya que en tiempo
de compilación no se sabe el número de variables que serán necesarias.
• Las estructuras dinámicas de datos tales como listas, árboles, etc. ya que el número de
elementos que la forman no es conocido hasta que el programa se ejecuta.


La Pila


La aparición de lenguajes con estructura de bloque trajo consigo la necesidad de técnicas
de alojamiento en memoria más flexibles, que pudieran adaptarse a las demandas de
memoria durante la ejecución del programa. En estos lenguajes, cada vez que comienza
la ejecución de un procedimiento se crea un registro de activación para contener los
objetos necesarios para su ejecución, eliminandolo una vez terminada ésta.
Dado que los bloques o procedimientos están organizados jerárquicamente, los distintos
registros de activación asociados a cada bloque deberán colocarse en una pila en la que
entrarán cuando comience la ejecución del bloque y saldrán al terminar el mismo. (Fig.
5b) La estructura de los registros de activación varía de unos lenguajes a otros, e incluso
de unos compiladores a otros. Este es uno de los problemas por los que a veces resulta
difícil enlazar los códigos generados por dos compiladores diferentes. En general, los
registros de activación de los procedimientos suelen tener algunos de los campos que
pueden verse en la fig.
5a.


viernes, 8 de mayo de 2009