Sei sulla pagina 1di 66

Instituto Tecnológico Superior de Misantla

ANTOLOGÍA

LENGUAJES Y AUTOMATAS II

ING. ELSA IRENE HERRERA SANTIAGO

Misantla, Veracruz. A 27 de Agosto de2 014

0
Instituto Tecnológico Superior de Misantla

INDICE

Introduccion
Objetivos del Curso

1. Análisis semántico
1.1. Arboles de expresiones.
1.2. Acciones semánticas de un analizador sintáctico.
1.3. Comprobaciones de tipos en expresiones.
1.4. Pila semántica en un analizador sintáctico.
1.5. Esquema de traducción.
1.6. Generación de la tabla de símbolo y de direcciones.
1.7. Manejo de errores semánticos.

2. Generación de código intermedio.


2.1. Notaciones
2.1.1. Prefija
2.1.2. Infija
2.1.3. Postfija
2.2. Representaciones de código Intermedio.
2.2.1. Notación Polaca
2.2.2. Código P
2.2.3. Triplos
2.2.4. Cuádruplos.
2.3. Esquema de generación.
2.3.1. Variables y constantes.
2.3.2. Expresiones.
2.3.3. Instrucción de asignación.
2.3.4. Instrucciones de control.
2.3.5. Funciones
2.3.6. Estructuras

3. Optimización
3.1. Tipos de optimización.
3.1.1. Locales.
3.1.2. Ciclos.
3.1.3. Globales.
3.1.4. De mirilla.
3.2. Costos.
3.2.1. Costo de ejecución. (memoria, registros, pilas)
3.2.2. Criterios para mejorar el código.

1
Instituto Tecnológico Superior de Misantla

3.2.3. Herramientas para el análisis del flujo de datos.

4. Generación de código objeto.


4.1. Registros.
4.2. Lenguaje ensamblador.
4.3. Lenguaje maquina.
4.4. Administración de memoria.

Evaluación
Bibliografia

2
Instituto Tecnológico Superior de Misantla

Objetivos del Curso

En esta asignatura se debe desarrollar el análisis semántico, la generación de


código, la optimización y la generación del código objeto para obtener el
funcionamiento de un compilador.
Esta asignatura busca proveer al estudiante de herramientas, conocimientos y
habilidades necesarias para desarrollar un compilador con base en los conocimientos
previos de la asignatura lenguajes y autómatas I.
La aportación de esta materia es relevante en el ámbito del desarrollo de
software de sistemas.
Es indispensable distinguir que la carrera de Ingeniería en Sistemas
Computacionales se basa no sólo en el desarrollo de software comercial y
administrativo, sino también en el desarrollo de software científico y para el desarrollo
tecnológico.
Esta materia se ubica en la segunda categoría y es indispensable desarrollar
software en estos campos para preparar a los egresados y tengan la posibilidad de
cursar posgrados de alto nivel.
La asignatura trata de concretar un traductor iniciado en la materia previa para
que el estudiante comprenda que es capaz, mediante técnicas bien definidas, de crear
su propio lenguaje de programación. La aportación de la asignatura al perfil del
egresado será específicamente la siguiente:

 Desarrollar, implementar y administrar software de sistemas o de aplicación que


cumpla con los estándares de calidad buscando como finalidad apoyar la
productividad y competitividad de las organizaciones.

 Integrar soluciones computacionales con diferentes tecnologías, plataformas o


dispositivos.

 Diseñar e implementar interfaces hombre – máquina y maquina – máquina para


la automatización de sistemas.

 Identificar y comprender las tecnologías de hardware para proponer, desarrollar


y mantener aplicaciones eficientes.

3
Instituto Tecnológico Superior de Misantla

Introducción
El lenguaje es un vehículo por el cual se transmiten instrucciones a un
procesador para que las ejecute y produzca ciertos resultados.

Es tarea del compilador extraer el contenido semántico incluido en las


sentencias del programa.

Ciertos aspectos relativos a la corrección de un programa no se pueden


expresar claramente mediante el lenguaje de programación.

Es necesario dotar al compilador de rutinas auxiliares para captar todo lo que no


se ha expresado mediante la sintaxis del lenguaje.

Semántica: conjunto de reglas que espesignifican cualquier sentencia


sintácticamente correcta y escrita en un determinado lenguaje. El análisis semántico, a
diferencia de otras no se realiza claramente diferenciado del resto de las tareas del
compilador.

 Fase en la que se obtiene información necesaria para la compilación, tras


conocer la estructura sintáctica del programa.
 Completa las fases de análisis léxico y sintáctico incorporando comprobaciones
que no pueden asimilarse al reconocimiento de una cadena dentro de un
lenguaje.

El análisis semántico se realiza posteriormente al sintáctico y mucho más difícil
de formalizar que éste.

La salida “teórica” de la fase de análisis semántico sería un árbol semántico.


 Análisis léxico: Detecta entradas de tokens legales.
 Análisis Sintáctico: Detecta entradas con árboles mal formados.
 Análisis Semántico: Detecta todos los errores restantes.

4
Instituto Tecnológico Superior de Misantla

Unidad 1. Análisis semántico

1.1. Arboles de Expresiones

Las expresiones regulares que se pueden representar a través de los símbolos


contenidos en un alfabeto, también se pueden representar a través de árboles, que se
denominan árboles de expresiones.

La información contenida en los árboles de expresiones tienen las siguientes


características.
 Se almacena en forma de estructura jerárquica, porque los componentes están
en diferentes niveles.
 La información se almacena en forma Dinámica porque su forma, tamaño y
contenido pueden variar durante la ejecución.
 Los datos son almacenados de forma Organizada porque importa la forma en
que está el contenido.
 La consulta de los datos se hace de manera rápida y eficiente obteniendo
mejores resultados.

Una vez que se tienen registradas las entidades que se usarán en el programa,
el siguiente paso es emplear este registro para la verificación de los tipos y el análisis
semántico de las operaciones; esto ocurre principalmente en donde se usan
expresiones, como veremos en el caso de la operación de asignación.
Junto con el registro de cada entidad, debemos tener definido una tabla donde
se relacionan los tipos en cada operación para determinar el tipo resultante (alguno de
los tipos simples, en caso que la operación es posible o ERROR cuando no puede
realizarse).

Los árboles de expresiones están compuestos por los siguientes elementos:

5
Instituto Tecnológico Superior de Misantla

Las reglas para representar una expresión mediante un árbol son las siguientes:

1. Cada hoja está etiquetada con un operando y solo consta de ese operando.
2. Cada nodo interior “n” está etiquetado con un solo operador.
3. Las hojas están representadas por los operandos y los nodos por la raíz de
cada árbol.

1.2. Acciones semánticas de un analizador sintáctico

Un analizador sintáctico (o parser) es una de las partes de un compilador que


transforma su entrada en un árbol de derivación.
El análisis sintáctico convierte el texto 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 de sintaxis abstracta.

1.3. Comprobaciones de tipos en expresiones

Consenso
 Un conjunto de valores
 Un conjunto de operadores sobre los valores
 Las clases son una instanciación moderna de la noción de tipo.
Ciertas Operaciones son legales para cada tipo
 No tiene sentido sumar un apuntador a función y un entero en Java
 Tiene sentido sumar dos enteros

6
Instituto Tecnológico Superior de Misantla

Cada analizador semántico implementa un sistema de tipo (no necesariamente


tan robusto)

Comprobación de tipos puede ser dinámica y/0 estática.


 Estática: en tiempo de compilación.
 Dinámica: en tiempo de ejecución.

La comprobación ayuda a evitar la mayoría de los errores de programación.


Ayuda a saber si el operador aplicado a los operadores es correcto.
Hay situaciones en las cuales se tiene un valor de un tipo dado y se desea
almacenar ese valor en una variable de un tipo diferente.
En algunos tipos es posible almacenar simplemente el valor sin una conversión
de tipos; lo que se denomina conversión automática.
A la conversión de tipos se le llama coerción o casting.
Esto sólo es posible en algún lenguaje de programación, si el compilador
reconoce que la variable destino tiene la suficiente precisión para contener el valor
origen.
En Java se puede almacenar un valor byte en una variable int, dado que este
tipo de datos es de mayor precisión que el primero.
A esto se le llama ensanchamiento o promoción, dado que el tipo más pequeño
se ensancha o promociona al tipo compatible más grande. Si por el contrario, se desea
asignar un valor de variable int a una variable byte se necesita realizar una conversión
de tipos explícita.
En algunos casos se puede realizar la conversión pero se pueden perder datos,
como por ejemplo al pasar un valor flotante a un entero.
A esto se le llama estrechamiento, dado que se estrecha explícitamente el valor
para que quepa en el destino.
La conversión de un tipo se realiza poniendo delante un nombre de tipo entre
paréntesis, por ejemplo, (tipo) valor.
Ejemplos de coerción: byte a; int b; a=(byte) b;

7
Instituto Tecnológico Superior de Misantla

Al conjunto de reglas que se definen para la comprobación de los tipos de datos


se denomina sistema de tipos.
Se compone de:
 Tipo básico: entero, carácter, real, lógico
 Nombres de tipo
 Constructores de tipo: estructuras, uniones, objetos
 Apuntadores: referencias a tipos
 Funciones a=suma();

Los sistemas de tipos dependen de los lenguajes.


 char a[5];
 strcpy(a, “abcdefghijk”);

Java es un lenguaje con un sistema de tipos más fuerte que C/C++ que lo hace
más seguro pero a la vez un poco más deficiente.
Los lenguajes interpretados en el pasado eran más susceptibles a errores en
tiempo de ejecución pero las cosas han cambiado.
En general la tabla de símbolos debe contener la siguiente estructura para
validar la parte semántica:
Símbolo {
nombre;
tipo;
ámbito;
}

La mayoría de veces la recuperación de errores se suele omitir ya que el


programa no finaliza pero tal vez no obtenga los valores deseados.
Generalmente en la etapa de análisis sintáctico se suelen agregar los tipos a la
tabla de símbolos.
Se revisa el árbol sintáctico para comprobar los tipos asignados.

8
Instituto Tecnológico Superior de Misantla

Existen conversiones explícitas en las cuales el usuario indica el tipo de datos:


a = (int)(23.3/18.2);

Polimorfismo: una función puede tener el mismo nombre con diferentes


elementos. El tipo de datos debe ser diferente.
Un ejemplo de polimorfismo son las plantillas en algún lenguaje de
programación.
Se debe considerar el ámbito de las variables (locales y globales).

1.4. Pila semántica en un analizador sintáctico

La pila juega un papel fundamental en el desarrollo de cualquier analizador


semántico. Dentro de cada elemento de la pila se guardan los valores que pueden
tener una expresión.
1.5. Esquema de traducción

Los programas de aplicación, los videojuegos y otras herramientas que se


ejecutan en las computadoras, generalmente se realizan en lenguajes de alto nivel.
Estos programas escritos en lenguajes de alto nivel necesitan ser traducidos a
un lenguaje que sea entendible por la computadora, este lenguaje es el lenguaje de
bajo nivel.
Ensamblador

1.6. Generación de la tabla de símbolo y de direcciones

La tabla de símbolos también recibe el nombre de ambiente.

9
Instituto Tecnológico Superior de Misantla

 Simplifican el análisis sintáctico.


 Ayudan en la comprobaciones SEMÁNTICAS.
 Ayudan en la generación de código.

Esencialmente la información que aparece en la tabla de símbolos es de dos


tipos:
 El propio símbolo, y
 Los atributos necesarios para definir el símbolo a nivel semántico y
de generación de código.

Los atributos requeridos para cada símbolo depende a nivel general si:
 del tipo de gestión de memoria,
 el lenguaje está, o no, estructurado en bloques,
 el símbolo es, o no, parámetros de un procedimiento o función.

El analizador de léxico deberá:


 Insertar los símbolo detectados en la tabla de símbolos,
 Crear la tabla de símbolos parcialmente,
 Señalar la línea del programa fuente en donde aparecen.

El analizador semántico:
 Añadir los tipos, si procede, a los símbolos que aparecen en la
tabla de símbolos.

Cuando se hace una implantación de una única pasada, entonces los símbolos
son insertados y calificados, a nivel semántica y de generación de código, por el
analizador sintáctico y en el mismo instante que son detectados por el analizador léxico
y que son pasados al analizador sintáctico.
 INSERTAR
 CONSULTAR
 MODIFICAR (añadir atributos nuevos)

El CUANDO y el CÓMO se usan estas operaciones dependen del tipo de


lenguaje:
Lenguajes con DECLARACIONES DE VARIABLES:
Explícitas:
 Declaraciones: sólo INSERTAR.
 Referencia: sólo CONSULTAR.
Implícitas:
 CONSULTAR si no está ya incluida.
 INSERTAR, en caso contrario.
 Lenguajes con estructura de BLOQUE : CREAR
SUBTABLAS.
La distribución de la información de la tabla de símbolos dependerá de las
características del lenguaje y de las restricciones establecidas para los símbolos.

10
Instituto Tecnológico Superior de Misantla

a)Lenguajes SIN estructura de Bloque

b)Lenguajes CON estructura de Bloque


 Una tabla para cada Bloque con estructura de PILA.
 Estructura --> Consta de una PILA + Índice (que apunta al inicio de
las variables pertenecientes a cada bloque).

El índice nos muestra el comienzo y final de un bloque.


Además de insertar, consultar y modificar (completar la información de un
símbolo), cuando se trabaja con bloques, se realizan además las operaciones
siguientes:
 Set (Marca de Inicio de bloque).
 Reset (Marca de Fin de bloque).

1.7. Manejo de errores semánticos

Los errores pueden ser sintácticos, semánticos o lógicos.


El tipo de error más obvio es el sintáctico, que se produce cuando se escribe
código de una forma no admitida por las reglas del lenguaje. Los errores de sintaxis
son detectados casi siempre por el compilador o intérprete, que muestra un mensaje de

11
Instituto Tecnológico Superior de Misantla

