[Opinion] Programando un lenguaje ensamblador semicompilado en C

Iniciado por Miky Gonzalez, 7 Agosto 2013, 22:15 PM

0 Miembros y 1 Visitante están viendo este tema.

Miky Gonzalez

Me gustaría que me diesen alguna opinión acerca de las implementaciones actuales y en el apartado 'para hacer' las ideas que tengo. La máquina virtual está programada y es rápida. Usa memoria dinámica para el uso de registros y la pila. Pronto intentaré implementar para el uso de API y añadir funcionalidades propias sin modificar código interno.

Expecificaciones del lenguaje:
Se trata de un microlenguaje de programación ensamblador orientado al uso de pila, registros y operaciones aritméticas. Esta diseñado con el mero propósito de aprender.

Dispone de 8 registros (r0-r7)(tipo entero), elegidos así porque me parece un número de registros adecuado. No admite cadenas de caracteres ni números reales. Es un lenguaje semicompilado, es decir, sus intrucciones son transmitidas a opcodes que son emulados por una máquina virtual (actualmente, la
máquina virtual ya está en una buena fase de desarrollo).

Algunas especificaciones técnicas del lenguaje:
   · Debe respetarse el orden de parámetros
   · Los registros son numerados desde 0 a 7, indicando que es un registro con la letra 'r' precediendo al número.
   · Para definir el inicio del programa debera utilizarse la etiqueta %main% precedida de las intrucciones.
   · Para definir el fin del programa debera utilizarse la etiqueta %end main%
   · Para definir una funcion común (no implementado) deberá utilizarse la etiqueta %nombredefuncion,parametro1,param...% finalizando esta con %end nombredefuncion%
   · Pueden incluirse comentarios en el código con ' y '= ='.
   · Cada línea tendrá como máximo una función
   · Pueden definirse 'cotas' de código para hacer el código más pequeño, compartiendo biblioteca que pueden ser incluidas en el código con %include archivo.mvm%. Se dispondrá de version estándar  de algunas implementaciones para agilizar el uso y ampliar las funciones con el uso de API.

Función 'setr' - Establece el valor a un registro:
   setr [r0-r7] num1      ' Uso normal

Función 'add' - Función con la capacidad de sumar 2 enteros y guardar la operación en el registro indicado:
   add [r0-r7] num1,num2   ' Uso normal
   
Función 'addr' - Suma 2 enteros, uno de ellos dado por el valor de un registro. Con las funciones add y addr pueden hacerse todas las operaciones de suma desde sumar dos registros hasta sumar registro con datos de la pila:
   addr [r0-r7] rX,rX      ' Uso normal
   
Función 'neg' - Niega el valor de un registro dado:
   neg [r0-r7]            ' Uso normal
   
El equivalente a la función 'sub' se puede utilizar conjuntamente con 'addr' y 'neg':
   '= Queremos restar r1 - r0 y guardar valor en r3 ='
   neg r0
   addr r3 r1 r0
   
Función 'mult' - Multiplica 2 enteros y guarda la operación en el registro dado:
   mult [r0-r7] num1,num2   ' Uso normal
   
Función 'multr' - Multiplica el valor de 2 registros y guarda la  operación en el registro dado:
   multr [r0-r7] rX,rX      ' Uso normal
   
Función 'push' - Pone el valor de un registro en pila:
   push [r0-r7]         ' Uso normal
   
Función 'pop' - Elimina el tos:
   pop                  ' Uso normal

Función 'inrp' - Intercambia el valor de un registro y del tos:
   inrp [r0-r7]         ' Uso normal

El equivalente a 'inrr' (intercambiar dos registros, mov), puede hacerse con funciones ya descritas:
   '= Queremos intercambiar r0 y r1. Vamos a suponer que todos los
   registros tienen valores que deben ser guardados. ='
   push r0
   inrp r1
   inrp r0
   pop

Otra utilidad que tiene las funciones anteriores, no se necesita de función 'clonr' (clonar valor de un registro en otro), ya que, esta podria hacerse de muchas otras maneras con inrp o addr.

Función 'cmp' - Comparar si el valor de un registro y un valor proporcionado son iguales, mayor, menor (con respecto al registro):
   cmp [r0-r7]   num1      ' Uso normal

Función 'cmpr' - Comparar si el valor de dos registros son iguales, mayor, o menor (con respecto al primer registro):
   cmpr [r0-r7] [r0-r7]   ' Uso normal

Para hacer:
Cambiar la forma de analizar las funciones, para evitar usar funciones clonadas para operar con registros (add, addr, mult, multr...); Así, se podrá operar con los registros de una forma más fácil y comoda.
Añadir funciones condicionales, uso de cadenas, operaciones con cadenas.
Ampliar numero de funciones aritméticas (shl, shr, xor, div, mod...).
Mi blog personal, con información acerca de programación, seguridad, desarrollo y electrónica:

EN CONSTRUCCIÓN

Alien-Z

#1
Las ideas son el plato fuerte del día.

Primeramente, resumiendo tu proyecto: estás diseñando un compilador interpretado al estilo de java, generas código intermedio que equivaldría a los bytecodes pero en este caso para un lenguaje ensamblador. Dicho esto, ideas y anotaciones que se me ocurren:

-Puedes tomar como referencia MIPS o DLX, son lenguajes ensamblador muy completos, legibles sintácticamente hablando y programados de una manera muy eficiente; además tienes mucha documentación por internet.

-Cuando tengas avanzado el lenguaje, lo normal es que programes pequeñas aplicaciones para comprobar que todo funciona correctamente; ahora si, también debes saber que esa fase se utiliza para averiguar si hay algún dato o valor que se use repetidamente en las aplicaciones en general y así declararlo como una constante en un registro. El más común y que habremos visto todos en el 90% de lenguajes ensamblador es el famoso Registro Cero que siempre tiene el valor "0" porque resulta sobradamente útil para hacer comparaciones en bucles, copiar registros, etc.

-Quizá de las cosas más pesadas que te vas a encontrar es tratar en todas las instrucciones los espacios/tabulador y saltos de línea. Por ejemplo, has comentado que las funciones se definen del siguiente modo:

%nombredefuncion,parametro1,param...%

Instrucciones

%end nombredefuncion%


¿Y si entre la coma y el parámetro dejo un espacio?, ¿y si dejo 10 espacios?.

Esto me recuerda: ¿Al final has utilizado las expresiones regulares para el analizador léxico-sintáctico como te comenté en el otro post?, si es así no vas a tener problemas para reconocer comentarios, saltos de línea sin ningún valor para el programa, espacios seguidos, etc.

-El anterior punto es bastante importante porque cuando generes el bytecodes tienes que procurar que sea un fichero pensado únicamente para su comprensión por parte del PC (como no usa un compilador sino un compilador interpretado realmente hablamos de la máquina virtual); es decir, los comentarios, espacios, saltos de línea, y demás carácteres que solo ayudan a la legibilidad del programador deben ser eliminados al crear el fichero intermedio (bytecode u opcode), así harás el programa más ligero y rápido cuando llegue el momento en que la máquina virtual lo interprete y ejecute las instrucciones.

-¿Las instrucciones son monoformato o multiformato?.

-Has escrito algo muy importante pero un poco implícito en el mensaje, por si acaso no le has dado vueltas te lo comento para que lo tengas en cuenta: Debes organizar el lenguaje de manera que:

1- Tenga el menor número posible de instrucciones.
2- Las instrucciones realicen operaciones básicas. A partir de instrucciones aritmetico-lógicas elementales, el programador construirá otras más complejas.


Estos dos puntos harán que la ejecución de los programas creados con tu lenguaje sean 100 veces más eficientes. Como se podrán incluir librerías o API's, para mantener la eficiencia que conseguimos en el apartado anterior te recomiendo que:

1- La inlusión de estas librerías sea totalmente opcional.
2- Su especificación e implementación sea libre y cualquiera pueda ver cómo está diseñada; esto permite que un programador de tu lenguaje pueda comprobar si la manera en que está implementada la librería merece la pena incluirla o no (lleva demasiadas funciones (o tareas en ensamblador) y prefiere crear él mismo las que necesita); parece una tontería desde el punto de vista de un lenguaje de alto nivel pero estamos hablando de un lenguaje ensamblador.

-No me ha quedado claro si vas a incluir una instrucción para copiar un registro a otro, pero resultaría una operación sobrecargada, es decir, puedes hacer lo mismo con otras más básicas:

r0; <- Registro Cero
addr r2, r1, r0; <- Hemos copiado el valor de r1 en el r2


-Para intercambiar registros se puede usar un tercero que sirva de auxiliar o la pila, tampoco merece la pena crear una operación solo para eso.

Un proyecto genial, espero que vayas comentando según avances. Si se me ocurre algo más lo posteo; plantea cualquier duda que tengas.

Un saludo.



EI: juntando mensajes.


Nueva anotación:

Si aún así te da por incluir operaciones complejas existe un método para mantener la eficiencia: "pre-traductor" (no precompilador). Es un proceso que se ejecuta antes del compilador y que se dedica a traducir las operaciones complejas a otras más elementales. Por ejemplo, el programador diseña el siguiente código:

addr r2, r1, r3
sub r2, r4, r5
operacion_compleja
addr r2, r1, r3


Antes de que se ejecute el compilador un proceso debe traducir la operación a su equivalente en instrucciones básicas:

