Inicio Proyectos Lenguaje FKScript Ensamblador FKScript – FKIL

Ensamblador FKScript – FKIL

por sgoliver

Esta entrada forma parte de una serie de artículos dedicados al proyecto FKScript, construcción de un compilador y una máquina virtual para un lenguaje de script con C# y ANTLR, entre los cuales podrás encontrar una descripción detallada de cada módulo, documentación técnica, ejemplos y tutoriales de uso que pueden ser de tu interés. No olvides consultar la página principal de FKScript para más información.

Una vez hemos construido el compilador para el lenguaje FKScript, que se encargará de transformar el lenguaje de script de alto nivel en código intermedio FKIL, necesitamos un nuevo módulo para transformar éste último en el código binario final que será interpretado por la máquina virtual. En esta sección nos pararemos a detallar la implementación en C# del módulo ensamblador para FKIL.

1. Tareas del ensamblador

Las tareas a realizar por el emsamblador serán las siguientes:

  1. Comprobar la validez del código FKIL, es decir, comprobar que la estructura del programa es correcta, comprobar la validez de las directivas e instrucciones utilizadas y validar sus parámetros.
  2. Comprobar y traducir las etiquetas utilizadas en el programa.
  3. Construir la tabla de literales y resolver sus referencias.
  4. Generar el fichero binario ejecutable del programa.

Para llevar a cabo todas estas tareas se realizarán dos pasadas al fichero de entrada. En la primera de ellas (método generacionP1()) se verificará el código FKIL y se generarán las tablas de traducción de etiquetas y literales. En la segunda pasada (método generacionP2()) se generará el código binario del programa resolviendo convenientemente todas las referencias a etiquetas y literales a partir de las tablas de traducción ya construidas.

public void ensamblar(string pathEntrada, string pathSalida)
{
ensambladoOK = true;

//Primera pasada
generacionP1(pathEntrada);

//Segunda pasada (si la primera no ha producido errores)
if(ensambladoOK)
generacionP2(pathEntrada, pathSalida);
}

2. Estructuras de datos

Para la realización de todas las tareas comentadas el ensamblador necesitará disponer de varias estructuras de datos para almacenar toda la información necesaria durante el ensamblado:

  • Ficheros de entrada (FKIL) y salida (Binario).
  • Contador de programa.
  • Tabla de instrucciones.
  • Tabla de literales.
  • Tabla de cadenas.
  • Cabecera del fichero de salida.
//Tabla de Instrucciones
private Dictionary<string,Instruccion> instSet = null;

//Fichero de entrada
private StreamReader fe = null;

//Fichero de salida
private BinaryWriter fsal = null;

//Tabla de literales
private List<string> cadenas = new List<string>();

//Tabla de etiquetas
private Dictionary<string, int> etiquetas = new Dictionary<string, int>();

//Cabecera
private Cabecera cab = new Cabecera();

//Contador de programa
private int progCounter = 0;

3. Inicialización del ensamblador

Como primer paso del ensamblado vamos a inicializar la tabla de intrucciones. Esta tabla se utilizará tanto para la validación de las intrucciones (operadores y parámetros) del programa como para la posterior generación del fichero de salida.

Esta tabla consistirá en una colección de objetos que nos indiquen, para cada instrucción válida, su nombre, su número de parámetros y su código binario. Para ello, definiremos en primer lugar una clase para almacenar esta información que llamaremos Instruccion:

class Instruccion
{
public string nombre;   //Nombre de la instrucción
public int opcode;      //Código de la instrucción
public int numpar;      //Número de parámetros

public Instruccion(string nombre, int opcode, int numpar)
{
this.nombre = nombre;
this.opcode = opcode;
this.numpar = numpar;
}
}

Una vez contamos con esta clase, la inicialización de la tabla de instrucciones se limitará a añadir a la colección uno de estos objetos por cada instrucción válida de nuestro lenguaje intermedio FKIL:

private void inicializarInstSet()
{
instSet = new Dictionary<string, Instruccion>();

instSet.Add("ipush", new Instruccion("ipush", 1, 1));
instSet.Add("fpush", new Instruccion("fpush", 2, 1));
instSet.Add("spush", new Instruccion("spush", 3, 1));
instSet.Add("bpush", new Instruccion("bpush", 4, 1));
instSet.Add("iload", new Instruccion("iload", 5, 1));
instSet.Add("fload", new Instruccion("fload", 6, 1));
instSet.Add("sload", new Instruccion("sload", 7, 1));
//....
}

4. Primera pasada del ensamblador

Como ya comentamos anteriormente, durante la primera pasada del ensamblador se realizará la validación de las instrucciones del programa y la generación de las tablas de literales y etiquetas que se utilizarán posteriormente para generar el fichero final ejecutable.

En primer lugar deberemeos identificar aquellas líneas del programa que no debemos procesar, como son los comentarios y las lineas en blanco. Posteriormente, para el resto de líneas decidiremos si se trata de una directiva, una etiqueta o una instrucción y actuaremos en consecuencia llamando a métodos independientes.

private void generacionP1(string pathEntrada)
{
string linea;

//Se abre el fichero de entrada
fe = File.OpenText(pathEntrada);

linea = fe.ReadLine();

//Se procesan todas las lineas que no sean comentarios ni lineas en blanco
while (linea != null)
{
if (linea != null)
{
linea = linea.Trim();

if (!linea.StartsWith("#") &amp;amp;&amp;amp; !linea.Equals(""))
{
procesarLineaP1(linea);
}
}

linea = fe.ReadLine();
}

fe.Close();
}

private void procesarLineaP1(string linea)
{
if (linea.Trim().EndsWith(":"))         //Etiqueta
{
procesarEtiquetaP1(linea);
}
else if (linea.Trim().StartsWith("."))  //Directiva
{
procesarDirectivaP1(linea);
}
else                                    //Instrucción
{
procesarInstruccionP1(linea);
}
}

4.1. Procesamiento de directivas

Las directivas soportadas por FKIL no darán como resultado ninguna instrucción ejecutable en el programa final, y tan sólo se utilizarán para completar la información incluida en la cabecera del fichero ejecutable, que contendrá datos generales sobre el formato del fichero, el programa, y el entorno de ejecución que creará la máquina virtual para albergarlo.

Para almacenar y tratar esta información construiremos una nueva clase llamada Cabecera:

public class Cabecera
{
public int magic          = 8080;  //Identificación del formato de fichero
public int version        = 1;     //Versión del formato de fichero
public int revision       = 0;     //Revisión del formato de fichero
public string programName = "";    //Nombre del programa
public int stackSize      = 1024;  //Tamaño de la pila
public int heapSize       = 1024;  //Tamaño de la memoria dinamica
public int nConst         = 0;     //Número de elementos en la tabla de literales
public int nLocals        = 0;     //Número de variables locales utilizadas

public Cabecera(){}
}

Como vimos en la sección sobre la especificación del lenguaje FKIL, son cuatro las directivas que podemos incluir en un programa escrito en este lenguaje: .program, .stack, .heap y .locals , que se corresponden directamente con cuatro de los datos almacenados en la cabecera por lo que su procesamiento por parte del ensamblador será directo. Nos limitaremos a leer cada una de las directivas y trasladar su información asociada a la cabecera:

private void procesarDirectivaP1(string linea)
{
//Se separa la directiva de su parámetro asociado
string[] tokens = linea.Split(new char[] { ' ' });

//Se rellena su atributo correspondiente de la cabecera
if (tokens[0].StartsWith(".program"))
{
cab.programName = tokens[1];
}
else if (tokens[0].StartsWith(".stack"))
{
cab.stackSize = Convert.ToInt32(tokens[1]);
}
else if (tokens[0].StartsWith(".heap"))
{
cab.heapSize = Convert.ToInt32(tokens[1]);
}
else if (tokens[0].StartsWith(".locals"))
{
cab.nLocals = Convert.ToInt32(tokens[1]);
}
}

4.2. Procesamiento de etiquetas

