Instituto de Computación - Facultad de Ingeniería - UDELAR 1
Tecnólogo en Informática
Paysandú - Uruguay
Notas de Teórico
Lenguajes y compilación
Arquitectura de Computadoras
(Versión 1.0 - 2014)
PROGRAMACIÓN BÁSICA DE LA COMPUTADORA
.1 Introducción
Un sistema de computadora total incluye tanto circuitería (hardware) como programación (software). El hardware consta de los componentes físicos y todo el equipo asociado, mientras que el software se refiere a los programas que están escritos para la computadora. Es posible conocer diferentes aspectos de programación de computadoras sin relacionarse con los detalles de cómo opera la circuitería. También es posible diseñar partes de la circuitería sin conocer las posibilidades de su software. Sin embargo, quienes se interesan en la arquitectura de la computadora deben conocer tanto la circuitería como el software, porque los dos aspectos influyen uno en el otro.
Escribir un programa para una computadora consiste en especificar, en forma directa o indirecta, una secuencia de instrucciones de máquina. Las instrucciones de máquina dentro de la computadora forman un patrón binario al que los usuarios les resulta difícil trabajar y entender (si no imposible). Es preferible escribir programas con los símbolos mas familiares del conjunto de caracteres alfanuméricos, pero es claro que una computadora no puede entender a priori el significado de los mismos. Como consecuencia, existe la necesidad de traducir los programas simbólicos orientados al usuario, a programas binarios que reconozca el hardware.
Un programa escrito por el usuario puede ser independiente o dependiente de la computadora física que corre su programa. Por ejemplo, un programa escrito en C estándar es independiente de la máquina porque la mayoría de las computadoras proporcionan un programa traductor que convierte el programa C estándar en código binario de la computadora particular. Pero el programa traductor es dependiente de la máquina porque debe traducir el programa C al código binario reconocido por la circuitería de la computadora particular que se utiliza.
Este capítulo presenta algunos conceptos de programación elementales y muestra su relación con la representación de instrucciones de hardware. Además se presentarán las operaciones básicas de los programas que traducen un programa simbólico de usuario a un programa binario equivalente (compiladores y ensambladores).
Para ilustrar con ejemplos los detalles que trataremos a continuación se utilizará un set de instrucciones de 16 bits de ejemplo. En la Tabla 1 mostramos un subconjunto de estas instrucciones. El formato de instrucción se detalla en la Figura 1.
Símbolo Código Descripción
add rA, rB, rC 0000 rA = rB + rC
sub rA, rB, rC 0001 rA = rB - rC
and rA, rB, rC 0010 rA = rB & rC
or rA,rB,rC 0011 rA = rB | rC
setlo rA, imm 0100 rA = rA | (imm >> 8)
sethi rA, imm 0101 rA = rA | (imm << 8)
not rA, rB 0110 rA = not rB
lw rA, rB 1000 rA = Memoria[rB]
sw rA, rB 1001 Memoria[rB] = rA
cmp rA, rB 1010 rA-rB y actualiza banderas N y Z
jz imm 1011 if (Z == 1) goto (PC + imm)
jn imm 1100 if (N == 1) go to (PC+imm)
Instituto de Computación - Facultad de Ingeniería -
UDELAR 3
.2 Lenguaje de Máquina
Un programa es una lista de instrucciones o enunciados para dirigir a la computadora con el propósito de que ejecute una tarea de procesamiento de datos. Existen varios tipos de lenguajes de programación que uno puede escribir para una computadora, pero esta sólo puede ejecutar programas cuando están representados de manera interna en forma binaria. Los programas escritos en cualquier otro lenguaje deben traducirse a la representación binaria de las instrucciones antes de que la computadora pueda ejecutarlos. Los programas escritos para una computadora pueden estar en una de las siguientes categorías:
1. Código binario. Este es una secuencia de instrucciones y operandos en binario que
lista la representación exacta de instrucciones conforme aparecen en la memoria de la computadora.
2. Código octal o hexadecimal. Este es una traducción equivalente del código binario en
representación octal o hexadecimal.
3. Código simbólico. El usuario emplea símbolos (letras, números o caracteres
especiales) para la parte de la operación, la parte de la dirección y las otras partes del código de instrucción. Cada instrucción simbólica puede traducirse a una instrucción codificada en binario. Esta traducción se hace mediante un programa especial llamado
ensamblador. Como un ensamblador traduce los símbolos, este tipo de programas
simbólicos se denomina programa de lenguaje ensamblador.
4. Lenguajes de programación de alto nivel. Estos son lenguajes especiales desarrollados para reflejar los procedimientos utilizados en la solución de un problema en lugar de interesarse en el desempeño de la circuitería de la computadora. Un ejemplo de un lenguaje de programación de alto nivel es C. Emplea símbolos y formatos orientados a un problema. El programa está escrito en una secuencia de enunciados establecidos de manera parecida a como las personas prefieren pensar cuando resuelven un problema. Sin embargo, cada enunciado debe traducirse a una secuencia de instrucciones de máquina antes de que la computadora pueda ejecutarlo. El programa que traduce un programa de lenguaje de alto nivel se llama
compilador.
Ahora usaremos la arquitectura de la Tabla 1 para ilustrar la relación entre los lenguajes binario y ensamblador. Consideremos el programa binario listado en la Tabla 2. La primera columna proporciona la posición de memoria (en binario) de cada instrucción u operando. La segunda columna lista el contenido binario de estas localidades de memoria. (La localidad es la dirección de la palabra de memoria en que se almacena la instrucción. Es importante establecer la diferencia entre la dirección de la instrucción y la instrucción misma.)
Instituto de Computación - Facultad de Ingeniería -
UDELAR 5
El programa puede almacenarse en la porción de memoria indicada y la computadora puede ejecutarlo después de comenzado desde la dirección 0. La circuitería de la computadora ejecutará estas instrucciones y realizará la tarea que se pretende. Sin embargo, una persona que observe este programa tendrá dificultades en comprender cuál será el efecto obtenido al ejecutar el programa. No obstante, la circuitería de la computadora sólo reconoce este tipo de código de instrucciones.
Escribir 16 bits para cada instrucción es tedioso porque hay demasiados símbolos. Podemos reducir la cantidad de símbolos por instrucción si escribimos el equivalente octal del código binario. Esto requerirá 6 dígitos por instrucción. Por otro parte, podemos reducir cada instrucción a 4 símbolos si escribimos el código hexadecimal equivalente como se muestra en la Tabla 3.
Posición (en binario) Código de instrucción 0 0100 0000 0001 0110 10 0101 0000 0000 0000 100 0100 0001 0001 1000 110 0101 0001 0000 0000 1000 0100 0010 0001 1010 1010 0101 0010 0000 0000 1100 1000 0011 0000 0000 1110 1000 0100 0001 0000 10000 0000 0011 0100 0011 10010 1001 0011 0010 0000 10100 1101 0000 0000 0000 10110 0000 0000 0101 0011 11000 1111 1111 1110 1001 11010 0000 0000 0000 0000
Tabla 2: Programa binario para sumar dos números
El programa de la Tabla 4 utiliza los nombres de instrucciones simbólicos en lugar de su equivalente binario o hexadecimal. Las partes de dirección de las instrucciones de referencia a memoria, al igual que los operandos, conservan su valor hexadecimal. Nótese que la posición 0x18 tiene un operando negativo porque el bit de signo de la posición de la extrema izquierda es 1. Los programas simbólicos son mas fáciles de manejar y, como consecuencia, es preferible escribir programas con símbolos.
Posición Instrucción 0000 4016 0002 5000 0004 4118 0006 5100 0008 421A 000A 5200 000C 8300 000E 8410 0010 0343 0012 9320 0014 D000 0016 0053 0018 FFE9 001A 0000
Instituto de Computación - Facultad de Ingeniería -
UDELAR 7
Tabla 3: Programa hexadecimal para sumar dos números
Podemos ir un paso mas adelante y sustituir cada dirección hexadecimal por una dirección simbólica y cada operando hexadecimal por un operando decimal. Esto es conveniente porque, por lo general, mientras se escribe un programa no se conoce con exactitud la posición de los operandos numéricos en la memoria. Si los operandos se colocan en la memoria después de las instrucciones y si no se sabe con anticipación el tamaño del programa, la posición de los operandos numéricos no se conoce hasta que se llega al final del programa. Además, estamos mas familiarizados con los números decimales que con sus equivalentes hexadecimales.
El programa de la Tabla 5 es el programa de lenguaje ensamblador para sumar dos números. Las primeras tres líneas tienen direcciones simbólicas. Sus valores se especifican porque están presentes como una etiqueta en la primera columna. Después del símbolo .word se especifican operandos decimales. Los números pueden ser positivos o negativos pero, si son negativos, deben convertirse a binario en representación complemento a 2 con signo.
Posición Instrucción Comentarios
0000 setlo r0, 16 Cargar en la parte baja del registro r0 el valor 0x16. 0002 sethi r0, 0 Cargar en la parte alta del registro r0 el valor 0x00.
0004 setlo r1, 18 Cargar en el registro r1 el valor 0x18.
0006 sethi r1, 0
0008 setlo r2, 1A Cargar en el registro r2 el valor 0x1A.
000A sethi r2, 0
000C lw r3, r0 Cargar en el registro r3 el valor en la posición de memoria indicada por r0.
000E lw r4, r1 Cargar en el registro r4 el valor en la posición de memoria indicada por r1.
0010 add r3, r4, r3 Sumar el valor de r4 al valor de r3, guarda resultado en r3.
0012 sw r3, r2 Almacenar el valor de r3 en la posición de memoria indicada por r2.
0014 j 0 Termino el programa.
0016 0053 Primer operando.
0018 FFE9 Segundo operando (negativo).
001A 0000 Almacenar la suma aquí.
Tabla 4: Programa con códigos de operación simbólicos (ensamblador)
.text
set r0, op1 #Cargar en el registro r0 el valor de la etiqueta op1. set r1, op2 #Cargar en el registro r1 el valor de la etiqueta op2. set r2, op3 #Cargar en el registro r2 el valor de la etiqueta op3. lw r3, r0 #Cargar en el registro r3 el valor en la posición de
memoria indicada por r0.
lw r4, r1 #Cargar en el registro r4 el valor en la posición de memoria indicada por r1.
add r3, r4, r3 #Sumar el valor de r4 al valor de r3, guarda resultado en r3.
sw r3, r2 #Almacenar el valor de r3 en la posición de memoria indicada por r2.
halt Termino el programa.
.data
op1: .word 83 #Primer operando decimal.
op2: .word -23 #Segundo operando decimal.
op3: .word 0 #Almacenar la suma aquí.
Tabla 5: Programa con códigos de operación simbólicos (ensamblador)
El programa en C equivalente para sumar dos números enteros se lista a continuación. La traducción de este programa C a un programa binario consiste en asignar tres localidades de memoria, dos para los sumandos y una para la suma, y después derivar la secuencia de instrucciones binarias que forman la suma. Por lo tanto, un programa compilador traduce los símbolos del programa C a los valores binarios que se listan en la primer tabla.
.3 Lenguaje Ensamblador
Un lenguaje de programación se define mediante un conjunto de reglas. Los usuarios deben apegarse a todas las reglas de formato del lenguaje si desean que sus programas se traduzcan en forma correcta. Casi cada computadora comercial tiene su propio lenguaje ensamblador. Las reglas para escribir un programa en lenguaje ensamblador se documentan y publican en manuales que, en general, tiene disponible el fabricante de la computadora.
La unidad básica de un programa de lenguaje ensamblador es una línea de código. El lenguaje específico se define mediante un conjunto de reglas que especifican los símbolos que pueden utilizarse y cómo pueden combinarse para formar una línea de código. Ahora formularemos las reglas generales del lenguaje ensamblador de ejemplo.
Reglas del lenguaje
Cada línea de un programa de lenguaje ensamblador se arregla en tres columnas llamadas campos. Los campos especifican la siguiente información:
1. El campo de etiqueta puede estar vacío o especificar una dirección simbólica. 2. El campo de instrucción especifica una instrucción de máquina.
3. El campo de comentario puede estar vacío o incluir un comentario.
Una dirección simbólica consta de caracteres alfanuméricos. El programador puede elegir en forma arbitraria el símbolo. Una dirección simbólica en el campo de etiqueta se termina mediante dos puntos para que el ensamblador pueda reconocerla como etiqueta.
El campo de instrucción en un programa de lenguaje ensamblador puede especificar algunas de las siguientes opciones:
1. Una instrucción de lectura/escritura a memoria. 2. Una instrucción aritmética de referencia a registros. 3. Una instrucción de control.
4. Una instrucción de llamada al sistema o entrada/salida.
Una dirección simbólica en el campo de instrucción especifica la localidad de memoria int a,b,c;
a=83; b=-23; c=a+b
Instituto de Computación - Facultad de Ingeniería -
UDELAR 9
del operando. Esta localidad debe definirse en alguna parte del programa apareciendo de nuevo como una etiqueta en la primera columna. Para poder traducir un programa de lenguaje ensamblador a un programa binario es absolutamente necesario que cada instrucción simbólica que se menciona en el campo de instrucción deba ocurrir nuevamente en el campo de etiqueta.
Una pseudoinstrucción no es una instrucción de máquina sino una instrucción para el ensamblador que se traduce en una o mas instrucciones de máquina. Por ejemplo en el programa que vimos anteriormente se usa la pseudoinstrucción set para cargar un operando de 16 bits en un registro. También existen las llamadas directivas del ensamblador, que también son instrucciones para el ensamblador que proporcionan información acerca de alguna parte de la traducción. Por ejemplo en el ejemplo anterior se usa la directiva .word, la cual avisa al ensamblador que el valor que le sigue es una palabra de 16 bits que se desea colocar en memoria.
El tercer campo de un programa está reservado para comentarios. Una línea de código puede tener o no un comentario, pero si lo tiene debe estar precedido por el símbolo
numeral para que el ensamblador reconozca el comienzo del campo de comentario. Los
comentarios son útiles para explicar el programa y para comprender el procedimiento detallado que realiza el programa. Los comentarios se insertan sólo para explicar y no se consideran durante el proceso de traducción a binario.
Un ejemplo
El programa de la Tabla 6 es un ejemplo de un programa de lenguaje ensamblador. La primera línea tiene la directiva .text para definir el origen del programa en la posición de memoria 0x100. Las siguientes 3 lineas son pseudoinstrucciones para cargar un operando de 16 bits en un registro. Las siguientes 5 son instrucciones de máquina. Se han utilizado tres direcciones simbólicas listadas en la primer columna como una etiqueta y en la columna 2 como una dirección.
.text 100 #Inicio el programa en la posición de memoria 0x100 set r0, min #Cargar en el registro r0 la dirección min. set r1, sub #Cargar en el registro r1 la dirección sub. set r2, dif #Cargar en el registro r2 la dirección dif.
lw r3, r0 #Cargar en el registro r3 el valor en la posición de memoria indicada por r0.
lw r4, r1 #Cargar en el registro r4 el valor en la posición de memoria indicada por r1.
sub r3, r4, r3 #Restar el valor de r4 al valor de r3, guarda resultado en r3. sw r3, r2 #Almacenar el valor de r3 en la posición de memoria
indicada por r2.
halt Termino el programa.
min: .word 83 #Minuendo.
sub: .word -23 #Sustraendo.
dif: .word 0 #Almacenar la resta aquí.
Tabla 6: Programa en lenguaje ensamblador para restar dos números
Cuando el programa se traduce a código binario y la computadora lo ejecuta, se realizará una resta entre dos números. La resta se ejecuta al sumar el minuendo del complemento a 2 del sustraendo. El sustraendo es un número negativo.
.3.1 Traducción a binario
La traducción del programa simbólico a binario se hace mediante un programa especial llamado ensamblador. Las tareas que ejecuta el ensamblador se comprenderán mejor si realizamos un ejemplo de traducción. La traducción del programa simbólico de la Tabla 6 a su código binario equivalente puede hacerse al examinar el programa y sustituir los símbolos por su código binario de máquina equivalente.
Esta traducción consiste en utilizar la Tabla 1 para convertir las instrucción simbólicas en su respectivo código binario. Para esto además el ensamblador utiliza las pseudoinstrucciones y directivas dadas por el programador.
El proceso de traducción puede simplificarse si examinamos el programa simbólico completo dos veces. No se hace ninguna traducción durante el primer examen. Simplemente asignamos una posición de memoria a cada instrucción de máquina y operando (teniendo en cuenta que las pseudoinstrucciones pueden generar mas de una instrucción). La asignación de posiciones definirá el valor de dirección de las etiquetas y facilitará el proceso de traducción durante el segundo examen. Por lo tanto, en la tabla, asignamos la posición 0x100 a la primera instrucción después del .text. Luego asignamos posiciones secuenciales para cada línea de código que tenga una instrucción de máquina u operando hasta el final del programa. Cuando se termina la primer examinación asociamos cada valor con su número de posición y formamos una tabla que defina el valor hexadecimal de cada dirección simbólica. Para este programa, la tabla de dirección de símbolo es como sigue:
Dirección simbólica Dirección hexadecimal
min 11C
sub 120
dif 124
Durante la segunda examinación del programa simbólico se hace referencia a la tabla de símbolos de dirección para determinar el valor de dirección de una instrucción que hace referencia a memoria. Por ejemplo, en la línea de código set r1, el valor hexadecimal de sub se obtiene de la tabla de símbolos de dirección que aparece arriba y la línea de código lw r, r1 se traduce durante la segunda examinación al obtener el valor hexadecimal de lw de la Tabla 1 y el valor de los registros r4 y r1 de una tabla similar. Después, se ensamblan las dos partes en una instrucción hexadecimal de cuatro dígitos. El código hexadecimal puede convertirse con facilidad a binario se deseamos conocer exactamente cómo reside este programa en la memoria de la computadora.
.4 El ensamblador y el compilador
Un ensamblador es un programa que acepta un código en lenguaje simbólico y produce su lenguaje de máquina binario equivalente. El programa simbólico de entrada se llama programa fuente y el programa binario que resulta se llama programa objeto. El ensamblador es un programa que opera sobre cadenas de caracteres y produce una interpretación binaria equivalente.
Un programa que traduce un programa escrito en un lenguaje de programación de alto nivel a un programa de lenguaje de máquina se llama compilador. Un compilador es un programa mas complicado que un ensamblador. Un compilador puede utilizar un lenguaje ensamblador como un paso intermedio en la traducción o puede traducir el programa en forma directa a binario.
Instituto de Computación - Facultad de Ingeniería -
UDELAR 11
La portabilidad en computación se define como la característica que posee un software para ejecutarse en diferentes plataformas, el código fuente del software es capaz de reutilizarse en vez de crearse un nuevo código cuando el software pasa de una plataforma a otra. A mayor portabilidad menor es la dependencia del software con respecto a la plataforma. El prerrequisito para la portabilidad es la abstracción entre la aplicación lógica y las interfaces del sistema. Cuando un software se puede compilar en diversas plataformas (x86, IA64, amd64, etc.), se dice que es multiplataforma. Esta característica es importante para la reducción de costos, cuando se quiere hacer una misma aplicación para ejecutarse en diferentes plataformas.
El software puede ser recompilado desde su código fuente para diferentes plataformas (sistemas operativos y procesadores) si es escrito en un lenguaje de programación que soporte la compilación en las distintas plataformas. Esto es usualmente una tarea de los desarrolladores; usuarios típicos no tienen acceso al código fuente ni las habilidades necesarias para hacerlo. Es por esto que en muchos casos el software se distribuye compilado para una plataforma específica y solo puede usarse en dicha plataforma.
.5.1 Lenguaje interpretado
Un lenguaje interpretado es un lenguaje de programación que está diseñado para ser ejecutado por medio de un intérprete, en contraste con los lenguajes compilados. Teóricamente, cualquier lenguaje puede ser compilado o ser interpretado, así que esta designación es aplicada puramente debido a la práctica de implementación común y no a alguna característica subyacente de un lenguaje en particular. Sin embargo, hay lenguajes que son diseñados para ser intrínsecamente interpretativos, por lo tanto un compilador de dicho lenguaje será poco eficiente. Muchos autores rechazan la clasificación de lenguajes de programación entre interpretados y compilados, considerando que el modo de ejecución (por medio de intérprete o de compilador) del programa escrito en el lenguaje es independiente del propio lenguaje. A ciertos lenguajes interpretados también se les conoce como lenguajes de script.
Mientras que Java es traducido a una forma que se destina a ser interpretada, la compilación justo a tiempo (just in time) es frecuentemente usada para generar el código de máquina. Los lenguajes de Microsoft .NET compilan a una forma intermedia (CIL) la cual es entonces a menudo compilada en código de máquina nativo; sin embargo hay una máquina virtual capaz de interpretar el CIL. Muchas implementaciones Lisp pueden mezclar libremente código interpretado y compilado. Estas implementaciones también usan un compilador que puede traducir arbitrariamente código fuente en tiempo de ejecución (runtime) a código de máquina.
Los lenguajes interpretados dan a los programas cierta flexibilidad adicional sobre los lenguajes compilados. Algunas características que son más fáciles de implementar en intérpretes que en compiladores incluyen, pero no se limitan, a:
• Independencia de plataforma (por ejemplo el byte code de Java).
• Generación funcional de primer orden, y orden sin necesidad de especificar metadata. • Posibilidad de generación de código in-situ, sin necesidad de recurrir a una
compilación (ie. Spring).
• Tipos Dinámicos.
• Facilidad en la depuración (es más fácil obtener información del código fuente en lenguajes interpretados).
• Pequeño tamaño del programa (puesto que los lenguajes interpretados tienen
Por otro lado, la ejecución del programa por medio de un intérprete es usualmente mucho menos eficiente que la ejecución de un programa compilado. No es eficiente en tiempo porque, o cada instrucción debe pasar por una interpretación en tiempo de ejecución, o como en más recientes implementaciones, el código tiene que ser compilado a una representación intermedia antes de cada ejecución. La máquina virtual es una solución parcial al problema de la eficiencia del tiempo pues la definición del lenguaje intermedio es mucha más cercana al lenguaje de máquina y por lo tanto más fácil de ser traducida en tiempo de ejecución. Otra desventaja es la necesidad de un intérprete en la máquina local para poder hacer la ejecución posible.