Está en la página 1de 9

© Pedro A. Castillo Valdivieso Dpto. ATC.

UGR 2008-2009 1

DESARROLLO DE UN TRADUCTOR DE LENGUAJE


ALTO NIVEL A ENSAMBLADOR
Objetivo de la práctica:

Con esta práctica pretendemos llegar a comprender el funcionamiento básico de un


compilador-traductor.

A la hora de construir programas, disponemos de muchos lenguajes de programación


que traducen las instrucciones de alto nivel a instrucciones binarias entendibles por la
máquina. Una parte importante de ese proceso es hacer una traducción del código fuente
a lenguaje ensamblador, más cercano a la máquina, y a partir de éste, generar el
programa ejecutable mediante un compilador de ensamblador.

Para poder traducir una instrucción a código máquina (o a ensamblador), hay que
conocer el nivel de lenguaje máquina con gran detalle (registros del procesador,
instrucciones máquina, acceso a memoria y dispositivos, etc) para determinar qué
instrucciones simples son necesarias para ejecutar cierta instrucción de nuestro lenguaje.

En esta práctica vamos a construir un compilador-traductor para un lenguaje de


programación de alto nivel sencillo inventado por nosotros. Para ello, debemos conocer
el lenguaje ensamblador que será el utilizado para hacer esa traducción intermedia que
comentábamos antes.

¿Cómo trabaja un compilador? Traducción de código de alto


nivel a lenguaje máquina
En el tema 1 de teoría vimos que para que una máquina entienda un programa escrito
lenguaje de alto nivel, hay que traducirlo a lenguaje máquina (que sí entiende el
procesador).

Lo habitual es traducir el lenguaje de más alto nivel (pensamiento u órdenes en lenguaje


natural) a un lenguaje de alto nivel con una sintaxis fija. Posteriormente podremos
traducir nuestro código (mediante el compilador) a ensamblador, y ese código
ensamblador resultante, ya muy cercano a la máquina, a código binario (usando el
compilador de ensamblador).

Por ejemplo, pensemos en un programa desarrollado en un lenguaje de alto nivel. El


programador utiliza un entorno integrado visual (Visual C++, VBasic, Builder, Delphi,
etc.) para crear de forma rápida y visual una aplicación. Realmente, cada elemento y su
comportamiento se hace corresponder con una serie de líneas de código (C++, Basic,
Pascal, etc.). Este código de alto nivel está más cerca de lo que entiende la máquina,
pero por otro lado, es más tedioso hacer esa aplicación escribiendo todas esas líneas de
código que seleccionando con el ratón los elementos, sus propiedades, su
comportamiento, etc.

Una vez que el entorno integrado ha generado ese código (de forma transparente al
programador), se guarda en ficheros, y se llama al compilador correspondiente (C++,
2 Desarrollo de un compilador-traductor

Basic, Pascal) para que traduzca esas instrucciones de alto nivel a otro lenguaje más
cercano a la máquina (ya que el procesador no entiende más que lenguaje máquina
binario). Entre el lenguaje de alto nivel y el código máquina, se utiliza el ensamblador.

Por último, se utiliza el compilador de ensamblador para traducir ese código


ensamblador (ya muy cercano a la máquina, y dependiente de la arquitectura del
microprocesador, y del sistema operativo) a lenguaje máquina binario ejecutable por el
procesador (es el código que sí entiende el microprocesador).

En cada paso, vemos que se va traduciendo el código fuente inicial a un lenguaje cada
vez más cercano al que entiende la máquina. En contrapartida, al traducir y bajar de
nivel, pasamos a manejar un lenguaje cada vez más complejo y difícil de entender para
un humano. Es más, desarrollar la misma aplicación en ese lenguaje de más bajo nivel,
si no se dispone del compilador adecuado, resultaría mucho más costoso en tiempo y
esfuerzo.

Estructura de un programa ensamblador.


En todo programa, esté escrito en el lenguaje que sea, hay una sintaxis fija; y en el
código habrá partes fijas (siempre hay que ponerlas) y en ocasiones será información al
compilador para saber cómo está estructurado el programa.

