2 Practica Compilador NASM

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++,

Transcript of 2 Practica Compilador NASM

Page 1: 2 Practica Compilador NASM

© 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++,

Page 2: 2 Practica Compilador NASM

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

Page 3: 2 Practica Compilador NASM

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

Page 4: 2 Practica Compilador NASM

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

por pantalla

printf(“%s”,cadena);

cout << cadena;

mov eax, 4

mov ebx, 1

mov ecx, cadena

mov edx, cadenaSIZE

int 80h

Leer una cadena de texto desde

teclado

cin >> buffer

mov eax, 3

mov ebx, 0

mov ecx, buffer

mov edx, 30

int 80h

Abrir un fichero, dada la ruta y

el nombre del fichero

ifstream F(nombre_f);

F=open(nombre_f);

mov ebx,nombre_fich

mov eax,5

mov ecx,0

int 0x80

mov [manipul_fich],eax

;dev. en EAX el manejador_F

Cerrar un fichero abierto, dado

el manejador

F.close();

close(F);

mov ebx,[manipul_fich]

mov eax,6

int 0x80

Terminar el programa y salir

devolviendo un código de

retorno

exit( 0 ); mov eax, 1

mov ebx, 0

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

caracteres almacenada en la

variable “cadena”

mov eax, 4

mov ebx, 1

mov ecx, cadena

mov edx, cadenaSIZE

int 80h

INCREMENTAR variable Incrementar en una unidad el

valor numérico de la variable

“variable”

mov eax, [variable]

inc eax

mov [variable], eax

DECREMENTAR variable Decrementar en una unidad el

valor numérico de la variable

“variable”

mov eax, [variable]

dec eax

mov [variable], eax

Page 5: 2 Practica Compilador NASM

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

Page 6: 2 Practica Compilador NASM

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() {

Page 7: 2 Practica Compilador NASM

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";

Page 8: 2 Practica Compilador NASM

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

Page 9: 2 Practica Compilador NASM

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