addr r2, r1, r3
sub r2, r4, r5
addr r0, r1, r4 //operacion_elemental 1
setr r0, 8 //operacion_elemental 2
... //operacion elemental x
addr r2, r1, r3


Seguro que te suena esto y resulta que se trata de la extensión que pones en la consola de comandos cuando quieres ejecutar un compilador de ensamblador para que aplique ténicas como: desenrrollado de bucles, intercambio de iteraciones, arrays combinados, etc. aunque lo que te he comentado mucho más sencillo que cualquiera de estas.

Ya nos dirás qué camino has seguido en tu proyecto. Un saludo.

Miky Gonzalez

#2
Gracias por comentar, leo todos los comentarios, aunque no siempre tengo tiempo para contestar.
Leí todas las indicaciones y miré algunas máquinas virtual y frameworkd como lo son la JVM y la plataforma .NET. Algunas notaciones que pusistes me parecieron útiles, que no pensé demasiado en ellas. El analizador léxico-sintáctico lo estoy programando en este instante, pero el tema de los espacios, saltos de línea no es de momento inconveniente, funcionando de la siguiente forma y manera:
1. Leo el archivo y paso su contenido (exceptuando comentarios ' y '= hasta =') a un buffer dinámico (así me aseguro).
2. Hago un parser inicial eliminando datos "obvios/inutiles", como son espacios entre funciones, declaraciones; respetando las reglas de conducta que se deben seguir. En esta etapa se deben precalcular resultados (por ejemplo add r1 (23+2+32+3+2),(23+2+3+2)).
3. Hago un segundo parser comprobando sintexis y pasando a opcode y guardando estos en un archivo. Por supuesto, está echo para que el fichero contenga toda la información y ocupe lo menos posible.

Es la forma más rápida, eficiente y limpia que encontré de programar el conversor a opcodes.

Para aclararte, no esta en mis pensamientos incluir una función para copiar el valor de un registro en otro. Como especifico, puedes hacer una subrutina a forma de libreria libre. Por supuesto, que todas las librerias que se usen a modo de API, serán libres, y espero que la máquina virtual tambien. De momento no puedo poner el código, pero si quieres echarle un vistazo, contactame.

He decidido modificar el proyecto para seguir otro camino, pero sin salirse del camino de aprender y hacer aprender. Pongo los cambios y las nuevas especificaciones:
La utilización de registros, tratándose de ellos, ahora se utilizará (como antes) precedida del carácter 'r'. La utilización de valores numéricos se especificará con '#'. Por ejemplo:
  add r2 r5 #50 ' guarda en el registro 2 la suma del registro 5 más 50 (decimal)

Implementaré funciones simples (que pueden dar lugar a funciones complejas) de utilización de posiciones de memoria dentro de la máquina virtual (punteros para acceder a ellas). Para especificar una posición de memoria debe utilizarse el carácter @: @1000 ' posicion de memoria 1000 (decimal).

Una especificación que me faltó de dar en el post principal es la generación de los opcodes, como es por ejemplo: una función sin parámetros utiliza un byte de tamaño: 0x00. Si utiliza parámetros, dependiendo de estos: 0x00 0x00 0x00 (funcion param1 param2).

  Función 'halt' - Tendra el opcode 0x00. Obviamente, esto detendrá la máquina virtual cualquier punto en que esta se encuentre. Tiene como opcode 0x00 porque supongamos que hay un error de programación y se empieza a leer fuera de memoria, la mayor parte de esta esta llena de ceros.

La idea que tuve es en hacer que los elementos de la pila tengan dirección de memoria (virtual en la máquina virtual), al ejemplo del elemento 1 (el último añadido) tendría la última dirección de memoria posible. Esto es así porque la pila se llena desde abajo.

Saludos,
   Miky Gonzalez
Mi blog personal, con información acerca de programación, seguridad, desarrollo y electrónica:

EN CONSTRUCCIÓN

eferion

Cita de: Miky Gonzalez en  8 Agosto 2013, 16:26 PM
Implementaré funciones simples (que pueden dar lugar a funciones complejas) de utilización de posiciones de memoria dentro de la máquina virtual (punteros para acceder a ellas). Para especificar una posición de memoria debe utilizarse el carácter @: @1000 ' posicion de memoria 1000 (decimal).

Los procesadores más eficientes suelen tener juegos de instrucciones simples... es más sencillo, por ejemplo, programar paralelismo de instrucciones si estas son sencillas a si se eligen juegos de instrucciones complejos.

Aplicado a esto te doy la razón, la mejor solución es mantener un juego de instrucciones simple y versátil... es más sencillo de depurar, mantener y optimizar.

Por lo demás la verdad es que tiene muy buena pinta tu proyecto. Sigue así ;)