Veamos un programa en C++ muy sencillo que muestra una cadena de texto:

#include <iostream>
using namespace std;
char *cadena=”hola”;
int main(void) {
cout << cadena;
return 0;
}

El mismo programa, escrito en C estándar quedaría como sigue:

#include <stdlib.h>
char *cadena=”hola”;
int main(void) {
printf(”%s”,cadena);
return 0;
}

Salvo las dos líneas resaltadas, el resto siempre es fijo: los #include como información
al compilador, y la función main que establece una estructura y un punto de comienzo
del programa.

En un programa ensamblador ocurre algo similar. Sabemos (de Introducción a los


Computadores) que un programa necesita una pila, una zona de almacenamiento de
datos, y una zona donde reside el código ejecutable. Nosotros debemos definir en
nuestros programas de ensamblador esas tres zonas (o segmentos).

El siguiente programa es una traducción directa del programa C++ anterior. En éste
vemos la declaración de la zona de datos con todas las variables que teníamos definidas
en el programa de C++ (la línea resaltada definiendo la cadena “hola”), y por último el
3 Desarrollo de un compilador-traductor

segmento de código, que contiene las instrucciones ensamblador que ejecutan cada
instrucción de C++ :

section .data ; directiva que indica que comienzan los datos


cadena db "hola"
cadenaSIZE equ $ - cadena

section .text ; directiva que indica que comienza el codigo


global _start
_start:

mov ecx, cadena


mov edx, cadenaSIZE
mov eax, 4
mov ebx, 1
int 80h

mov eax, 1
mov ebx, 0
int 80h

En este ejemplo, la instrucción cout<<cadena; (o la printf(”%s”,cadena); en C


estándar) queda traducida por las cinco instrucciones de ensamblador que están
resaltadas.

En ocasiones, nuestros programas necesitan hacer uso de funciones complejas del


sistema operativo para, por ejemplo, mostrar una cadena de texto por pantalla, leer un
dato desde el teclado, etc.

Este tipo de funciones, que sólo las puede proveer el sistema operativo se llaman
interrupciones software. La forma de pedirle al sistema que ejecute cierta función para
servir a nuestro programa (así nuestro programa puede escribir en pantalla, leer de
teclado, etc.) es utilizando la instrucción de ensamblador INT (seguido del número de
interrupción, que identifica “a quién” le pedimos que ejecute esa función).

Vemos que en ese programa anterior se le pide al kernel de Linux (interrupción 80h)
que ejecute la función de sacar una cadena de texto (función 4) a salida estándar
(ebx=3). Al final del programa, vemos que se le pide de nuevo al kernel (interrupción
80h) que ejecute la función de terminar el programa y salir al sistema (función 1,
especificado en EAX).

Como vemos, los valores que identifican la función que queremos ejecutar para nuestro
programa, y los parámetros para esa función se indican en los registros del
microprocesador.

Traducción de un programa en C++ a ensamblador.


Acabamos de ver que utilizando el mismo “esqueleto”, y cambiando las instrucciones
de alto nivel por las correspondientes en ensamblador, es relativamente fácil traducir un
programa en C o C++ a ensamblador.

Veamos a continuación cómo traducir algunas funciones de C estándar a ensamblador,


indicando qué servicios de interrupción hacen la misma función. Sólo pondremos varias
4 Desarrollo de un compilador-traductor

funciones a modo de ejemplo. Para disponer de un listado más completo conviene


estudiar las referencias y enlaces listados al final del guión.

Función Código C / C++ Traducción a ensamblador