error que informa del problema. En Visual Studio, estos mensajes de error aparecen en
la ventana de resultados. Los mensajes indican la ubicación del error de sintaxis
(número de línea y archivo), y muestran una breve descripción del problema.
Normalmente, la determinación de la causa de un error de sintaxis es un proceso
sencillo cuando se conocen estas descripciones. Para obtener más información, vea
Ventana de resultados.
Los errores semánticos son más sutiles. Un error semántico se produce cuando
la sintaxis del código es correcta, pero la semántica o significado no es el que se
pretendía. La construcción obedece las reglas del lenguaje, y por ello el compilador o
intérprete no detectan los errores semánticos. Los compiladores e intérpretes sólo se
ocupan de la estructura del código que se escribe, y no de su significado. Un error
semántico puede hacer que el programa termine de forma anormal, con o sin un
mensaje de error. Hablando en términos coloquiales, puede hacer que el equipo se
quede "colgado".
Sin embargo, no todos los errores semánticos se manifiestan de una forma tan
obvia. Un programa puede continuar en ejecución después de haberse producido
errores semánticos, pero su estado interno puede ser distinto del esperado. Quizá las
variables no contengan los datos correctos, o bien es posible que el programa siga un
camino distinto del pretendido. Eventualmente, la consecuencia será un resultado
incorrecto. Estos errores se denominan lógicos, ya que aunque el programa no se
bloquea, la lógica que representan contiene un error.
La única forma de detectar los errores lógicos es probar el programa, ya sea
manual o automáticamente, y comprobar que el resultado es el esperado. Para obtener
más información, vea Probar y optimizar. Las pruebas deben ser una parte integrante
del proceso de desarrollo de software. Desgraciadamente, aunque las pruebas pueden
indicar que el resultado de un programa es incorrecto, normalmente no proporcionarán
ninguna pista acerca de qué parte del código ha causado realmente el problema. Es
aquí donde comienza la depuración.
Normalmente, aunque no siempre, la depuración implicará el uso de un
depurador, una eficaz herramienta que permite observar el comportamiento del
programa en tiempo de ejecución y determinar la ubicación de los errores semánticos.
También es posible utilizar ciertas funciones de depuración integradas en el lenguaje y
sus bibliotecas asociadas. Muchos programadores tienen su primer contacto con la
depuración cuando intentan aislar un problema agregando al código llamadas a
funciones de salida, como printf o MsgBox. Esta técnica de depuración es
perfectamente legítima, pero hace que una vez encontrado y resuelto el problema sea
necesario volver a recorrer el código para eliminar las llamadas a funciones
adicionales. Ocasionalmente, puede encontrarse una situación en la que, al agregar
código nuevo ( incluso una simple llamada a printf o MsgBox), el comportamiento del
código que se intenta depurar cambia.
Con un depurador puede examinar el contenido de las variables del programa
sin tener que insertar llamadas adicionales a funciones que lo muestren. Puede insertar
un punto de interrupción en el código para detener la ejecución en el punto que le
interese. Cuando el programa esté detenido (en modo de interrupción), puede examinar
las variables locales u otros datos relevantes a través de medios tales como la ventana
Inspección, el cuadro de diálogo Inspección rápida y la ventana Memoria. Para obtener

12
Instituto Tecnológico Superior de Misantla

más información, vea Ventana Inspección, Cuadro de diálogo Inspección rápida o


Memoria (Ventana). En el modo de interrupción no sólo puede ver el contenido, si no
también modificarlo o cambiarlo, si lo desea. En la mayoría de los casos, el punto de
interrupción se establece en una ventana de código fuente de Visual Studio, en la que
se escribe y se modifica el código. En otras ocasiones puede encontrar más
conveniente establecer el punto de interrupción en la ventana Desensamblador. La
ventana Desensamblador muestra las instrucciones creadas a partir del código fuente.
Para obtener más información, vea Ventana Desensamblador. A diferencia de printf o
MsgBox, cuando se establece un punto de interrupción no se agrega una llamada
funcional al código fuente. Por ello, es poco probable que el establecimiento de un
punto de interrupción cambie el comportamiento del programa que se intenta depurar.
Para obtener más información, vea Interrumpir la ejecución.
Pero estas no son las únicas posibilidades que el depurador ofrece. Haga clic en
los elementos de Vea también para obtener más información.

Unidad 2. Generación de código intermedio

2.1 Lenguajes intermedios.

Códigos Intermedios (CI)


Se refiere a una estructura de código cuya complejidad está entre el código
fuente en el LAN y el código de máquina; por lo tanto los CI se pueden considerar
como una interfaz entre el generador de código y las fases previas del compilador.
Las ventajas de considerar un CI en la traducción es que podemos considerar
una máquina abstracta y con esto:
i.- El compilador es independiente de la máquina de destino, ya que las
condsideraciones particulares de la máquina destino están consideradas en el
generador de código.
ii.- Es fácil llevar a cabo una estratégia de optimización.
Desventaja: El código que puede generarse a partir del código intermedio por lo
general será menos eficiente que el código de máquina generado directamente, debido
al nivel de traducción adicional.

Lenguajes intermedios
Un lenguaje intermedio se puede definir como una manera de representar
procedimientos y estructuras de datos que sirva como entrada para una MV en alguna
parte de su jerarquía, entre el lenguaje de entrada (el nivel más alto) y el código
ejecutado en la máquina (el nivel más bajo) - tanto en el tiempo de compilación como
en el de ejecución (lo que se puede ver en la figura 1).
Para considerar el papel de los lenguajes intermedios y sus ventajas y
desventajas, conviene destacar la diferencia entre la traducción de un lenguaje de alto
nivel a código máquina anteriormente a su ejecución (su compilación) y su
interpretación, es decir, la conversión de cada instrucción del lenguaje a código

13
Instituto Tecnológico Superior de Misantla

máquina y su ejecución, una por una, al ejecutar el programa. Este proceso se realiza a
través de una MV de interpretación que simula un ordenador cuyo código máquina es
el lenguaje de alto nivel que está siendo interpretado. Y típicamente, esta MV se
construye a través de un conjunto de programas de código máquina que representa los
algoritmos y estructuras de datos necesarios para la ejecución de las instrucciones del
lenguaje de alto nivel. Hay ventajas y desventajas en cada manera de convertir los
lenguajes de alto nivel a código máquina, que se pueden resumir así:

Estos dos casos representan los dos extremos porque, como ya se ha visto,
existe también lo que se llama la compilación parcial, que es una mezcla de los dos
enfoques, donde se compila el lenguaje de alto nivel a un lenguaje intermedio (más
cerca de las estructuras presentes en el código máquina que las del código fuente) y
luego se interpreta este lenguaje al ejecutar el programa. Como puede imaginarse, esta
técnica combina las ventajas y desventajas de los dos enfoques anteriores. Un ejemplo
de esta combinación existe en el lenguaje de programación Java y su entorno.
Entre otras cosas, Java empezó con la idea de liberar al programador de las
dificultades de portar su aplicación a nuevas plataformas lo cual, si el programa está
muy vinculado a algún aspecto del sistema operativo donde fue escrito, podría ser muy
difícil. Se compilará el código fuente de Java a un código byte (bytecode) antes de
ejecutarlo. Y a la hora de correr el programa, este código, como lenguaje intermedio,
sería el lenguaje de entrada para una MV, que con un conjunto de librerías (el entorno
de ejecución de Java, Java Runtime o JRE), la interpretaría para su ejecución. Por lo
tanto, este bytecode podría correr en cualquier hardware donde haya una versión del
JRE disponible. Como este bytecode está más cerca del nivel de máquina que de un
lenguaje de alto nivel, los programas correrán más rápidamente que los programas
completamente interpretados, aunque más despacio que los programas previamente
compilados al código máquina.
Como se puede ver en la figura 1, tanto los programas compilados parcialmente
a un lenguaje intermedio (como Java) como los programas escritos en lenguajes de
alto nivel que se interpretan (como Lisp) requieren una MV para interpretar el
programa. La principal ventaja del lenguaje intermedio en este caso es su proximidad al
nivel del código máquina, en el sentido de que supone menos trabajo a la hora de
ejecutarlo y, por lo tanto, los programas corren más rápidamente que los puramente
interpretados.
Además del papel de los lenguajes intermedios en la compilación parcial, se
puede destacar su papel en la compilación estándar. Como ejemplo se puede
considerar C como lenguaje intermedio para un lenguaje compilado nuevo (como se

14
Instituto Tecnológico Superior de Misantla

planteó en la sección 1.5.1 para C++). Si el autor de un nuevo lenguaje decide utilizar
C, por ejemplo, como su lenguaje intermedio, sólo tendrá que implementar una MV
para convertir el código fuente de su lenguaje a C, ahorrando mucho trabajo. Además
de las razones dadas antes, las ventajas de utilizar un lenguaje tan establecido como C
como lenguaje intermedio son:
• La facilidad de portar el lenguaje a una nueva máquina (sólo hay que tener un
compilador C disponible allí).
• La generación de código máquina es una tarea muy compleja que requiere un
conocimiento profundo de la arquitectura de la máquina en cuestión – y de cada
máquina en que se quiere una versión del lenguaje.
• La facilidad de modificar algún rasgo del comportamiento del lenguaje en
alguna máquina en concreto (por ejemplo, características de memoria o rendimiento –
se pueden añadir librerías C customizadas sin grandes problemas).
• Las posibilidades disponibles para mapear estructuras intermedias del nuevo
lenguaje a estructuras de datos de C.
Y las desventajas son:
• La depuración es muy difícil porque, entre otras cosas, los errores que ocurren
en el código C no son muy fáciles de localizar en lo que ha escrito el programador
originalmente en el nuevo lenguaje.
• Las características de rendimiento y eficiencia del lenguaje están determinadas
por el compilador C.
• Habrá ocasiones en las que no exista una buena traducción entre una
estructura en el nuevo lenguaje y las estructuras de datos en C, por lo que habrá una
pérdida de eficiencia en el programa resultante (como, por ejemplo, ocurre en la
mayoría de las ocasiones en que se compilan estructuras de Prolog a C – sólo se
puede expresar iteración en Prolog utilizando recursión).

2.2 Notaciones

Notación infija: esta notación es mas adecuada para operaciones binarias, en


esta notación el símbolo operador se escribe entre dos operandos, la notación para
estas operaciones ha sido adaptada en lenguaje de programación. La forma infija para
el árbol de la figura anterior es (a+b)x(c-a).
Semántica para expresiones.
Cada una de las 3 notaciones prefija, posfija e infija, tiene cierto atributo que son
útiles en el diseño de lenguaje de programación. Difieren en la manera de calcular el
valor para cada expresión. Proporcionan algoritmos para evaluar cada formato de
expresión, para hacer más fácil la evaluación de expresiones.

NOTACION INFIJA
Es la forma mas común que utilizamos para escribir expresiones matemáticas,
estas notaciones se refiere a que el operador esta entre los operandos. La notación
infija puede estar completamente parentizada o puede basarse en un esquema de
precedencia de operadores así como el uso de paréntesis para invalidar los arreglos al
expresar el orden de evaluación de una expresión:

15
Instituto Tecnológico Superior de Misantla

3*4=12
3*4+2=14
3*(4+2)=18

Evaluación infija: Aunque estas notaciones común, su uso en un lenguaje de


programación conduce a varios problemas siguientes.
1.- Puesto que la notación infija se adecua solo para operadores binarios, es
necesario conbinarlo con otra notación ya sea prefija o posfija. La mezcla hace que la
traducción sea más complejas, los operadores unarios y las funciones con argumento
múltiples deben ser excepciones a la propiedad infija general.
2.- Cuando aparece mas de un operador infijo en una expresión, la notación en
inherentemente ambigua a menos que se utilicen paréntesis.
En este ultimo punto se demuestra que el valor de la expresión 2x3+4,la
expresión significa 10, pero con la misma facilidad puede ser 14. En la figura 3 (A) se
hace la multiplicación antes que la suma y el figura 3 (B) es la suma antes que la
multiplicación.
Se puede usar paréntesis para eliminar la ambigüedad de cualquier expresión
indicando explícitamente el agrupamiento de operadores y operandos, como en (AxB)
+C o Ax(b+C), pero la expresión complejas los nidos profundos de paréntesis que
resultan se vuelven confusas.

Notación sufija
Llamada también postfija o polaca inversa, se usa para representar expresiones
sin necesidad de paréntesis.
Ejemplos:
a*b ab*
a*(b+c/d) abcd/+*
a*b+c*d ab*cd*+
Los identificadores aparecen en el mismo orden. Los operadores en el de
evaluación (de izquierda a derecha).
Problema: operadores monádicos (unarios). O bien se transforman en diádicos
(binarios) o se cambia el símbolo.
Ejemplo: -a se convierte en 0-a o en @a
a*(-b+c/d) ab@cd/+*
Existen dos problemas principales:
 Construir la notación sufija a partir de la infija.
 Analizar la notación sufija en el segundo paso de la compilación.

Análisis de la notación sufija


La gramática completa que permite analizar la notación sufija es:
<Operando> ::= id |
cte |
<Operando> <Operando> <Operador diádico> |
<Operando> <Operador monádico>

16
Instituto Tecnológico Superior de Misantla

<Operador diádico> ::= + | - | * | / | ...


<Operador monádico> ::= @ | ...
Algoritmo de evaluación de una expresión en notación sufija que utiliza una pila:
 Si el próximo símbolo es un identificador, se pasa a la pila.
Corresponde a la aplicación de la regla
 <Operando> ::= id
 Si el próximo símbolo es una constante, se pasa a la pila.
Corresponde a la aplicación de la regla
 <Operando> ::= cte
 Si el próximo símbolo es un operador diádico, se aplica el operador
a los dos operandos situados en lo alto de la pila y se sustituyen éstos por el
resultado de la operación. Corresponde a la aplicación de la regla
 <Operando> ::= <Operando> <Operando> <Operador
diádico>
 Si el próximo símbolo es un operador monádico, se aplica el
operador al operando situado en lo alto de la pila y se sustituye éste por el
resultado de la operación. Corresponde a la aplicación de la regla
 <Operando> ::= <Operando> <Operador monádico>

Extensión de la notación sufija a otros operadores


 La asignación, teniendo en cuenta que podemos no querer valor
resultante. Además, no interesa tener en la pila el valor del identificador
izquierdo, sino su dirección.
 a:=b*c+d abc*d+:=
 La transferencia (GOTO).
 GOTO L L TR
 La instrucción condicional
 if p then inst1 else inst2
Se convierte en
p L1 TRZ inst1 L2 TR inst2
L1: L2:
 Subíndices:
 a[exp1; exp2; ...; expn]
Se convierte en
a exp1 exp2 ... expn SUBIN-n

Notación posfija(sufija o polaca inversa): Esta notación es similar a la