El procesamiento de las etiquetas utilizadas en el programa será aún más sencillo que el de las directivas, ya que tan sólo deberemos almacenar las etiquetas encontradas en la tabla de etiquetas junto a la posición que ocupan dentro del programa. Esta posición se obtiene a partir de la variable progCounter, que se ira actualizando, como veremos después, a medida que se procesan y validan instrucciones ejecutables.

private void procesarEtiquetaP1(string linea)
{
etiquetas.Add(linea.Substring(0, linea.Length - 1), progCounter);
}

4.3. Procesamiento de instrucciones

Durante el procesamiento de cada instrucción deberemos realizar las siguientes tareas:

  1. Validación de la instrucción: el operador tendrá que ser uno de los soportados por el lenguaje.
  2. Validación de los parámetros de la instrucción: el número de parámetros de la instrucción deberá coincidir con la información contenida en la tabla de instrucciones.
  3. Actualización del contador de programa: se actualizará convenientemente la variable progCounter según la instrucción procesada y su número de parámetros.
  4. Actualización de la tabla de literales: se añadirá el literal correspondiente a la tabla de literales cada vez que se procese una instrucción que acepte este tipo de parámetros (SPUSH y CALLAPI).
private void procesarInstruccionP1(string linea)
{
//Separamos la instrucción de sus parámetros
string[] tokens = linea.Split(new char[] { ' ' });

//Buscamos la instrucción en la tabla de instrucciones
Instruccion inst = instSet[tokens[0]];

//Si la instrucción existe
if (inst != null)
{
//Si el número de parámetros de la instrucción es correcto
if ((tokens.Length - 1) == inst.numpar)
{
//Se actualiza el contador de programa
progCounter += inst.numpar + 1;

//Si es una instrucción SPUSH o CALLAPI almacenamos el literal en la tabla de cadenas
if (tokens[0].Equals("spush"))
{
cadenas.Add(tokens[1].Substring(1, tokens[1].Length - 2));
}
else if (tokens[0].Equals("callapi"))
{
cadenas.Add(tokens[1]);
}
}
else
{
Console.WriteLine("Número de parámetros incorrecto:");
Console.WriteLine(linea);
ensambladoOK = false;
}
}
else
{
Console.WriteLine("Instrucción inexistente:");
Console.WriteLine(linea);
ensambladoOK = false;
}
}

5. Segunda pasada del ensamblador

En la primera pasada del módulo ensamblador nos hemos preocupado de validar el programa FKIL y de recopilar toda la información necesaria para la verdadera generación del programa ejecutable final. En esta segunda pasada nos preocuparemos tan sólo de las instrucciones ya que como dijimos son los únicos elementos que generarán el código ejecutable del programa, el resto de elementos tan sólo servirán de apoyo para generar correctamente el fichero de salida.

En primer lugar abriremos el fichero de salida y escribiremos toda la información de la cabecera que hemos generado durante la primera pasada. Esto lo haremos mediante la llamada al método escribirCabecera() cuya implementación es directa:

private void escribirCabecera()
{
//Cabecera
fsal.Write(cab.magic);
fsal.Write(cab.version);
fsal.Write(cab.revision);
fsal.Write(cab.programName);
fsal.Write(cab.stackSize);
fsal.Write(cab.heapSize);
fsal.Write(cab.nLocals);
fsal.Write(cadenas.Count);

//Tabla de literales
foreach (string c in cadenas)
fsal.Write(c);
}

Tras escribir la cabecera, recorreremos de nuevo el fichero de entrada pero esta vez procesando tan solo las lineas que contienen instrucciones ejecutables:

private void generacionP2(string pathEntrada, string pathSalida)
{
string linea;

//Abrimos el fichero de entrada
fe = File.OpenText(pathEntrada);

//Abrimos el fichero de salida
fsal = new BinaryWriter(new FileStream(pathSalida, FileMode.Create));

//Generación de la Cabecera
escribirCabecera();

//Generación del Código
linea = fe.ReadLine();

while (linea != null)
{
if (linea != null)
{
//Si no es un comentario o una linea en blanco se procesa
if (!linea.Trim().StartsWith("#") &amp;amp;&amp;amp; !linea.Trim().Equals(""))
{
procesarLineaP2(linea);
}
}

linea = fe.ReadLine();
}

fe.Close();

fsal.Flush();
fsal.Close();
}