Escribir una cadena de texto printf(“%s”,cadena); mov eax, 4
por pantalla cout << cadena; mov ebx, 1
mov ecx, cadena
mov edx, cadenaSIZE
int 80h
Leer una cadena de texto desde mov eax, 3
teclado mov ebx, 0
cin >> buffer mov ecx, buffer
mov edx, 30
int 80h
Abrir un fichero, dada la ruta y ifstream F(nombre_f); mov ebx,nombre_fich
el nombre del fichero mov eax,5
F=open(nombre_f); mov ecx,0
int 0x80
mov [manipul_fich],eax
;dev. en EAX el manejador_F
Cerrar un fichero abierto, dado F.close(); mov ebx,[manipul_fich]
el manejador mov eax,6
close(F); int 0x80
Terminar el programa y salir mov eax, 1
devolviendo un código de exit( 0 ); mov ebx, 0
retorno int 80h

Creación de un lenguaje de programación sencillo. Un ejemplo


En esta práctica, lo primero que debemos hacer es inventarnos un lenguaje de
programación (muy sencillo), con una sintaxis definida, un conjunto de instrucciones
válidas en nuestro lenguaje (y que posteriormente nuestro compilador-traductor debe
reconocer y traducir a ensamblador), y el significado de cada una de esas instrucciones
(en definitiva, qué instrucciones ensamblador se ejecutarán al ejecutar cada instrucción
de alto nivel de nuestro lenguaje).

Supongamos que en nuestro lenguaje vamos a permitir el uso de variables de tipo


cadena de caracteres y de variables de tipo entero. Para ello, dichas variables se
definirán como globales, al principio del programa. Para delimitar la zona en la que se
declaren las variables vamos a utilizar las palabras clave “VARIABLES” y
“FIN_VARIABLES”.

Por otro lado, nuestros programas podrán hacer uso de las siguientes instrucciones:

Sintaxis Significado Instrucciones ASM


IMPRIMIR cadena Mostrar por pantalla la cadena de mov eax, 4
caracteres almacenada en la mov ebx, 1
variable “cadena” mov ecx, cadena
mov edx, cadenaSIZE
int 80h
INCREMENTAR variable Incrementar en una unidad el mov eax, [variable]
valor numérico de la variable inc eax
“variable” mov [variable], eax
DECREMENTAR variable Decrementar en una unidad el mov eax, [variable]
valor numérico de la variable dec eax
“variable” mov [variable], eax
5 Desarrollo de un compilador-traductor

Para delimitar el código del programa (la secuencia de instrucciones), vamos a utilizar
las palabras clave “INICIO_CUERPO” y “FIN_CUERPO”.

Supongamos que nos piden escribir un programa que muestre la cadena de caracteres
“hola”, y a continuación que muestre la cadena de caracteres “adios”.

Decidimos desarrollar el programa en nuestro lenguaje de programación. El programa


desarrollado podría ser el siguiente:

VARIABLES
cad "hola"
cad2 "adios"
FIN_VARIABLES
INICIO_CUERPO
IMPRIMIR cad
IMPRIMIR cad2
FIN_CUERPO

Vemos las dos zonas bien diferenciadas: la primera delimita la definición de las
variables que usaremos en nuestro programa. La segunda recoge las instrucciones que
componen el código de nuestro programa. En este ejemplo tan básico, el flujo del
programa es secuencial. Más adelante, tendríamos que definir cómo se construyen
bucles y estructuras condicionales.

El programa en alto nivel que mostrábamos antes, una vez traducido a ensamblador
(sintaxis NASM), quedaría como sigue:

section .data
cad db "hola"
cadSIZE equ $ - cad

cad2 db "adios"
cad2SIZE equ $ - cad2

section .text
global _start
_start:

mov eax, 4
mov ebx, 1
mov ecx, cad
mov edx, cadSIZE
int 80h

mov eax, 4
mov ebx, 1
mov ecx, cad2
mov edx, cad2SIZE
int 80h

mov eax, 1
mov ebx, 0
int 80h
6 Desarrollo de un compilador-traductor

Veamos cómo compilar el programa C++ (nuestro traductor-compilador) para conseguir


un programa que automáticamente pase de nuestro lenguaje de alto nivel a
ensamblador:

g++ -Wall -o lenguaje_ej lenguaje_ej.cc

Obtendremos un ejecutable que es realmente el que traducirá los archivos escritos en