notación prefija excepto que el símbolo de operación sigue a la lista de operandos. Ej la
figura anterior se representa como ab+ca-x.

NOTACION POSFIJA
Como su nombre lo indica se refiere a que el operador ocupa la posición
después de los operandos sus características principales son: el orden de los
operandos se conserva igual que la expresión infija equivalente no utiliza paréntesis ya
que no es una operación ambigua.

17
Instituto Tecnológico Superior de Misantla

-La operación posfija no es exactamente lo inverso a la operación prefija equivalente:

(A+B)*C AB+C*

Evaluación posfija: en esta notación el operador sigue a sus operandos,


cuando se examina un operador sus operandos ya fueron evaluados. La evaluación de
una expresión pos fija P usando una pila, ahora tiene lugar, como sigue:
1.- Si el elemento siguiente de P es un operando, colocarlo en la pila.
2.- Si el elemento siguiente de P es un operador n-ario, entonces sus n
argumentos deben ser los n elementos superiores en la pila. Reemplazar estos n
elementos por el resultado de aclarar esta operación usando los n elementos como
argumento.
La evaluación es directa y fácil de implementar el generador de códigos usara el
algoritmo antes mencionado para determinar el orden del código de generación para
contar el valor de la expresión.

Notación prefija
Ordinaria * ( + ( a , b ) , - ( c , d ) )
Cambridge ( * (+ a b ) (- c d ) ) {Lenguaje Lisp}
Polaca * + a b - c d

Notación prefija(polaca prefija): Se escribe primero el símbolo de la operación