private void procesarLineaP2(string linea)
{
//Procesamos tan sólo la líneas que contengan instrucciones
if(!linea.Trim().StartsWith(".") &amp;amp;&amp;amp; !linea.Trim().EndsWith(":"))
{
procesarInstruccionP2(linea);
}
}

Por último, el procesamiento de cada instrucción será una tarea sencilla, debiéndonos preocupar tan sólo de escribir al fichero de salida el código de la instrucción leída junto a sus parámetros en caso de existir. Para ello, nos basaremos una vez más en la información de la tabla de instrucciones.

private void procesarInstruccionP2(string linea)
{
//Separamos la instrucción de sus posibles parámetros
string[] tokens = linea.Split(new char[] { ' ' });

//Buscamos la instrucción en la tabla de instrucciones
Instruccion inst = instSet[tokens[0]];

//Escribimos el código de la instrucción al fichero de salida
fsal.Write((float)inst.opcode);

//Si la instrucción tiene ún parámetro simple
if (inst.nombre.Equals("ipush") ||
inst.nombre.Equals("iload")  ||
inst.nombre.Equals("istore") ||
inst.nombre.Equals("bpush")  ||
inst.nombre.Equals("bload")  ||
inst.nombre.Equals("bstore") ||
inst.nombre.Equals("sload")  ||
inst.nombre.Equals("sstore") ||
inst.nombre.Equals("fpush")  ||
inst.nombre.Equals("fload")  ||
inst.nombre.Equals("fstore"))
{
fsal.Write(Convert.ToSingle(tokens[1]));
}
//Si la instrucción tiene asociada una etiqueta
else if (inst.nombre.Equals("goto") ||
inst.nombre.Equals("ifeq") ||
inst.nombre.Equals("ifne") ||
inst.nombre.Equals("ifgt") ||
inst.nombre.Equals("ifge") ||
inst.nombre.Equals("iflt") ||
inst.nombre.Equals("ifle"))
{
fsal.Write((float)etiquetas[tokens[1]]);
}
//Si la instrucción tiene asociada un literal cadena
else if (inst.nombre.Equals("spush"))
{
fsal.Write((float)cadenas.IndexOf(tokens[1].Substring(1, tokens[1].Length - 2)));
}
//Si la instrucción tiene asociada un nombre de función
else if(inst.nombre.Equals("callapi"))
{
fsal.Write((float)cadenas.IndexOf(tokens[1]));
}
}

Como podemos ver en el código anterior, el procesador comenzará escribiendo el código de la instrucción al fichero de salida. Posteriormente decidirá si debe escribir algún dato más asociado a dicha instrucción, como puede ser un parámetro numérico, una referencia a etiqueta o una referencia a un literal.

En el primer caso, parámetro numérico, se escribirá el parámetro directamente al fichero de salida. En caso de referencias a etiquetas se traducirá el nombre de la etiqueta por su posición en el código, información que teníamos ya almacenada en la tabla de etiquetas. Por último, para instrucciones que hacen referencia a cadenas de caracteres (SPUSH) o nombres de función (CALLAPI) resolveremos dicha referencia consultando la tabla de literales que hemos construido durante la primera pasada del ensamblador.

Podéis descargar el código del ensamblador y todas las clases asociadas pulsando aquí.

Esta entrada forma parte de una serie de artículos dedicados al proyecto FKScript, construcción de un compilador y una máquina virtual para un lenguaje de script con C# y ANTLR, entre los cuales podrás encontrar una descripción detallada de cada módulo, documentación técnica, ejemplos y tutoriales de uso que pueden ser de tu interés. No olvides consultar la página principal de FKScript para más información.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información. Aceptar Más Información

Política de Privacidad y Cookies