nuestro lenguaje a ensamblador en sintaxis NASM. Lo ejecutamos para traducir el
ejemplo anterior y obtener el programa en código ensamblador:

./lenguaje_ej ej_primero.src miprog.asm

Ahora ya podemos compilar con el NASM ese programa:

nasm -f elf miprog.asm

ld -o miprog miprog.o

./miprog

Código de ayuda
Para construir nuevas instrucciones secuenciales, será de gran ayuda el código
entregado en la parte común a todas las prácticas para escribir cadenas de texto por
pantalla, convertir números en cadenas y viceversa, acceder a ficheros en modo texto
para lectura y escritura, etc. (cada función de interrupción que ejecuta cierta función, se
puede traducir por una instrucción de más alto nivel para nuestro lenguaje).

Para facilitar la comprensión de la práctica, y aclarar qué es lo que debemos diseñar y


desarrollar, ofrecemos el código C++ de un compilador para un lenguaje de
programación inventado y muy sencillo (sólo reconoce tres instrucciones, y ninguna
estructura condicional o bucles). Para no tener que teclear esas líneas de código,
podemos obtener un fichero ZIP que incluye el código C++, el ejecutable del
compilador, y ejemplos de programas construidos en ese lenguaje de programación
inventado.

//**************************************************************************//
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>
#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
using namespace std;
//**************************************************************************//
ifstream fichSRC;
ofstream fichASM;
//**************************************************************************//
string cabeceras_asm_1() {
string nuevo_codigo="";
nuevo_codigo = nuevo_codigo+"section .data \n";
return ((string) nuevo_codigo);
}
string cabeceras_asm_2() {
7 Desarrollo de un compilador-traductor

string nuevo_codigo="";
nuevo_codigo = nuevo_codigo+"section .text \n";
nuevo_codigo = nuevo_codigo+"global _start \n";
nuevo_codigo = nuevo_codigo+"_start: \n";
return ((string) nuevo_codigo);
}
string cabeceras_asm_3() {
string nuevo_codigo="";
nuevo_codigo = nuevo_codigo+"mov eax, 1 \n";
nuevo_codigo = nuevo_codigo+"mov ebx, 0 \n";
nuevo_codigo = nuevo_codigo+"int 80h \n\n";
return ((string) nuevo_codigo);
}
string definicion_de_variables() {
string nuevo_codigo="";
string token="";
string nombre="";
string valor="";
while( token != "VARIABLES" ) {
fichSRC >> token ;
}
bool quedan=true;
do{
fichSRC >> token ;
if( token == "FIN_VARIABLES" ) {
quedan=false;
}else{
// ya está leido el tipo de la variable.
// queda comprobar de qué tipo es y leer dos tokens: nombre y el valor.
// Parte de definición de cadenas (STR)
if( token == "STR" ) {
fichSRC >> nombre;
fichSRC >> valor;
nuevo_codigo=nuevo_codigo+"\t"+nombre+" db "+valor+" \n";
nuevo_codigo=nuevo_codigo+"\t"+nombre+"SIZE equ $- "+nombre+" \n";
}
// La parte de definición de "integers" no está terminado
if( token == "INT" ) {
fichSRC >> nombre;
fichSRC >> valor;
}
}
}while( quedan );
return ((string) nuevo_codigo);
}
string procesar_codigo() {
string nuevo_codigo="";
string token="";
while( token != "INICIO_CUERPO" ) {
fichSRC >> token ;
}
bool quedan=true;
do{
fichSRC >> token ;
if( token == "FIN_CUERPO" ) {
quedan=false;
}else{
if( token == "IMPRIMIR" ) {
fichSRC >> token ;
nuevo_codigo=nuevo_codigo+"mov eax, 4 \n";
nuevo_codigo=nuevo_codigo+"mov ebx, 1 \n";
nuevo_codigo=nuevo_codigo+"mov ecx, "+token+" \n";
nuevo_codigo=nuevo_codigo+"mov edx, "+token+"SIZE \n";
nuevo_codigo=nuevo_codigo+"int 80h \n\n";
}
if( token == "INCREMENTAR" ) {
fichSRC >> token ;
nuevo_codigo=nuevo_codigo+"mov eax, ["+token+"] \n";
8 Desarrollo de un compilador-traductor

nuevo_codigo=nuevo_codigo+"add eax, 1 \n";


nuevo_codigo=nuevo_codigo+"mov ["+token+"] ,eax \n\n";
}
}
}while( quedan );
return ((string) nuevo_codigo);
}
//**************************************************************************//
int main(int argc, char **argv) {
cout << "\n(c)2008 - Pedro Angel Castillo Valdivieso";
if( argc < 3) {
cout << "\nOPCIONES: "<<argv[0]<<" fich.src fich.asm " << endl;
return 0;
}

string nombre_fichSRC( argv[1] );


string nombre_fichASM( argv[2] );
cout << "\n\tTraducir: " << nombre_fichSRC << " -> " << nombre_fichASM <<
endl;

fichSRC.open( nombre_fichSRC.c_str() );
fichASM.open( nombre_fichASM.c_str() );

cout << "\t Insertamos las cabeceras ASM..." << endl ;


fichASM << cabeceras_asm_1() << endl;
cout << "\t Procesando la definición de variables..." << endl ;
fichASM << definicion_de_variables() << endl;
fichASM << cabeceras_asm_2() << endl;
cout << "\t Procesando el código del programa..." << endl ;
fichASM << procesar_codigo() << endl;
cout << "\t Terminamos la traducción." << endl ;
fichASM << cabeceras_asm_3() << endl;

fichSRC.close();
fichASM.close();
return 0;
}
//**************************************************************************//