seguido de operandos en orden de izquierda a derecha, si un operando a su ves es
una operación con operandos. Se aplica el mismo criterio, en la figura anterior, el árbol
se convierte en x + a b - c a. Puesto que el signo + es un operador diádico, los
argumentos para + son a y b, y similar los argumentos para x son +y-, no se necesita
paréntesis para especificar como evaluar la expresión.
Una variante es el LISP(designada como polaca de Cambridge). En esta, un
operador y sus argumentos están rodeados por paréntesis. Una expresión se ve, por
tanto, como un conjunto anidado de lista, donde cada lista comienza con un símbolo de
operandos. En polaca de Cambridge la fig. anterior se convierte en:
(x(+ab)(-ca))
Considerando la formula de la figura 1 en notación prefija (usando ! para
exponenciación, " para sqrt y _ para menos unaria)
/+_b"-!b2xx4acx2a (polaca)
(/(+(_b)("(-(!b2)(x(x4a)c))))(x2a)) (polaca de Cambridge)

Evaluación prefija: Con notación prefija se puede evaluar cada expresión en un


solo examen, sin embargo es necesario conocer el numero de argumentos para cada
operación, por esto es necesario utilizar signos especiales para sustracciones
diadicas(-) y menos unarias(_), para distinguir las operaciones.
Además del ahorro de paréntesis, tiene un valor en el diseño de lenguaje de
programación.
1.- La llamada de función usual ya está escrita en notación prefija.

18
Instituto Tecnológico Superior de Misantla

2.- La notación prefija se puede usar para representar operaciones con cualquier
numero de operandos y es, por tanto, completamente general
3.- Es relativamente fácil decodificar mecánicamente; por esto se consigue con
facilidad expresiones prefijas a secuencias de código simple.

2.3 Representación de código intermedio.

Generación de código a partir de notación polaca


El algoritmo es el mismo que se utilizó en la unidad 2 para la evaluación de
expresiones en notación polaca, sustituyendo las evaluaciones (intérprete) por
generación de código (compilador).
Si el acumulador está en segundo lugar en la pila, al introducir un operando
nuevo en la pila hay que generar una instrucción que pase el acumulador a una
variable intermedia y sustituir el acumulador por el nombre de ésta. Con ello, un
operador diádico no tendría que preocuparse por el acumulador. Uno monádico tendría
que comprobar sólo si es el segundo operando de la pila.

GENERACION DE CODIGO DESDE RPN


Para la generación de código a partir de la RPN se utiliza el siguiente algoritmo
que consta de 2 pasos:
1. Se va analizando secuencialmente cada símbolo de entrada,
si es operando se apila y sigue con este mismo paso (pero con el símbolo
siguiente, etc.). Si el símbolo de entrada es un operador se va al paso 2.
2. Si el símbolo en cuestión es un operador se aplica a los dos
símbolos superiores de la pila. Así si se tenia en la pila A,B como los dos
elementos superiores y llegara el operador "+", entonces debemos
generar "LOAD A" seguido de "ADD B" y de STORE T.

Expresiones Aritméticas.

En la tabla 1 se da un ejemplo de aplicación de este algoritmo al caso de la


expresión aritmética "A*B+C-D*E" que en polaca es naturalmente "AB*C+DE*-".
Como el código que se obtiene es muy ineficiente por estarse continuamente
cargando el acumulador con una cantidad que ya tenía,etc, se tiene una variante del
algoritmo anterior que elimina las cargas innecesarias del acumulador, para ello se
introduce una metanoción ACC que refleja en todo momento el contenido del
acumulador. Si en algún momento se fuera a hundir ACC más de dos sitios, se tendría
que generar un Store del contenido del acumulador.
En la tabla 2 se repite la generación de código para la misma sentencia de la
tabla 1, pero con esta variante. En este ejemplo se reduce el número de instrucciones
de 11 a 9. Añadiendo la comprobación de operadores conmutativos se pueden
conseguir reducciones adicionales.

NOTACIÓN POLACA PREFIJA:


:= x + * a b * a b

19
Instituto Tecnológico Superior de Misantla

Puede ser generada por un recorrido pre-orden del árbol:

1. Procesa el nodo.
2. Procesa por el sucesor izquierdo.
3. Procesa por el sucesor derecho.

NOTACIÓN POLACA POSTFIJA:

 Los operadores se mandan directamente a un pila


 Cuando se lee un operador se retiene en la pila( pila de
operadores). Los operadores se van poniendo en esta pila sucesivamente
hasta encontrar uno con menor o igual prioridad que el de la cima, en cuyo
caso se se sacan los que hubieran en la pila de mayor o igual prioridad y se
coloca en ella este ultimo.
 El parentesis derecho no se coloca nunca en la pila y cuando
aparece hay que desapilar todos los operadores hasta llegar al parentesis
izquierdo y borrar este. Aunque el parentesis izquierdo tiene la mayor
prioridad solo se saca cuando aparece un parentesis derecho.

EJEMPLO:

x a b * a b * + :=

Puede ser generada por un recorrido post-orden del árbol:

1. Cada nodo es visitado después que sus subárboles.


2. Los sub-árboles del mismo padre son visitados de izquierda a derecha.

Código P (P-code)
El código-P (abreviatura de "código empaquetado") se genera para optimizar la
velocidad y el tamaño del código. El código-P puede reducir de forma significativa el
tamaño de un programa y la velocidad de ejecución en más de un 60 por lOO. Mejor
aún, todo esto se ejecuta simplemente cuando se activa una opción específica del
compilador Esto significa que cualquier código escrito en C o en C++ se puede
compilar normalmente o con código-P.
Esta tecnología compila un código fuente de una aplicación dentro del "código
objeto interpretado", que es una representación más condensada del código objeto y
con un nivel mayor El proceso finaliza cuando se enlaza un pequeño módulo intérprete
con la aplicación.
Sin embargo, la utilización eficiente de esta tecnología requiere cierta habilidad.
Como el intérprete genera código objeto en tiempo de ejecución, el código-P se ejecuta
más lentamente que el código objeto origen. Con una utilización muy cuidadosa de la

20
Instituto Tecnológico Superior de Misantla

directiva #pragma, una aplicación puede generar código-P para funciones críticas en
cuanto a espacio y pasar a generar código origen para funciones de velocidad crítica.
Las mejores candidatas para la generación de código-P son aquellas rutinas que
tratan con la interfaz de usuario, y como muchas aplicaciones Windows emplean el 50
por 100 de su tiempo generando la interfaz de usuario, el código-P proporciona las
características de rendimiento óptimas.

Tripletes
No se pone el resultado, se sustituye por referencias a tripletes. Por ejemplo: la
expresión a*b+c*d equivale a:
(1) (*,a,b)
(2) (*,c,d)
(3) (+,(1),(2))
mientras que a*b+1 equivale a:
(1) (*,a,b)
(2) (*,(1),1)
Tripletes indirectos: se numeran arbitrariamente los tripletes y se da el orden de
ejecución. Ejemplo, sean las instrucciones:
a := b*c
b := b*c
Equivalen a los tripletes
(1) (*,b,c)
(2) (:=,(1),a)
(3) (:=,(1),b)
y el orden de ejecución es (1),(2),(1),(3). Esta forma es útil para preparar la
optimización de código. Si hay que alterar el orden de las operaciones o eliminar
alguna, es más fácil hacerlo ahí.

Cuádruplas
Una operación diádica se puede representar mediante la cuádrupla
(<Operador>, <Operando1>, <Operando2>, <Resultado>)
Ejemplo:
(*,A,B,T)
Una expresión se puede representar mediante un conjunto de cuádruplas.
Ejemplo: la expresión a*b+c*d equivale a:
(*,a,b,t1)
(*,c,d,t2)
(+,t1,t2,t3)
Ejemplo: la expresión c:=a[i;b[j]] equivale a:
(*,i,d1,t1)
(+,t1,b[j],t2)
(:=,a[t2],,c)

Generación automática de cuádruplas

21
Instituto Tecnológico Superior de Misantla

En un análisis bottom-up, asociamos a cada símbolo no terminal una información


semántica, y a cada regla de producción una acción semántica. Ejemplo, sea la
gramática
E ::= E + T
E ::= E - T
E ::= T
T ::= T * F
T ::= T / F
T ::= F
F ::= i
F ::= (E)
F ::= -F
La regla F::=i asocia a F como información semántica el identificador concreto.
La regla F::=(E) asocia a F como información semántica la información
semántica asociada a E.
La regla U::=V asocia a U como información semántica la información semántica
asociada a V.
La regla U::=VoW analiza la compatibilidad de los operandos, crea la cuádrupla
(o,Sem(V),Sem(W),Ti) y asocia a U la información semántica Ti.
La regla U::=oV crea la cuádrupla (o,Sem(V),,Ti) y asocia a U la información
semántica Ti.
La información semántica se suele almacenar en otra pila paralela.
Ejemplo: análisis de a*(b+c)
Pila Entrada Regla Cuádrupla
----------- ------------- ------------ ---------
| a*(b+c)|
|a *(b+c)| F::=i
|F(a) *(b+c)| T::=F
|T(a) *(b+c)|
|T(a)* (b+c)|
|T(a)*( b+c)|
|T(a)*(b +c)| F::=i
|T(a)*(F(b) +c)| T::=F
|T(a)*(T(b) +c)| E::=T
|T(a)*(E(b) +c)|
|T(a)*(E(b)+ c)|
|T(a)*(E(b)+c )| F::=i
|T(a)*(E(b)+F(c) )| T::=F
|T(a)*(E(b)+T(c) )| E::=E+T
|T(a)*(E(b)+T(c) )| E::=E+T (+,b,c,T1)
|T(a)*(E(T1) )|
|T(a)*(E(T1)) | F::=(E)
|T(a)*F(T1) | T::=T*F (*,a,T1,T2)
|T(T2) | E::=T
|E(T2) |

22
Instituto Tecnológico Superior de Misantla

Ejemplo de generación de cuádruplas en análisis top-down


unsigned int E (char *cadena, unsigned int i)
{
if (i<0) return i;
switch (cadena[i]) {
case 'i':
push (id);
i++;
i = V (cadena, i);
break;
case '(':
i++;
i = E (cadena, i);
i = C (cadena, i);
i = V (cadena, i);
break;
default: return -1;
}
return i;
}

unsigned int V (char *cadena, unsigned int i)


{
unsigned int j;
if (i<0) return i;
switch (cadena[i]) {
case '*':
case '/':
j = i;
i++;
i = T (cadena, i);
cuad (cadena[j], pop(), pop(), gen(Ti));
push (Ti);
i = X (cadena, i);
break;
case '+':
case '-':
j = i;
i++;
i = E (cadena, i);
cuad (cadena[j], pop(), pop(), gen(Ti));
push (Ti);
break;
}
return i;
}

23
Instituto Tecnológico Superior de Misantla

unsigned int X (char *cadena, unsigned int i)


{
unsigned int j;
if (i<0) return i;
switch (cadena[i]) {
case '+':
case '-':
j = i;
i++;
i = E (cadena, i);
cuad (cadena[j], pop(), pop(), gen(Ti));
push (Ti);
break;
}
return i;
}

unsigned int T (char *cadena, unsigned int i)


{
if (i<0) return i;
switch (cadena[i]) {
case 'i':
push (id);
i++;
i = U (cadena, i);
break;
case '(':
i++;
i = E (cadena, i);
i = C (cadena, i);
i = U (cadena, i);
break;
default: return -2;
}
return i;
}

unsigned int U (char *cadena, unsigned int i)


{
if (i<0) return i;
unsigned int j;
switch (cadena[i]) {
case '*':
case '/':
j = i;

24
Instituto Tecnológico Superior de Misantla

i++;
i = T (cadena, i);
cuad (cadena[j], pop(), pop(), gen(Ti));
push (Ti);
break;
}
return i;
}

unsigned int F (char *cadena, unsigned int i)


{
if (i<0) return i;
switch (cadena[i]) {
case 'i':
push (id);
i++;
break;
case '(':
i++;
i = E (cadena, i);
i = C (cadena, i);
break;
default: return -3;
}
return i;
}

unsigned int C (char *cadena, unsigned int i)


{
if (i<0) return i;
switch (cadena[i]) {
case ')':
i++;
break;
default: return -4;
}
return i;
}

Generación a partir de cuádruplas


Para funciones diádicas conmutativas (suma, producto):
SUMA (opd *x, opd *y, opd *z)
{
if (CAC (x, y)) GEN ("ADD AC,",x);
else GEN ("ADD AC,",y);
AC=z;

25
Instituto Tecnológico Superior de Misantla

}
(+, O1, O2, R) se traduce por SUMA (O1, O2, R).
La multiplicación se haría igual, sustituyendo "ADD" por "MUL".
Para funciones diádicas no conmutativas:
RESTA (opd *x, opd *y, opd *z)
{
CAC (x, NULL);
GEN ("SUB AC,",y);
AC=z;
}
(-, O1, O2, R) se traduce por RESTA (O1, O2, R).
La división se haría igual, sustituyendo "SUB" por "DIV".
Para funciones monádicas:
NEG (opd *x, opd *z)
{
CAC (x, NULL);
GEN ("NEG AC");
AC=z;
}
(@, O1,, R) se traduce por NEG (O1, R).
Ejemplo: sea la expresión A*((A*B+C)-C*D)
Las cuádruplas son:
Cuádrupla Se genera Valor de AC
------------ ---------- -----------
(*,A,B,T1) MOV AC,A A
MUL AC,B T1
(+,T1,C,T2) ADD AC,C T2
(*,C,D,T3) MOV T2,AC
MOV AC,C C
MUL AC,D T3
(-,T2,T3,T4) MOV T3,AC
MOV AC,T2 T2
SUB AC,T3 T4
(*,A,T4,T5) MUL AC,A T5

Generación de código para la asignación


Sea la cuádrupla
(=,B,,A)
En general, habrá que generar
MOV AC,B
MOV A,AC
En algunas máquinas existe una instrucción que permite pasar de memoria a
memoria, tal como
MOV A,B
También puede ser que, al generar esta instruccción, nos encontremos con que
el acumulador contiene B (AC="B"), en cuyo caso bastaría con generar

26
Instituto Tecnológico Superior de Misantla

MOV A,AC
La asignación puede llevar implícitas diversas conversiones de tipo, que pueden
haber sido tratadas previamente por el analizador semántico.
También puede haber conversión implícita de la profundidad de los punteros,
como en
ref int A;
int B;
...
B := A;
En este caso, el código generado debería ser:
MOV índice,A
MOV AC,[índice]
MOV B,AC

2.4 Esquemas de generación.

Sintaxis de declaración de variables


La declaración de una variable es una sentencia simple con la siguiente sintáxis:
tipoVariable nombreVariable;
De esta forma declaramos que la variable nombreVariable es de tipo
tipoVariable. El siguiente ejemplo declara una variable de tipo int (entero):
int contador;
También es posible dar un valor inicial a la variable declarada:
int contador = 2;
Como habrás adivinado esta sentencia declara la variable contador y le da un
valor inicial de 3.
A veces en necesario declarar varias variables de un mismo tipo. Existe una
forma abreviada de declaración de variables que permite hacer esto en una sola
sentencia:
int x,y,z;
El anterior ejemplo declara tres variables (x, y, z), todas de tipo entero (int).
Usando esta forma abreviada también es posible dar un valor inicial a las variables:
int x=0,y=1,z=23;

Constantes
def: Una constante es un dato cuyo valor no puede cambiar durante la
ejecución del programa. Recibe un valor en el momento de la compilación y este
permanece inalterado durante todo el programa.
Como ya se ha comentado en el tema sobre las partes de un programa, las
constantes se declaran en una sección que comienza con la palabra reservada const.
Después de declarar una constante ya puedes usarla en el cuerpo principal del
programa. Tienen varios usos: ser miembro en una expresion, en una comparación,
asignar su valor a una variable, etc.

27
Instituto Tecnológico Superior de Misantla

En este ejemplo se declaran tres constantes (Min, Max y Sep). En la primera


línea del cuerpo del programa se asigna una constante a una variable. En la siguiente,
se usa una constante en una comparación. Y en la cuarta, la constante Sep interviene
en una expresión que se asigna a una variable. El resultado de ejecutar este programa
sería una impresión en pantalla de los números: 0, 10, 20, 30, 40, 50, 60, 70, 80 y 90.

Se puede hacer una división de las constantes en tres clases:


 constantes literales (sin nombre)
 constantes declaradas (con nombre)
 constantes expresión
Constantes literales
Son valores de cualquier tipo que se utilizan directamente, no se declaran ya que no
tienen nombre. En el siguiente ejemplo tienes un par de constantes literales (el 3, el 4,
y el 3.1416):
VolumenEsfera := 4/3 * 3.1416 * Radio * Radio *
Radio;
Constantes declaradas

También llamadas constantes con nombre, son las que se declaran en la sección
const asignándoles un valor directamente. Por ejemplo:
const
Pi = 3.141592; (* valor real *)

Min = 0; (* entero *)
Max = 99; (* entero *)
Saludo = 'Hola'; (* cadena caract. *)

Constantes expresión

También se declaran en la sección const, pero a estas no se les asigna un valor


directamente, sino que se les asigna una expresión. Esta expresión se evalúa en tiempo de
compilación y el resultado se le asigna a la constante. Ejemplo:
const
Min = 0;
Max = 100;
Intervalo = 10;
N = (Max - Min) div Intervalo;
Centro = (Max - Min) div 2;

Variables
def: Una variable es un nombre asociado a un elemento de datos que está
situado en posiciones contiguas de la memoria principal, y su valor puede cambiar
durante la ejecución de un programa.
Toda variable pertenece a un tipo de dato concreto. En la declaración de una
variable se debe indicar el tipo al que pertenece. Así tendremos variables enteras,

28
Instituto Tecnológico Superior de Misantla

reales, booleanas, etc. Por otro lado, distinguimos tres partes fundamentales en la vida
de una variable:
 Declaración
 Iniciación
 Utilización
Declaración de variables

Esta es la primera fase en la vida de cualquier variable. La declaración se realiza en la


sección que comienza con la palabra var. Si quieres más información, puedes ir al
apartado que trata sobre la declaración de variables en el tema Estructura de un
programa.
Nota: Toda variable que vaya a ser utilizada en Pascal tiene que ser
previamente declarada.
Iniciación de variables

Esto no es más que darle un valor inicial a una variable. Así como lo primero que se
hace con una variable es declararla, lo siguiente tiene que ser iniciarla. Esto se hace
para evitar posibles errores en tiempo de ejecución, pues una variable tiene un valor
indeterminado después de declararla. Principalmente, existen dos maneras de otorgar
valores iniciales a variables:
 Mediante una sentencia de asignación
 Mediante uno de los procedimientos de entrada de datos (read o
readln)
Veamos un ejemplo que reúne los dos casos:
begin
...
i:=1;
readln(n);
while i < n do begin
(* cuerpo del bucle *)
i := i + 1
end;
...
end.

Utilización de variables

Una vez declarada e iniciada una variable, es el momento de utilizarla. Esta es la parte
que presenta un mayor abanico de posibilidades. A continuación tienes unas cuantas:
 Incrementar su valor:
i := i + 1
 Controlar un bucle:
for i:=1 to 10 do ...
 Chequear una condición:
if i<10 then ...

29
Instituto Tecnológico Superior de Misantla

 Participar en una expresión:


n := (Max - Min) div i
 Y otras que ya irás descubriendo

Diferencias entre ambas


Después de haber visto cada una por separado, pasemos a explicar las
diferencias que existen entre constantes y variables. A primera vista pudieran parecer
conceptos similares, pero realmente son cosas muy distintas. Y esta distinción viene
dada por la posibilidad que tienen las variables para cambiar de valor.
Principales diferencias:
 Las constantes ya reciben un valor inicial en su declaración
 Las variables primero se declaran, luego se inician, y luego se usan
 Las constantes, una vez declaradas mantienen su valor durante
toda la ejecución del programa
 En cambio, las variables pueden cambiar su valor tantas veces
como deseen
 Además de cambiar su valor, las variables también pueden cambiar
de tamaño en tiempo de ejecución (punteros)
Nota: En el último punto se alude a las variables que cambian su tamaño. Como
curiosidad saber que se denominan variables dinámicas o punteros, pero debido a
su complejidad no son objeto de este tutorial. Todas las demás variables son estáticas
y se tratarán las más significativas.
Ejemplo
A continuación tienes un ejemplo con el que puedes interactuar para que repases
algunos de los conceptos que se han tratado en este tema. En él puedes observar
cómo se encuentran los valores de dos variables y una constante en varios puntos del
programa.

Nota: Debido a la última sentencia, el ejemplo no compilaría. Pero se incluye


para que veas que no puedes cambiar el valor de una constante.

La sección de declaración de variables


 La sección de declaración de variables tiene la forma:
Sintaxis:
VAR
lista-variables-1: tipo-1;
lista-variables-2: tipo-2;

lista-variables-n: tipo-n;

donde:
 VAR es una palabra reservada;
 cada lista-variables-i es una única variable o
una lista de variables separadas por comas;

30
Instituto Tecnológico Superior de Misantla

 cada tipo-i es uno de los tipos de datos de


Pascal integer, real, char, boolean (o uno de los tipos de
datos discutidos más adelante).
Objetivo:
Declara los tipos de las variables que se van a usar en el
programa. Cada variable en lista-variables-i se declara de tipo
tipo-i.
Objetos de datos
Una variable es un objeto de datos con un nombre, un tipo y un valor asociado
que puede modificarse libremente durante la ejecución de la parte de un programa en
la que es visible (ámbito). Una declaración de variables consta de: (1) una lista de
nombres de las variables que se declaran separadas por comas (",") y terminada con
dos puntos (":"), (2) el tipo de las variables y, opcionalmente, (3) la asignación de un
valor inicial.
X: Float;
Y, Z: Integer := 0;

Una constante es un objeto de datos con un nombre, un tipo y un valor asociado


que no puede modificarse una vez definido. Una declaración de constantes es igual
que una declaración de variables excepto en que se precede al tipo con la palabra
constant .

C, K: constant Integer := 0;

En la parte visible de un paquete se puede hacer una declaración de constantes


sin especificar el valor (deferred declaration, declaración diferida) siempre que en la
parte private del mismo paquete se haga una nueva declaración (full declaration) para
definirlo.

package Ejemplo is
C: constant Integer; -- declaración diferida
...
private
C: constant Integer := 0; -- declaración completa
...
end Ejemplo;

Ada contempla además una clase de constantes que sirve para asociar un
nombre para identificar un valor numérico y que no requiere la especificación de un
tipo.

Pi: constant := 3.1416;

Los objetos de datos creados mediante declaraciones escritas en el código son


variables que se crean cuando se elabora la declaración al iniciarse la ejecución de la
región donde se encuentra y se destruyen cuando se termina dicha ejecución. Se

31
Instituto Tecnológico Superior de Misantla

pueden crear variables de forma dinámica en cualquier momento de la ejecución de un


programa usando la palabra reservada new acompañada del tipo de la variable que se
pretende crear; como resultado se devuelve la dirección del bloque de memoria
asociado a la variable que se crea, esta dirección puede almacenarse en una variable
puntero (access) para poder acceder posteriormente a la variable dinámica.

P := new Integer; --P debe ser un puntero (access) a variables de tipo Integer.

Las variables dinámicas también se pueden crear con un valor inicial.

P := new Integer'(35); --la variable dinámica se crea con el valor 35.

En Ada, una variable dinámica se libera automáticamente cuando finaliza la


ejecución de la parte del programa donde está definido el tipo puntero que permite
referenciarla. Para realizar un control más eficiente de la memoria es necesario
emplear operaciones de liberación explícita de las variables creadas dinámicamente
cuando ya no sean necesarias; se necesita instanciar el procedimiento genérico
"Unchecked_Deallocation" para lo que se debe incluir en la cláusula de contexto. La
instanciación posterior tendrá que indicar el tipo de las variables a liberar y el tipo de
puntero que se usa para referenciarlas.

--se incluye "Unchecked_Deallocation" en la cláusula de contexto


with Unchecked_Deallocation;
...
--se instancia dentro de la unidad de programa
--se supone que P_Integer ha sido declarado como "access Integer"
procedure Liberar_Integer is new Unchecked_Deallocation(Integer,P_Integer);
...
--ahora se puede usar Liberar_Integer para liberar variables dinámicas Integer
--Suponiendo que P es de tipo P_Integer y contiene la dirección de una
--variable Integer dinámica creada previamente:
Liberar_Integer(P); --Libera la variable dinámica referenciada por P

La sección de declaración de constantes


 La sección de declaración de constantes tiene la forma:
Sintaxis:
CONST
identificador-1= constante-1;
identificador-2= constante-2;

identificador-n= constante-n;

donde:

32
Instituto Tecnológico Superior de Misantla

 CONST es una palabra reservada;


 cada identificador-i es un identificador válido
de Pascal.
Objetivo:
Asigna nombres a las constantes; cada identificador-i puede
usarse en el programa allí donde la constante-i asociada pueda
usarse.

Arreglos unidimensionales y multidimensionales


Los arreglos son una colección de variables del mismo tipo que se referencian
utilizando un nombre común. Un arreglo consta de posiciones de memoria contigua. La
dirección más baja corresponde al primer elemento y la más alta al último. Un arreglo
puede tener una o varias dimensiones. Para acceder a un elemento en particular de un
arreglo se usa un índice.
El formato para declarar un arreglo unidimensional es:
tipo nombre_arr [ tamaño ]
Por ejemplo, para declarar un arreglo de enteros llamado listanum con diez
elementos se hace de la siguiente forma:
int listanum[10];
En C, todos los arreglos usan cero como índice para el primer elemento. Por
tanto, el ejemplo anterior declara un arreglo de enteros con diez elementos desde
listanum[0] hasta listanum[9].
La forma como pueden ser accesados los elementos de un arreglo, es de la
siguiente forma:
listanum[2] = 15; /* Asigna 15 al 3er elemento del arreglo listanum*/
num = listanum[2]; /* Asigna el contenido del 3er elemento a la variable num */
El lenguaje C no realiza comprobación de contornos en los arreglos. En el caso
de que sobrepase el final durante una operación de asignación, entonces se asignarán
valores a otra variable o a un trozo del código, esto es, si se dimensiona un arreglo de
tamaño N, se puede referenciar el arreglo por encima de N sin provocar ningún
mensaje de error en tiempo de compilación o ejecución, incluso aunque probablemente
se provoque el fallo del programa. Como programador se es responsable de asegurar
que todos los arreglos sean lo suficientemente grandes para guardar lo que pondrá en
ellos el programa.
C permite arreglos con más de una dimensión , el formato general es:
tipo nombre_arr [ tam1 ][ tam2 ] ... [ tamN];
Por ejemplo un arreglo de enteros bidimensionales se escribirá como:
int tabladenums[50][50];
Observar que para declarar cada dimensión lleva sus propios paréntesis
cuadrados.
Para acceder los elementos se procede de forma similar al ejemplo del arreglo
unidimensional, esto es,
tabladenums[2][3] = 15; /* Asigna 15 al elemento de la 3ª fila y la 4ª columna*/
num = tabladenums[25][16];
A continuación se muestra un ejemplo que asigna al primer elemento de un
arreglo bidimensional cero, al siguiente 1, y así sucesivamente.

33
Instituto Tecnológico Superior de Misantla

main()
{
int t,i,num[3][4];

for(t=0; t<3; ++t)


for(i=0; i<4; ++i)
num[t][i]=(t*4)+i*1;

for(t=0; t<3; ++t)


{
for(i=0; i<4; ++i)
printf("num[%d][%d]=%d ", t,i,num[t][i]);
printf("\n");
}

}
En C se permite la inicialización de arreglos, debiendo seguir el siguiente
formato:
tipo nombre_arr[ tam1 ][ tam2 ] ... [ tamN] = {lista-valores};
Por ejemplo:
int i[10] = {1,2,3,4,5,6,7,8,9,10};
int num[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};

Arreglos
Un arreglo es una lista ordenada de datos escalares. Los arreglos pueden tener
cualquier número de elementos. Los elementos de un arreglo son escalares que
empiezan por $, cuyos subíndices empiezan por 0.

Representación
Un arreglo es una lista de valores separados por coma, y dentro de paréntesis.
Los valores de los elementos pueden ser de distinto tipo.

@num = (1,2,3);

@var = ("juan",4.5);

Sus valores no necesariamente tienen que ser constantes, puede tener


variables.

@num = ($a,17);

@var = ($b+$c,$d+$e);

El arreglo vacío es representado por ().

34
Instituto Tecnológico Superior de Misantla

Acceso a los elementos


Para enumerar los elementos de un arreglo se empieza desde el 0. El primer
elemento del arreglo @a es $a[0]; así, para accesar un elemento específico del arreglo
ya no se emplea @.

@a=(7,8,9);
$b=$a[0]; # asigna 7 a $b
$a[1]=5; # ahora @a es (7,5,9)
$a[2]++; # suma uno al tercer valor de @a siendo ahora (7,5,10)
$c=$a[0]+$a[1]; # asigna 12 a $c

Los índices de los arreglos también pueden ser variables:

@juan=(7,8,9);
$a=2;
$b=$juan[$a]; # $b = 9
$c=$juan[$a-1]; # $c = 8

Si se accesa un elemento del arreglo mas allá del fin del arreglo el valor undef es
retornado.

@a=(1,2,3);
$b=$a[7]; # $b = 0 o "" , dependiendo del contexto

Así mismo, asignar un valor a un elemento más alla del fin del arreglo, agranda
el arreglo asignando undef a los valores intermedios.

@a=(1,2,3);
$a[3]="hola"; # @a es ahora (1,2,3,"hola")
$a[6]="chao"; # @a es ahora (1,2,3,"hola",undef,undef,"chao")

El largo (número de posiciones - 1) de un arreglo ( ej : @a ) está almacenado en


$#a.

@a=('b',3,"a"); # $#a = 2
$d=$a[$#a]; # $d='a'
$c=$#a+5; # $c=7

Operaciones sobre arreglos

 push() y pop()

35
Instituto Tecnológico Superior de Misantla

push() es utilizado para agregar y pop() para sacar elementos por el lado
derecho del arreglo.

@lista=(1,2,3);
push(@lista,5); # @lista=(1,2,3,5)
$a=pop(@lista); # $a=5, @a=(1,2,3)

 shift() y unshift()
Al igual que pop() y push(), éstos extraen e insertan elementos de un
arreglo, pero lo hacen por el lado izquierdo.

@a=(1,2,3);
unshift(@a,0); # @a=(0,1,2,3);
$x=shift(@a); # $x=0 , @a=(1,2,3)
unshift(@a,5,6,7) # @a=(5,6,7,1,2,3)

 reverse()
reverse() invierte el orden de los elementos de un arreglo, retornando la
lista invertida.

@a=(1,2,3);
@b=reverse(@a); # @b = (3,2,1)
@c=reverse(1,2,3); # @c = (3,2,1)

La lista que se pasa como parámetro no es alterada, si se desea alterar


debemos almacenar el resultado en la misma lista:

@a=reverse(@a) ;

 chop()
chop() le saca el último caracter a cada elemento de un arreglo.

@a=("hola","mundo\n","felices dias");
chop(@a); # @a=("hol","mundo","felices dia")

 join()
join() combina los elementos de una lista y retorna un escalar donde cada
elemento está separado por el valor que se haya pasado como parámetro.

@campos=(1,2,"hola");
$salida=join(".",@campos); # $salida="1.2.hola"

36
Instituto Tecnológico Superior de Misantla

 split()
split() usa una expresión regular para dividir un escalar u otra expresión
de cadena para analizar una cadena, retorna una lista o arreglo.

$linea="mvargas::117:10:Juan";
@a=split(/:/,$line); # @a=("mvargas","","117","10","Juan")

 grep()
grep() funciona igual que la orden grep de Unix, excepto que opera sobre
una lista y no sobre un archivo.

@a = ("a1", "a2", "b1", "b2", "c1", "c2");


@b = grep /^b/ , @a; # devuelve un subarreglo de @a que contiene
los elementos
# donde la expresión es verdadera...
# en este caso los que empiecen por "b"
print "@b"; # imprime: b1 b2

Operaciones raras

@a=(1,2,3,4,5);
($b,$c,$d)=@a; # b=1 c=2 d=3
@a=(1,'b',2,'c');
($b,@a)=@a; # b=1 a=('b',2,'c')
@a=('hola','tu');
@b=(1,2,@a,7,8); # b=(1,2,'hola','tu',7,8)
@a=(1,2,3,4,5);
@b=@a[1,3]; # b=(2,3,4)
@b=(1,2,3,4,6)[0,1]; # b=(1,2)
@a=(7,8,9);
@b=(1,2,0);
@c=@a[@b]; # c=(8,9,7)

Ejemplo 3-6. Demostración de arreglos

#!/usr/bin/perl
# Ejemplo 6 : Demostración de arreglos

print "Ejemplo 6\n";

37
Instituto Tecnológico Superior de Misantla

print "Uso de push() y pop()\n";


@lista=(1,2,3);
print "@lista\n";

push(@lista,5); # @lista=(1,2,3,5)
print "@lista\n";

$a=pop(@lista); # $a=5, @a=(1,2,3)


print "@lista\n";
print "a = $a\n";

print "Uso de reverse()\n";


@a=(1,2,3);
@b=reverse(@a); # @b = (3,2,1)

print "@a\n";
print "@b\n";

print "Uso de chop()\n";


@a=("hola","mundo\n","felices dias");
print "@a\n";

chop(@a); # @a=("hol","mundo","felices dia")


print "@a\n";

Funciones
Una función es un conjunto de declaraciones, definiciones, expresiones y
sentencias que realizan una tarea específica.
El formato general de una función en C es
especificador_de_tipo nombre_de_función( lista_de_parámetros )
{
variables locales
código de la función
}
El especificador_de_tipo indica el tipo del valor que la función devolverá
mediante el uso de return. El valor puede ser de cualquier tipo válido. Si no se
específica un valor, entonces la computadora asume por defecto que la función
devolverá un resultado entero. No se tienen siempre que incluir parámetros en una
función. la lista de parámetros puede estar vacía.
Las funciones terminan y regresan automáticamente al procedimiento que las
llamó cuando se encuentra la última llave }, o bien, se puede forzar el regreso antes

38
Instituto Tecnológico Superior de Misantla

usando la sentencia return. Ademas del uso señalado la función return se usa para
devolver un valor.
Se examina a continuación un ejemplo que encuentra el promedio de dos
enteros:
float encontprom(int num1, int num2)
{
float promedio;

promedio = (num1 + num2) / 2.0;


return(promedio);
}

main()
{
int a=7, b=10;
float resultado;

resultado = encontprom(a, b);


printf("Promedio=%f\n",resultado);
}

Funciones void
Las funciones void dan una forma de emular, lo que en otros lenguajes se
conocen como procedimientos (por ejemplo, en PASCAL). Se usan cuando no requiere
regresar un valor. Se muestra un ejemplo que imprime los cuadrados de ciertos
números.
void cuadrados()
{
int contador;

for( contador=1; contador<10; contador++)


printf("%d\n",contador*contador);
}

main()
{
cuadrados();
}
En la función cuadrados no esta definido ningún parámetro, y por otra parte
tampoco se emplea la sentencia return para regresar de la función.

Funciones y arreglos
Cuando se usan un arreglo como un argumento a la función, se pasa sólo la
dirección del arreglo y no la copia del arreglo entero. Para fines prácticos podemos
considerar el nombre del arreglo sin ningún índice como la dirección del arreglo.

39
Instituto Tecnológico Superior de Misantla

Considerar el siguiente ejemplo en donde se pasa un arreglo a la función


imp_rev, observar que no es necesario especificar la dimensión del arreglo cuando es
un parámetro de la función.
void imp_rev(char s[])
{
int t;

for( t=strlen(s)-1; t>=0; t--)


printf("%c",s[t]);
}

main()
{
char nombre[]="Facultad";

imp_rev(nombre);
}
Observar que en la función imp_rev se usa la función strlen para calcular la
longitud de la cadena sin incluir el terminador nulo. Por otra parte, la función imp_rev no
usa la sentencia return ni para terminar de usar la función, ni para regresar algún valor.
Se muestra otro ejemplo,
float enconprom(int tam, float lista[])
{
int i;
float suma = 0.0;

for ( i=0; i<tam; i++)


suma += lista[i];
return(suma/tam);
}

main()
{
float numeros[]={2.3, 8.0, 15.0, 20.2, 44.01, -3.0, -2.9};

printf("El promedio de la lista es %f\n", enconprom(7,numeros) );


}
Para el caso de que se tenga que pasar un arreglo con más de una dimensión,
no se indica la primera dimensión pero, el resto de las dimensiones deben señalarse.
Se muestra a continuación un ejemplo:
void imprtabla(int tamx,int tamy, float tabla[][5])
{
int x,y;

for ( x=0; x<tamx; x++ )


{

40
Instituto Tecnológico Superior de Misantla

for ( y=0; y<tamy; y++ )


printf("t[%d][%d]=%f",x,y,tabla[x][y]);
printf("\n");
}
}

Prototipos de funciones
Antes de usar una función C debe tener conocimiento acerca del tipo de dato
que regresará y el tipo de los parámetros que la función espera.
El estándar ANSI de C introdujó una nueva (mejor) forma de hacer lo anterior
respecto a las versiones previas de C.
La importancia de usar prototipos de funciones es la siguiente:
 Se hace el código más estructurado y por lo tanto, más fácil de leer.
 Se permite al compilador de C revisar la sintaxis de las funciones
llamadas.
Lo anterior es hecho, dependiendo del alcance de la función. Básicamente si una
función ha sido definida antes de que sea usada (o llamada), entonces se puede usar
la función sin problemas.
Si no es así, entonces la función se debe declarar. La declaración simplemente
maneja el tipo de dato que la función regresa y el tipo de par^o'ametros usados por la
función.
Es una práctica usual y conveniente escribir el prototipo de todas las funciones al
principio del programa, sin embargo esto no es estrictamente necesario.
Para declarar un prototipo de una función se indicará el tipo de dato que
regresará la función, el nombre de la función y entre paréntesis la lista del tipo de los
parámetros de acuerdo al orden que aparecen en la definición de la función. Por
ejemplo:
int longcad(char []);
Lo anterior declara una función llamada longcad que regresa un valor entero y
acepta una cadena como parámetro.

Ejercicios
1. Escribir una función ``reemplaza'', la cual toma una cadena como
parámetro, le reemplaza todos los espacios de la cadena por un guión bajo, y
devuelve el número de espacios reemplazados. Por ejemplo:
2. char cadena[] = "El gato negro";
3. n = reemplaza( cadena );
deberá devolver:
cadena convertida "El_gato_negro"
n=2
4. Escribir un programa que lea una línea de texto en un buffer (una
cadena de caracteres) usando la función gets y calcule la longitud de la línea
(NO usar la función strlen).
5. Modificar el programa anterior para que lea un archivo de texto. El
archivo deberá redireccionarse al programa, debiendo mostrar el contenido del

41
Instituto Tecnológico Superior de Misantla

mismo. En caso de que se lea una línea con longitud 0 deberá terminar el
programa.

Unidad 3. Optimizacion

3.1. Tipos de optimización.

OPTIMIZADORES INDEPENDIENTES DE LA MÁQUINA


Optimización por reducción simple.-Se realizan sobre los elementos de una
expresión aritmética cuando todos ellos son conocidos.
Ejemplo:
(3+5)*8
8 * 8 = 64

Optimización por reacondicionamiento de interrupciones.- En ocasiones es


posible reestructurar una expresión dada de forma que se evite en lo posible el uso de
variables temporales. De las expresiones aritméticas en muchas ocasiones basta de
disponer de otra forma el acomodo de los elementos para dar con el objetivo deseado.
Ejemplo:
A:=B*C*(D+E)
A:=(D+E)*B*C

Optimización por reducción de potencia.-Este modelo consiste en eliminar


operaciones completas cambiándolas por operaciones de más fácil realización, que
utilicen menos recursos y se resuelvan en menos tiempo. De esta forma una potencia
puede ser remplazada por una serie de multiplicaciones.
Ejemplo:
X³ = (X) (X) (X)

42
Instituto Tecnológico Superior de Misantla

Optimización por reducción de frecuencia.- La repetición innecesaria para la


lógica de programación de una serie de instrucciones es una circunstancia bastante
común entre programadores de poca experiencia. Un buen optimizador de código
deberá de desplazar este tipo de operaciones hacia lugares del programa donde su
ejecución sea lo menos reiterada posible.
Ejemplo:

For ( i=0 ; i<10 ; i++ )


{
printf ( " %d ", i);
if (x > y)
{
printf (" x es mayor que y ");
}
}

OPTIMIZADORES DEPENDIENTES DE LA ARQUITECTURA DE LA


COMPUTADORA

Este tipo de optimizadores utiliza para su realización recursos de la estructura


interna de la arquitectura de la computadora. Considera el uso de operaciones y
registros que agilicen el procesamiento de alguna expresión aritmética.
Habitualmente los compiladores permiten introducir las optimizaciones de
código como opciones de compilación. Las opciones habituales son velocidad de
ejecución y tamaño del ejecutable. Ambas optimizaciones son opuestas, es decir si se
quiere mejorar la velocidad suele implicar incrementar el tamaño del ejecutable.

Los optimizadores se pueden plantear como:

 Optimizadotes locales. Son las que se realizan secuencialmentre


dentro de un bloque de código. Son las más sencillas de realizar.
 Optimizador de bucles. Se realizan para optimizar bucles.
 Optimización del flujo de un programa. Es la más compleja y
habitualmente se realizan por medio de teoría de grafos.

Algunas de las optimizaciones locales independientes de la máquina objeto se


presentan a continuación:

 Reducción o precálculo de constantes. Consiste en realizar las


operaciones con constantes antes de generar código. Así (2+3)*5 podría generar
en una pila:
o PUSH 2
o PUSH 3
o ADD
o PUSH 5

43
Instituto Tecnológico Superior de Misantla

o MUL
Sin embargo si se hace la operación con las constantes sólo quedaría
PUSH 25.
 Fusión de constantes. suele realizarse con las constantes de
cadena. Así el uso de la misma constante dos o más veces tan sólo genera
código para una constante, el resto son la referencia a la misma constante.
 Reacondicionamiento de instrucciones. También se cambia en
algunos casos en algunos casos el orden de evaluación, para generar código
optimizado.
 Comprobación de los rangos y de desbordamiento de la pila. Una
vez que los programas compilados han sido probados, pueden quitarse del
compilador las opciones de comprobación de rangos y las opciones de
comprobación de desbordamientos de la pila. Al generar el código
correspondiente para generar dichas comprobaciones.
 Uso de operadores de desplazamiento de bits en vez multiplicación
y división. Las operaciones como x*a, donde a es una constante y potencia de 2,
se puede generar código con operaciones de desplazamiento. Lo mismo ocurre
para operaciones de la forma x/a.
 Eliminación de código muerto. El código que no se ejecuta no debe
generar código. Así los bucles vacíos no generan código.

44
Instituto Tecnológico Superior de Misantla

Unidad 4. Generación de código objeto.


4.1. Lenguaje máquina.

Son aquéllos que están escritos en lenguajes directamente inteligibles por la


máquina (computadora), ya que sus instrucciones son cadenas binarias (cadenas o
series de caracteres de dígitos 0 y 1) que especifican una operación y las posiciones
(dirección) de memoria implicadas en la operación se denominan instrucciones de
máquina o código máquina. El código máquina es el conocido código binario.
Las instrucciones en lenguaje máquina dependen del hardware de la
computadora y, por tanto, diferirán de una computadora a otra.

En la actualidad, las desventajas superan a las ventajas, lo que hace


prácticamente no recomendables a los lenguajes máquinas.

Ventajas del Lenguaje Máquina


Posibilidad de cargar (transferir un programa a la memoria) sin necesidad
de traducción posterior, lo que supone una velocidad de ejecución superior a
cualquier otro lenguaje de programación.

Desventajas del Lenguaje Máquina


Dificultad y lentitud en la codificación.
Poca fiabilidad.
Gran dificultad para verificar y poner a punto los programas.
Los programas solo son ejecutables en el mismo procesador (CPU).

Direccionamiento.
-LENGUAJE MÁQUINA-

45
Instituto Tecnológico Superior de Misantla

Se dice que un programa se forma con un grupo de patrones binarios, por ejemplo sumar las
localidades 1500H y 3020H y almacenar el resultado en la localidad 3000H el programa a
ejecutar es el siguiente:
0 1
011 010

0 0
000 000

0 0
001 101

0 0
100 111

0 1
011 010

0 0
010 000

0 0
011 000

1 0
000 000

0 0
011 010

0 0
000 000

0 0
011 000

A estos patrones binarios se les conoce como "LENGUAJE MÁQUINA" ya que este es
el único lenguaje que entienden las computadoras.

-PROBLEMAS CON LOS PATRONES BINARIOS-


1. Es muy difícil para el programador entender o depurar los
programas.
2. El proceso de cargar el programa en la computadora es muy lento
ya que se tiene que alimentar bit por bit.
3. El programa no describe la tarea que realiza de una forma fácil de
entender para el usuario.
4. Los programas largos son cansados en su elaboración.
5. Existe una gran posibilidad de que el programador se equivoque en
la redacción del programa, produciendo de esta forma, una gran dificultado para
encontrar los errores.
6. Si en el programa binario existiera un bit erróneo sería laborioso el
detectarlo y corregirlo.

46
Instituto Tecnológico Superior de Misantla

-MNEMÓNICOS-
Un método de mejorar el manejo de las instrucciones para el programador consiste en
asignar un nombre a cada instrucción, por ejemplo a la instrucción de decrementar se
le nombra como "DEC" y a la de cargar un dato se nombra como "LD", a los nombres
de las instrucciones se les conoce como "MNEMÓNICOS", se escogen de tal manera
que orienten en forma fácil al programador.
Es más fácil recordar que INC A es el mnemónico de la instrucción para
incrementar en uno el contenido del registro acumulador que recordar que 3Ch es el
código operacional correspondiente a la misma instrucción, cada fabricante de
microprocesadores proporciona una serie de mnemónicos para el conjunto de
instrucciones de cada microprocesador.

-MODOS DE DIRECCIONAMIENTO-
Una parte importante en la programación es el de entender las formas de
direccionamiento para las localidades de memoria que contienen los datos que se
procesan como instrucciones, la CPU Z80 tiene 10 modos diferentes para direccionar
los datos almacenados en la memoria y en los registros:
Implícito o inherente
Inmediato
Inmediato extendido
Registro
Registro indirecto
Extendido
Página cero modificada
Relativo
Indexado
Bit

-DIRECCIONAMIENTO IMPLÍCITO O INHERENTE-


Este modo de direccionamiento se refiere a operaciones en donde el código
operacional implica automáticamente a uno o más registros de la CPU, el código
operacional es fijo y no tiene campos variables de tal forma que la instrucción siempre
ejecuta la misma operación.
Ejemplos de este modo de direccionamiento son las instrucciones CPL y RRA.
El código operacional de CPL es 2Fh, ésta instrucción toma el contenido del acumulador,
lo complementa a uno, cambia los ceros por unos y los unos por ceros, y deposita el resultado en
el acumulador, las bandeas de condiciones no se afectan, la fuente y el destino siempre son fijos;

47
Instituto Tecnológico Superior de Misantla

-DIRECCIONAMIENTO INMEDIATO-
Este modo no constituye un direccionamiento propiamente dicho, pues aquí la
instrucción no contiene ninguna dirección, en su lugar figura un operando sobre el cual
se realiza la operación que indica la instrucción, no hay necesidad del ciclo de memoria
(ejecute) de ejecución, una denominación más precisa es la de operando inmediato.
Una instrucción con direccionamiento inmediato es la que contiene en el byte 2 o
en el byte 3 de la propia instrucción en el dato de la transferencia, es decir, el dato es
parte integral de los bytes que constituyen la instrucción.

OP-CODE <----- uno o más bytes


OPERANDO
Este tipo de direccionamiento es útil al cargar o efectuar una operación
aritmética o lógica con un dato constante.
Ejemplo: Para cargar 07h en el acumulador se utiliza la instrucción LD A,N

x 3E LD A,N
x+1 N A  07h

El código operacional de la instrucción es 3Eh y el segundo byte es el dato que se va a


cargar en el acumulador, en este caso N = 07h, los movimientos que realiza el CPU
Z80 son los siguientes:
1. El contenido de la localidad op-code que apunta el contador del
programa (PC) se carga en el registro de instrucciones IR y se incrementa el PC
en uno.
1. La unidad de control ordena, después de decodificar la instrucción
que el contenido de la localidad (dato inmediato u operando) que apunta el PC
se cargue en el registro "r" indicado por el código de la instrucción, el PC se
incrementa en uno y apunta a la próxima instrucción a ejecutarse.
En general este tipo de direccionamiento lo utilizan las instrucciones aritméticas
y lógicas de 8 bits, ejemplo de estas son: ADD N, XOR N, AND N, etc.

48
Instituto Tecnológico Superior de Misantla

-DIRECCIONAMIENTO INMEDIATO EXTENDIDO-


Este modo de direccionamiento como su nombre lo indica es una extensión del
Direccionamiento Inmediato, este tipo de direccionamiento permite cargar con un dato
de 16 bits a un registro par y obtiene la dirección donde continua el procesamiento en
forma inmediata.

x OP-CODE
x BYTE BAJO
+1
x BYTE ALTO
+2

Este modo de direccionamiento lo utilizan las instrucciones del grupo "cargar 16 bits" y
del grupo de instrucciones "saltos, llamadas y regresos", (JP, CALL y RET).
El primero de los dos bytes del dato o de la dirección contiene los bits menos
significativos (LSB) y el segundo byte contiene los bits más significativos (MSB).
Ejemplo:
X 01
x+1 68 NN = 7968
x+2 79
Los movimientos de esta instrucción son los siguientes:
1. El contenido de la localidad "x" apuntada por el contador de
programa (PC) se carga en el registro de instrucciones IR y se incrementa el
contador de programa, (PC  PC + 1)
2. La unidad de control después de decodificar la instrucción ordena
que el contenido de la localidad "x + 1" apuntada por el PC se cargue en el
registro B, y se incrementa el contador de programa ( PC  PC + 1)
3. La unidad de control ordena que el contenido apuntado por el PC
se cargue en el registro C, se incrementa el contador de programa, (PC  PC +
1) y el PC apunta a la siguiente instrucción a ejecutarse.

-DIRECCIONAMIENTO DE REGISTRO-
Muchas instrucciones utilizan los registros del Z80 para recibir o proporcionar un
dato, el código de la instrucción tiene un campo que se usa para especificar el ó los
registros que se utilizan en la ejecución de la instrucción.
Ejemplo: LD r,s
Esta instrucción ordena que el contenido del registro "s" se deposite en el
registro "r".
R  registro destino
S  registro fuente

49
Instituto Tecnológico Superior de Misantla

"r" y "s" pueden ser cualquiera de los registros activos o primarios de 8 bits de la
CPU Z80, el movimiento de la información de esta instrucción es:
1. El contenido de la localidad apuntada por el PC se deposita en el
registro de instrucciones IR, la unidad de control ordena copiar en el registro r el
contenido del registro s, PC  PC + 1, y el contador de programa apunta a la
siguiente instrucción a ejecutarse.
Los grupos de instrucciones que utilizan este tipo de direccionamiento de
registro, son los de aritmética y lógica de 8 bits y de 16 bits como son:
1. Rotar
2. Girar
3. Poner en uno
4. Poner en cero y
5. Probar bits

-DIRECCIONAMIENTO DE REGISTRO INDIRECTO-


Este tipo de direccionamiento utiliza el contenido de un registro par, como la
dirección de una localidad de memoria, los registros pares son BC, DE y HL, en donde
los registros B, D y H contienen los bits menos significativos, el código operacional de
la instrucción puede ser de uno o de dos bytes:
Ejemplo: LD r, (HL)
Los movimientos de esta instrucción son:
El contenido de la localidad que contiene el código operacional, al cual apunta el
contador de programa PC, se deposita en el registro de instrucciones IR y se
incrementa el contador de programa. PC  PC + 1.
La unidad de control envía los 16 bits del registro par HL por el bus de
direcciones, lee el contenido de la localidad con esta dirección y deposita su contenido
sobre el registro r.
Este tipo de direccionamiento permite apuntar a cualquier localidad de memoria,
antes de usar instrucciones que utilicen este tipo de direccionamiento se debe cargar el
registro par con el valor de la dirección requerida.

-DIRECCIONAMIENTO EXTENDIDO-
En este tipo de direccionamiento la dirección de la palabra de instrucción
contiene la dirección eficaz de la memoria, este es, las instrucciones que utilizan
direccionamiento extendido proporcionan los 16 bits de la dirección eficaz de la
memoria, el campo de la dirección eficaz contiene un operando, con este modo de
direccionamiento se pueden accesar directamente todas las localidades de memoria
desde 0000h hasta FFFFh, el primer byte contiene los 8 bits menos significativos (LSB)
de la dirección eficaz y el segundo byte contiene los 8 bits más significativos (MSB) de
la dirección eficaz.
Ejemplo: LD A,(1580h)

OP - CODE  3A
LSB  80

50
Instituto Tecnológico Superior de Misantla

MSB  15

Los movimientos que se producen son los siguientes:


1. La localidad apuntada por el contador de programa PC contiene el
código operacional, este se deposita en el registro de instrucciones IR, el
contador de programa se incrementa. PC  PC+ 1.
2. La unidad de control ordena que el contenido de la localidad
(dirección LSB) se deposite en un registro temporal, y se incrementa el contador
de programa PC PC + 1.
3. La unidad de control ordena que el contenido de la localidad
(dirección MSB) se deposite en otro registro temporal, y se incrementa el
contador de programa PC  PC + 1.
4. La unidad de control envía por el bus de direcciones la dirección
recién leída de 16 bits, lee el contenido de la localidad de memoria de esa
dirección y deposita el operando en el acumulador.
Con este tipo de direccionamiento se pueden hacer transferencias de datos de
16 bits, LD (1580h), HL

-DIRECCIONAMIENTO DE PAGINA ZERO MODIFICADA-


Este tipo de direccionamiento se usa únicamente con la instrucción RESTAR
(RST), esta instrucción causa que el procedimiento del programa continúe en una de
ocho localidades posibles y bien especificadas de la pagina zero.
La pagina zero se define como área de menor memoria que se puede accesar
con 8 bits del bus de direcciones, esto es las 256 localidades obtenidas con ocho
combinaciones posibles, desde la 00h hasta la FFh, todas estas direcciones
constituyen la pagina zero.
El código operacional de "RST" m" se encuentra como sigue:

11xxx111 RST m
Localidad m
0000 0
0008 1
0010 2
0018 3
0020 4
0028 5
0030 6
0038 7

51
Instituto Tecnológico Superior de Misantla

Las direcciones especificas de la pagina zero se obtienen al multiplicar por 8 el valor binario de
la variable m, por ejemplo si m = 2 la dirección es 10h.

RST m OP-CODE
RST 0 C7
RST 8 CF
RST 10 D7
RST 18 DF
RST 20 E7
RST 28 EF
RST 30 F7
RST 38 FF

-DIRECCIONAMIENTO RELATIVO-
En el direccionamiento relativo a la dirección proporcionada por el registro de
instrucciones se le añade una dirección de referencia, normalmente esta dirección de
referencia es el contador de programa PC, es posible obtener dos modos diferentes de
direccionamiento relativo :
1).- Relativo hacia adelante
2).- Relativo hacia atrás

En las direcciones de salto, este tipo de direccionamientos permite reducir el


numero de bytes para especificar la dirección y reducir así el largo del programa, se
usa el byte que sigue al código operacional relativo para especificar un desplazamiento
a partir del valor actual del contador de "programa más dos", el resultado de la
operación es una dirección a la cual el programa salta para continuar el procesamiento
en esta nueva dirección.

52
Instituto Tecnológico Superior de Misantla

El direccionamiento relativo permite el acceso a 256 localidades alrededor de la


dirección de la siguiente instrucción, el valor del operando es un valor con signo entre
+128 y -127, tomando como punto de partida del desplazamiento a la localidad de
memoria donde esta la siguiente instrucción:

El direccionamiento relativo se usa en la CPU Z80 únicamente con el grupo de


instrucciones de salto, permitiendo saltos condicionales o incondicionales que se
obtienen de la resta o la suma respectivamente, del contador de programa actual más
el operando.

-DIRECCIONAMIENTO DE BIT-
La CPU Z80 tiene instrucciones que permiten probar, poner en cero y poner en
uno a los bits de un operando:
1. BIT b, r
2. RES b, r
3. SET b, r
Estas instrucciones efectúan operaciones a nivel de bits sobre los contenidos de
las localidades de memoria o los registros de la CPU Z80 seleccionados por medio de
uno de tres tipos de direccionamiento, de registro, de registro indirecto e indexado, con
tres bits del código operacional se especifica cual de los 8 bits del operando es el
involucrado.
Ejemplo: SET b, r
En donde "b" puede ser cualquier numero del 0h al 7h y "r" es cualquier registro
primario, acumulador, registro par HL o registro de índice IX o IY.

CAMPO b BIT =1

53
Instituto Tecnológico Superior de Misantla

000 xxxx xxx1


001 xxxx xx1x
010 xxxx x1xx
011 xxxx 1xxx
100 xxx1 xxxx
101 xx1x xxxx
110 x1xx xxxx
111 1xxx xxxx
Ejemplo: SET 5, B
El bit "5" del registro B toma el valor de "1" los otros siete bits no sufren
alteración alguna, en este caso se esta usando direccionamiento de registro.
1000 1000 antes (B)  88
1010 1000 después (B)  A8

-DIRECCIONAMIENTO INDEXADO-
Con este modo se obtiene la dirección efectiva haciendo la suma de:
1. El contenido de un registro especial del Z80 denominado "registro de índice".
2. El registro de índice se suma con el operando que proporciona la instrucción.
La indexación permite tratar bucles durante un programa, si se dispone de varios
registros de índice se pueden añadir varios bucles, normalmente el registro de índice
se incrementa por software después de cada operación.
Las instrucciones que utilizan direccionamientos indexado usan byte que sigue al
código operacional para especificar el desplazamiento que suma a uno de los dos
registros de índice del Z80, para formar la dirección efectiva de la memoria, el
contenido del registro de índice no se altera.
Ejemplo:

REGISTRO DE INDICE + OPERANDO = DIRECCION EFICAZ


1200h + 08h = 1208h
Los movimientos son:

54
Instituto Tecnológico Superior de Misantla

1. El contenido de la localidad 1000h apuntada por el PC se deposita en el


registro I R , PC  PC + 1, la unidad de control reconoce que debe cargar otra vez el
contenido de la localidad 1001h sobre el I R, PC  PC + 1.
2. El código operacional en IR le ordena a la unidad de control leer en un registro
temporal el contenido de la localidad apuntada por el PC, PC  PC + 1.
3. La unidad de control envía por el bus de direcciones la dirección que resulta al
sumar el registro de índice x con el contenido del registro temporal, lee el contenido de
esa localidad y lo carga en el acumulador.

-DIRECCIONAMIENTO CON EL APUNTADOR DE PILA- (STACK POINTER)


El contenido de un registro por (BC, DE o HL), o el contenido del contador de
programa (PC), puede transferirse a una área de memoria llamada "stack pointer" y se
puede realizar la función inversa, las localidades de memoria que reciben el dato se
direccionan por medio de un registro de 16 bits denominado "apuntador de pila o stack
pointer", existen únicamente dos operaciones que se realizan con el apuntador de pila,
cargar un dato de 16 bits en la pila de la memoria por medio de la instrucción
denominada PUSH y la operación de leer un dato de 16 bits por medio de la instrucción
llamada POP, es importante aclarar que las direcciones a las que se esta
seleccionando con el apuntador de pila (SP) debe ser memorias de leer y escribir, esto
es, memorias tipo RAM.

-OPERACION PUSH –
Un dato de 16 bits se transfiere al área de la pila de la memoria desde un
registro par o desde el PC durante una operación PUSH, las direcciones del área de
memoria que son accesadas durante la operación PUSH se determinan de la siguiente
manera:
1.Los 8 bits mas significativos del dato se almacenan en la localidad de memoria
con dirección igual al contenido actual del apuntador de pila de la memoria (SP) menos
uno.
2.Los 8 bits menos significativos del dato se almacenan en la localidad de
memoria con dirección igual al contenido actual del apuntador de pila de la memoria
menos dos.
3. El contenido actual del apuntador de pila de la memoria se decrementa
automáticamente por dos.
Por ejemplo sea: PUSH BC
(SP)  13A6h, (B)  6A, (C)  30
antes después
1303 FF 1303 FF  SP
1304 FF 1304 30  C
1305 FF 1305 6A  B
1306 FF  SP 1306 FF
1307 FF 1307 FF

- OPERACION POP-
Un dato de 16 bits se transfiere desde la pila de la memoria a un registro par o al
contador del programa durante una operación POP.

55
Instituto Tecnológico Superior de Misantla

Las direcciones del área de memoria que son accesadas durante la operación
POP se determinan de la siguiente manera:
1.El segundo registro del par o los 8 bits menos significativos del contador de
programa se cargan con el contenido de la localidad direccionada por el apuntador de
la pila.
2.El primer registro del par o los 8 bits mas significativos del contador de
programa se cargan con el contenido de la localidad direccionada por el apuntador de
la pila mas uno.
3.El contenido actual de la pila se incrementa automáticamente por dos.
Ejemplo: POP BC

antes después
1507 FF 1507 FF
1508 33  SP 1508 33
1509 0C 1509 0C
150A FF 150A FF  SP
150B FF 150B FF
(B)  6A (B)  0C
(C)  30 (C)  33

Lenguajes de Bajo Nivel


Son más fáciles de utilizar que los lenguajes máquina, pero al igual que ellos,
dependen de la máquina en particular. El lenguaje de bajo nivel por excelencia es el
ensamblador. Las instrucciones en lenguaje ensamblador son instrucciones conocidas
como nemotécnicos. Por ejemplo, nemotécnicos típicos de operaciones aritméticas
son : en inglés : ADD, SUB, DIV, etc. ; en español : SUM, RES, DIV, etc.

Una instrucción típica de suma sería :


ADD M, N, P

Esta instrucción significa "sumar el contenido en la posición de memoria M al


número almacenado en la posición de memoria N y situar el resultado en la posición de
memoria P" . Evidentemente es más sencillo recordar la instrucción anterior con un
nemotécnico que su equivalente en código máquina.
0110 1001 1010 1011
Un programa escrito en lenguaje ensamblador, requiere de una fase de
traducción al lenguaje máquina para poder ser ejecutado directamente por la
computadora.
El programa original escrito en lenguaje ensamblador se denomina programa
fuente y el programa traducido en lenguaje máquina se conoce como programa
objeto, el cual ya es directamente entendible por la computadora.

Los lenguajes ensamblador tienen sus aplicaciones muy reducidas, se centran


básicamente en aplicaciones de tiempo real, control de procesos y de dispositivos
electrónicos.

56
Instituto Tecnológico Superior de Misantla

Ensamblador
Lenguaje de Bajo Nivel

Se denomina lenguaje máquina a la serie de datos que la parte física de la


computadora o hardware, es capaz de interpretar.
Una computadora digital o, mejor dicho, su parte física, sólo distingue datos de
tipo binario, es decir, constituidos por dos únicos valores a los que se denomina valor 0
y valor 1 y que, físicamente, se materializan con tensiones comprendidas entre 0 y 4.0
voltios y entre 4 y 5 voltios, respectivamente. Para representar datos que contengan
una información se utilizan una serie de unos y ceros cuyo conjunto indica dicha
información.
La información que hace que el hardware de la computadora realice una
determinada actividad de llama instrucción. Por consiguiente una instrucción es un
conjunto de unos y ceros. Las instrucciones así formadas equivalen a acciones
elementales de la máquina, por lo que al conjunto de dichas instrucciones que son
interpretadas directamente por la máquina se denomina lenguaje máquina.
El lenguaje máquina fue el primero que empleo el hombre para la programación
de las primeras computadoras. Una instrucción en lenguaje máquina puede
representarse de la siguiente forma:
011011001010010011110110

Esta secuencia es fácilmente ejecutada por la computadora, pero es de difícil


interpretación, siendo aun mas difícil la interpretación de un programa (conjunto de
instrucciones) escrito de esta forma. Esta dificultad hace que los errores sean
frecuentes y la corrección de los mismos costosa, cuando no imposible, al igual que la
verificación y modificación de los programas.

La anterior secuencia de dígitos binarios (bits) puede indicar a la


computadora que: <>

Si lo vemos escrito de esta forma, lo entenderemos fácilmente, ya que está


en nuestro lenguaje natural, pero la máquina elemental será incapaz de entender nada.
Vemos, pues, que la forma de indicar a la máquina lo que debe hacer es totalmente
diferente de la indicar a un ser humano lo mismo, por lo que deben emplearse
sistemas de traducción de una forma a otra.

En un principio el programador empleaba directamente el lenguaje máquina.


En este caso el traductor era el programador; pero vimos también los problemas que
esto causaba. Con la práctica en el manejo de la máquina se cayó en la cuenta de que
se podría utilizar la propia máquina para ayudar en la traducción de estos programas.
Es decir, que si a una máquina elemental se le dotaba de un programa, también
elemental, que tradujera un número determinado de caracteres de caracteres
alfabéticos en una secuencia de unos y ceros, se podría escribir un programa
constituido por una secuencia de grupos de caracteres alfabéticos, en la que cada uno
de los grupos indicaría una acción a realizar por el ordenador y, una vez escrito el

57
Instituto Tecnológico Superior de Misantla

programa, sería la propia máquina la que pasaría los grupos de caracteres a bits.
Las ventajas de esto son evidentes, ya que para el hombre resulta mas fácil manipular
grupos de caracteres y la traducción se haría de manera automática. Por ejemplo, se
podría escribir:
TRASLADAR 11010110, 00011101

Esto indicaría que el contenido de la posición 11010110 había que pasarlo a la posición
00011101 si se sabe que al grupo alfabético TRASLADAR le corresponde la secuencia
de bits 11110101. La máquina traduciría la anterior instrucción como:
11110101 11010110 00011101

Al grupo alfabético se le denomina mnemotécnico, y existirá un mnemotécnico


por cada instrucción. Se le da este nombre porque sirve para recordar con mayor
facilidad el conjunto de instrucciones de una determinada máquina. De esta forma
aparecieron los lenguajes ensambladores (Assembler, en inglés). Poco a poco, con el
avance de la programación (Software), estas primeras y sencillas ayudas se fueron
haciendo más complejas, permitiendo que, además de los mnemotécnicos
correspondientes a la operación a realizar, se pudieran emplear otros para indicar, por
ejemplo, los operandos. La anterior instrucción se podría escribir de la siguiente forma:
TRASLADAR POS-A POS-B Que nos resulta de más fácil comprensión. También se
introdujo la posibilidad de indicar a la computadora la dirección de un salto en la
secuencia de ejecución de un programa mediante la utilización de etiquetas.

.
Ventajas De Los Lenguajes Ensambladores.
El corazón de la computadora es el microprocesador, éste maneja las
necesidades aritméticas, de lógica y de control de la computadora.
El microprocesador tiene su origen en la década de los sesenta, cuando se diseño el
circuito integrado (IC por sus siglas en ingles) al combinar varios componentes
electrónicos en un solo componente sobre un "chip" de silicio. Los fabricantes

58
Instituto Tecnológico Superior de Misantla

colocaron este diminuto chip en un dispositivo parecido a un ciempiés y lo conectaron a


un sistema en funcionamiento. A principios de los años setenta Intel introdujo el chip
8008 el cual, instalado en una computadora terminal, acompaño a la primera
generación de microprocesadores. La variedad de microcomputadoras también
ocasiono un renovado interés en el lenguaje ensamblado, cuyo uso conlleva a
diferentes ventajas:
Un programa escrito en el lenguaje ensamblador requiere considerablemente
menos memoria y tiempo de ejecución que un programa escrito en los conocidos
lenguajes de alto nivel, como Pascal y C.
El lenguaje ensamblador da a un programador la capacidad de realizar tareas
muy técnicas que serian difíciles, si no es que imposibles de realizar en un lenguaje de
alto nivel.
El conocimiento del lenguaje ensamblador permite una comprensión de la
arquitectura de la maquina que ningún lenguaje de alto nivel puede ofrecer.
Aunque la mayoría de los especialistas en Software desarrolla aplicaciones
en lenguajes de alto nivel, que son más fáciles de escribir y de dar mantenimiento, una
practica común es recodificar en lenguaje ensamblador aquellas rutinas que han
causado cuellos de botella en el procesamiento.
Los programas residentes y rutinas de servicio de interrupción casi siempre
son desarrollados en el lenguaje ensamblador.
Los lenguajes de alto nivel fueron diseñados para eliminar las particularidades
de una computadora especifica, mientras que un lenguaje ensamblador esta diseñado
para una computadora especifica, o, de manera más correcta, para una familia
especifica de microprocesadores.

A continuación se listan los requisitos para aprender el lenguaje ensamblador de la PC:


 Tener acceso a una computadora personal de IBM (cualquier modelo) o
una compatible.
 Una copia del sistema operativo MS-DOS o PC-DOS y estar
familiarizados con su uso.
 Una copia de un programa ensamblador. Las versiones de Microsoft son
conocidas como MASM y QuickAssembler: TASM es de Borland y OPTASM es de
System.

Para el aprendizaje de lenguaje ensamblador no es necesario lo siguiente:


 Conocimiento previo de un lenguaje de programación, aunque tenerlo
puede ayudarle a comprender algunos conceptos de programación más rápido.
Conocimiento previo de electrónica o circuiteria.

Ventajas del lenguaje ensamblador frente al lenguaje máquina


Mayor facilidad de codificación y, en general, su velocidad de cálculo.

Desventajas del lenguaje ensamblador


Dependencia total de la máquina lo que impide la transportabilidad de los

59
Instituto Tecnológico Superior de Misantla

programas (posibilidad de ejecutar un programa en diferentes máquinas. El lenguaje


ensamblador del PC es distinto del lenguaje ensamblador del Apple Machintosh.

La formación de los programadores es más compleja que la correspondiente a


los programadores de alto nivel, ya que exige no solo las técnicas de programación,
sino también el conocimiento del interior de la máquina.

Administración de memoria automática


La administración de memoria automática es uno de los servicios que
proporciona Common Language Runtime durante la ejecución administrada. El
recolector de elementos no utilizados de Common Language Runtime administra la
asignación y liberación de la memoria de una aplicación. Esto significa que los
programadores no tienen que escribir código para realizar tareas de administración de
memoria al programar aplicaciones administradas. La administración automática de la
memoria puede eliminar problemas frecuentes, como olvidar liberar un objeto y causar
una pérdida de memoria, o intentar tener acceso a la memoria de un objeto que ya se
ha liberado. En esta sección se describe cómo asigna y libera memoria el recolector de
elementos no utilizados.

Asignar memoria
Cuando se inicializa un nuevo proceso, el motor de tiempo de ejecución reserva
una región contigua de espacio de direcciones para el proceso. Este espacio de
direcciones reservado se denomina montón administrado. El montón administrado
mantiene un puntero a la dirección a la que se asignará el siguiente objeto del montón.
Inicialmente, este puntero se establece en la dirección base del montón administrado.
Todos los tipos de referencia se asignan en el montón administrado. Cuando una
aplicación crea el primer tipo de referencia, se le asigna memoria en la dirección base
del montón administrado. Cuando la aplicación crea el siguiente objeto, el recolector de
elementos no utilizados le asigna memoria en el espacio de direcciones que sigue
inmediatamente al primer objeto. Siempre que haya espacio de direcciones disponible,
el recolector de elementos no utilizados continúa asignando espacio a los objetos
nuevos de este modo.
La asignación de memoria desde el montón administrado es más rápida que la
asignación de memoria no administrada. Como el tiempo de ejecución asigna memoria
a los objetos agregando un valor a un puntero, este método es casi tan rápido como la
asignación de memoria desde la pila. Además, puesto que los nuevos objetos que se
asignan consecutivamente se almacenan uno junto a otro en el montón administrado, la
aplicación puede tener un acceso muy rápido a los objetos.

Liberar memoria
El motor de optimización del recolector de elementos no utilizados determina
cuál es el mejor momento para realizar una recolección basándose en las asignaciones
realizadas. Cuando el recolector lleva a cabo una recolección, libera la memoria de los
objetos que ya no usa la aplicación. Determina qué objetos ya no se usan examinando
las raíces de la aplicación. Todas las aplicaciones tienen un conjunto de raíces. Cada

60
Instituto Tecnológico Superior de Misantla

raíz hace referencia a un objeto del montón administrado, o bien se establece en null.
Las raíces de una aplicación incluyen punteros de objeto globales y estáticos, variables
locales y parámetros de objetos de referencia en la pila de un subproceso, y registros
de la CPU. El recolector de elementos no utilizados tiene acceso a la lista de raíces
activas que mantienen el compilador Just-In-Time (JIT) y el motor de tiempo de
ejecución. Con esta lista examina las raíces de la aplicación y, durante este proceso,
crea un gráfico que contiene todos los objetos que no se pueden alcanzar desde las
raíces.
Los objetos que no están en el gráfico no se pueden alcanzar desde las raíces
de la aplicación. El recolector considera los objetos inalcanzables elementos no
utilizados y libera la memoria que tienen asignada. Durante una recolección, el
recolector de elementos no utilizados examina el montón administrado y busca los
bloques de espacio de direcciones que ocupan los objetos que no se pueden alcanzar.
Cuando encuentra cada uno de los objetos inalcanzables, usa una función de copia de
memoria para compactar los objetos alcanzables en la memoria y libera los bloques de
espacios de direcciones asignados a los objetos no alcanzables. Una vez que se ha
compactado la memoria de los objetos alcanzables, el recolector de elementos no
utilizados hace las correcciones de puntero necesarias para que las raíces de la
aplicación señalen a los objetos en sus nuevas ubicaciones. También sitúa el puntero
del montón administrado después del último objeto alcanzable. Tenga en cuenta que la
memoria sólo se compacta si, durante una recolección, se encuentra un número
significativo de objetos inalcanzables. Si todos los objetos del montón administrado
sobreviven a una recolección, no hay necesidad de comprimir la memoria.
Para mejorar el rendimiento, el tiempo de ejecución asigna memoria a los
objetos grandes en un montón aparte. El recolector de elementos no utilizados libera la
memoria para los objetos grandes automáticamente. Sin embargo, para no mover
objetos grandes en la memoria, dicha memoria no se compacta.

Generaciones y rendimiento
Para optimizar el rendimiento del recolector de elementos no utilizados, el
montón administrado se divide en tres generaciones: 0, 1 y 2. El algoritmo de
recolección de elementos no utilizados del motor de tiempo de ejecución se basa en
varias afirmaciones que la industria de software informático ha comprobado como
ciertas experimentando con esquemas de recolección de elementos no utilizados.
Primero, es más rápido compactar la memoria de una parte del montón administrado
que la de todo el montón. En segundo lugar, los objetos más recientes tienen una
duración más corta y los objetos antiguos tienen una duración más larga. Por último,
los objetos más recientes suelen estar relacionados unos con otros y la aplicación tiene
acceso a ellos más o menos al mismo tiempo.
El recolector de elementos no utilizados del motor de tiempo de ejecución
guarda los nuevos objetos en la generación 0. Los objetos creados en las primeras
etapas de la duración de la aplicación y que sobreviven a las recolecciones se
promueven y se almacenan en las generaciones 1 y 2. El proceso de promoción de
objetos se describe más adelante en este tema. Como es más rápido compactar una
parte del montón administrado que todo el montón, este esquema permite que el
recolector de elementos no utilizados libere la memoria en una generación específica

61
Instituto Tecnológico Superior de Misantla

en lugar de liberarla para todo el montón administrado cada vez que realiza una
recolección.
En realidad, el recolector de elementos no utilizados realiza una recolección
cuando se llena la generación 0. Si una aplicación trata de crear un nuevo objeto
cuando la generación 0 está llena, el recolector de elementos no utilizados descubre
que no queda espacio de direcciones en la generación 0 para asignárselo. El recolector
de elementos no utilizados realiza una recolección, en un intento de liberar espacio de
direcciones para el objeto en la generación 0. Primero examina los objetos de la
generación 0 y no todos los objetos del montón administrado. Éste es un enfoque más
eficaz, ya que los objetos nuevos suelen tener una duración más corta y se espera que
la aplicación no utilice muchos de los objetos de la generación 0 cuando se realice una
recolección. Además, una recolección de tan sólo la generación 0 a menudo recupera
suficiente memoria para que la aplicación pueda continuar creando nuevos objetos.
Una vez que el recolector de elementos no utilizados realiza una recolección de
la generación 0, compacta la memoria de los objetos que se pueden alcanzar como se
ha explicado antes en este tema, en Liberar memoria. A continuación, el recolector de
elementos no utilizados promueve estos objetos y considera que esta parte del montón
administrado está en la generación 1. Puesto que los objetos que sobreviven a las
recolecciones suelen tener una duración más larga, es lógico promoverlos a una
generación superior. En consecuencia, el recolector de elementos no utilizados no tiene
que volver a examinar los objetos de las generaciones 1 y 2 cada vez que realiza una
recolección en la generación 0.
Una vez que el recolector de elementos no utilizados ha realizado la primera
recolección de la generación 0 y ha promovido los objetos que se pueden alcanzar a la
generación 1, considera lo que queda del montón administrado como generación 0.
Continúa asignando memoria a los nuevos objetos en la generación 0 hasta que está
llena y es necesario realizar otra recolección. En este momento, el motor de
optimización del recolector de elementos no utilizados determina si es necesario
examinar los objetos de generaciones más antiguas. Por ejemplo, si una recolección de
la generación 0 no recupera memoria suficiente para que la aplicación pueda terminar
satisfactoriamente su intento de crear un nuevo objeto, el recolector de elementos no
utilizados puede realizar una recolección de la generación 1 y, después, de la
generación 0. Si así no se recupera suficiente memoria, el recolector de elementos no
utilizados puede realizar una recolección de las generaciones 2, 1 y 0. Después de
cada recolección, el recolector de elementos no utilizados compacta los objetos
alcanzables de la generación 0 y los promueve a la generación 1. Los objetos de la
generación 1 que se siguen utilizando después de la recolección se promueven a la
generación 2. Como el recolector de elementos no utilizados sólo admite tres
generaciones, los objetos de la generación 2 que se siguen utilizando después de una
recolección permanecen en la generación 2 hasta que se determina, en una
recolección posterior, que no se pueden alcanzar.

Liberar memoria para recursos no administrados


En el caso de la mayoría de los objetos creados por la aplicación, puede utilizar
el recolector de elementos no utilizados para realizar automáticamente las tareas de
administración de memoria. Sin embargo, los recursos no administrados requieren una

62
Instituto Tecnológico Superior de Misantla

limpieza explícita. El tipo más habitual de recurso no administrado es un objeto que


contiene un recurso del sistema operativo, como un identificador de archivo,
identificador de ventana o conexión de red. Aunque el recolector de elementos no
utilizados puede realizar el seguimiento del período de duración de un objeto
administrado que encapsula un recurso no administrado, no tiene un conocimiento
específico de cómo limpiar el recurso. Cuando se crea un objeto que encapsula un
recurso no administrado, es recomendable proporcionar el código necesario para
limpiar dicho recurso en un método público Dispose. Si se proporciona un método
Dispose, se permite que los usuarios del objeto liberen su memoria de manera
explícita cuando hayan terminado de usarlo. Si se utiliza un objeto que encapsula un
recurso no administrado, se debe conocer la existencia de Dispose y llamarlo cuando
sea necesario. Para obtener más información sobre la limpieza de recursos no
administrados y el ejemplo de un modelo de diseño para implementar Dispose, vea
Conceptos de programación para la recolección de elementos no utilizados.

63
Instituto Tecnológico Superior de Misantla

Evaluación

63
Instituto Tecnológico Superior de Misantla

Bibliografía
1. Aho, Sethi, Ullman. Compiladores Principios, técnicas y herramientasEd. Addison Wesley.
2. Lemone Karen A. , Fundamentos de compiladores Cómo traducir al lenguaje de
computadora, Ed. Compañía Editorial Continental.
3. Kenneth C. Louden. Construcción de compiladores Principios y práctica.Ed. Thomson.
4. Martin John, Lenguajes formales y teoría de la computación, ED. Mc Graw Hill
5. Hopcroft John E., Introducción a la Teoría de Autómatas, Lenguajes y Computación, ED.
Addison Wesley
6. Guerra Crespo. Hector. Compiladores. Ed. Tecnologica didáctica.
7. Ronald Mak. Writing compilers and interpreters. Ed. Wiley Computer Publishing.
8. Fischer, LeBlanc. Crafting a compiler with C. Ed. Cummings Publishing Company, Inc.
9. Salas Parrilla, Jesús. Sistemas Operativos y Compiladores. McGraw Hill.
10. Beck. Software de Sistemas, Introducción a la programación de Sistemas. Addison-
Wesley Iberoamericana.
11. Teufel, Schmidt, Teufel. Compiladores Conceptos Fundamentales. Addison-Wesley
Iberoamericana.
12. C. Louden, Kenneth. Lenguajes de programación Principios y práctica. Thomson.
13. Levine Gutiérrez, Guillermo. Computación y programación moderna Perspectiva integral
de la informática. Pearson Educación.
14. Abel, Peter. Lenguaje ensamblador y programación para PC IBM y compatibles.
Pearson Educación.
15. Mak, Ronald. Writing compilers and interpreters. Wiley Computer Publishing.
16. Pittman, Thomas, Peters, James. The art of compiler design Theory and practice.
Prentice Hall.
17. Temblay & Sorenson. Compilers Writing. Mc Graw Hill.
18. R. Levine, John; Mason, Tony, Brown, Doug. Lex y Yacc. O'Reilly & Associates.
19. The Lex & Yacc Page, 3-mar-04, 12:45, http://dinosaur.compilertools.net
20. A compact guide to lex & Yacc, Thomas Niemann, 3-Mar-04, 12:50,
http://epaperpress.com/lexandyacc
21. Lex & Yacc HOWTO, Bert Hubert (PowerDNS.COM.BV), 3-Mar-04, 12:55,
http://ds9a.nl/lex_yacc
22. Flex, 3-mar-04, 13:02, http://www.gnu.org/software/flex/flex.html
23. Compiler construction using flex and Bison, Anthony Aaby, 3-mar-04, 13:05,
http://cs.wwc.edu/aabyan/464/BooK/
24. Flex, version 2.5 A fast scanner generator, Edition 2.5, March 1995, Vern Paxson, 3-mar-
04, 13:10, http://www.cs.princelon.edu/appel/modern/c/software/flex/flex_toc.html
25. Bison. The Yacc-compatible Parser Generator, November 1995, Bison Version 1.5,
Charles Donnelly and Richard Stallman, 3-mar-04, 13:10,
http://www.cs.princelon.edu/appel/modern/c/software/bison/bison_toc .html, 13/dic/2009
26. Bison. http://3d2f.com/programs/30-170-microprocessor-emulator-and- assembler-
download.shtml, 13/dic/2009
27. 2/Ago/2005 ,Microprocessor Emulator and Assembler 3.10-k,
http://software.intel.com/en-us/articles/all/1/, 24/feb/2010
28. Intel, 31/dic/2009, Intel® Software Development EmulatorBottom of Form,
http://software.intel.com/en‐us/articles/int el‐software‐development‐emulator/, 24/feb/2010

64

Potrebbero piacerti anche