Referencias y enlaces interesantes.


http://leto.net/writing/nasm.php
http://linuxassembly.org/
http://linuxassembly.org/howto/Assembly-HOWTO.html
http://www.janw.dommel.be/eng.html
http://navet.ics.hawaii.edu/~casanova/courses/ics312_fall07/nasm_howto.html
http://geneura.ugr.es/~pedro/docencia/ec1/enlaces_ec.htm
http://atc.ugr.es/~acanas/arquitectura.html
9 Desarrollo de un compilador-traductor

¿En qué consistirá la práctica?


Tomar el compilador entregado en este guión como código de ayuda y ampliarlo para
que reconozca (el lenguaje incluya y el compilador procese), como mínimo, las
siguientes funciones:

• mostrar una cadena de texto


• leer desde teclado una cadena de texto
• mostrar un número entero
• operaciones aritméticas enteras ( + , - , * , / )
• operaciones lógicas ( AND , OR , NOT , etc )
• borrar pantalla
• bucle del tipo “Repetir N veces”
• estructura condicional del tipo if (condición) then ....
• definición de varios tipos de datos (cadenas y enteros)
• acceso a ficheros

Para mejorar la práctica, y así subir nota, se pueden incluir en el lenguaje (y el


compilador), entre otras, las siguientes características:

• funciones y procedimientos
• estructura condicional del tipo if (condición) then .... else ....
• bucle del tipo do{ .... } while (condición)
• bucle del tipo for (i=0;i<limite;i++){....}
• acceso a los argumentos pasados por la línea de comando
• definición y uso de arrays unidimensionales (y/o multidimensionales)
• función GOTO etiqueta
• comprobación de errores de sintaxis y/o semántica

Una vez entregada, en la evaluación de la práctica se tendrá en cuenta:

• la documentación del lenguaje de programación propuesto (explicación


clara de cómo se utiliza ese lenguaje, descripción de cada una de las
características que soporta, ejemplos de programación, etc.)
• la cantidad de características del lenguaje que se han implementado, y la
dificultad de cada una
• que todas las características incluidas funcionen correctamente
• cómo de optimizado es el código ensamblador que se genera en la
traducción desde el lenguaje de alto nivel
• la claridad y organización del código del programa entregado
• que el código generado no tenga errores de programación

También podría gustarte