8

Estoy haciendo un ensamblador 8051.Diseño de patrón para hacer un ensamblador

Antes de todo es un señalizador que dice siguientes fichas, establece indicadores de error, reconoce EOF, etc.
Luego está el bucle principal del compilador, que lee próximos fichas y compruebe de las sentencias válidas:

mnemonic= NextToken(); 
if (mnemonic.Error) 
{ 
    //throw some error 
} 
else if (mnemonic.Text == "ADD") 
{ 
    ... 
} 
else if (mnemonic.Text == "ADDC") 
{ 
    ... 
} 

Y continúa en varios casos. Peor que eso es el código dentro de cada caso, que verifica los parámetros válidos y luego los convierte en código compilado. En este momento se ve así:

if (mnemonic.Text == "MOV") 
{ 
    arg1 = NextToken(); 
    if (arg1.Error) { /* throw error */ break; } 
    arg2 = NextToken(); 
    if (arg2.Error) { /* throw error */ break; } 

    if (arg1.Text == "A") 
    { 
     if (arg2.Text == "B") 
      output << 0x1234; //Example compiled code 
     else if (arg2.Text == "@B") 
      output << 0x5678; //Example compiled code 
     else 
      /* throw "Invalid parameters" */ 
    } 
    else if (arg1.Text == "B") 
    { 
     if (arg2.Text == "A") 
      output << 0x9ABC; //Example compiled code 
     else if (arg2.Text == "@A") 
      output << 0x0DEF; //Example compiled code 
     else 
      /* throw "Invalid parameters" */ 
    } 
} 

Para cada uno de los mnemónicos que tengo que comprobar si hay parámetros válidos a continuación, crear el código compilado correcta. Códigos muy similares para verificar los parámetros válidos para cada repetición mnemónica en cada caso.

¿Existe un patrón de diseño para mejorar este código?
¿O simplemente una forma más simple de implementar esto?

Editar: Acepté la respuesta de plinth, gracias a él. Aún si tienes ideas sobre esto, estaré feliz de aprenderlas. Gracias a todos.

Respuesta

7

He escrito varios ensambladores a lo largo de los años analizando a mano y, francamente, es mejor que uses un lenguaje gramatical y un generador de analizadores.

He aquí por qué - una línea de montaje típica probablemente se verá algo como esto:

[label:] [instruction|directive][newline] 

y una instrucción será:

plain-mnemonic|mnemonic-withargs 

y una directiva será:

plain-directive|directive-withargs 

etc.

Con un generador de analizador decente como Gold, debería poder eliminar una gramática de 8051 en unas pocas horas. La ventaja de este análisis es sobre la mano que va a ser capaz de tener bastante complicada expresiones en su código ensamblador como:

.define kMagicNumber 0xdeadbeef 
CMPA #(2 * kMagicNumber + 1) 

que puede ser un oso real para hacer a mano.

Si desea hacerlo a mano, haga una tabla de todos sus mnemotécnicos que también incluirán los distintos modos de direccionamiento permitidos que admiten y para cada modo de direccionamiento, el número de bytes que tomará cada variante y el código de operación para ello. Algo como esto:

enum { 
    Implied = 1, Direct = 2, Extended = 4, Indexed = 8 // etc 
} AddressingMode; 

/* for a 4 char mnemonic, this struct will be 5 bytes. A typical small processor 
* has on the order of 100 instructions, making this table come in at ~500 bytes when all 
* is said and done. 
* The time to binary search that will be, worst case 8 compares on the mnemonic. 
* I claim that I/O will take way more time than look up. 
* You will also need a table and/or a routine that given a mnemonic and addressing mode 
* will give you the actual opcode. 
*/ 

struct InstructionInfo { 
    char Mnemonic[4]; 
    char AddessingMode; 
} 

/* order them by mnemonic */ 
static InstructionInfo instrs[] = { 
    { {'A', 'D', 'D', '\0'}, Direct|Extended|Indexed }, 
    { {'A', 'D', 'D', 'A'}, Direct|Extended|Indexed }, 
    { {'S', 'U', 'B', '\0'}, Direct|Extended|Indexed }, 
    { {'S', 'U', 'B', 'A'}, Direct|Extended|Indexed } 
}; /* etc */ 

static int nInstrs = sizeof(instrs)/sizeof(InstrcutionInfo); 

InstructionInfo *GetInstruction(char *mnemonic) { 
    /* binary search for mnemonic */ 
} 

int InstructionSize(AddressingMode mode) 
{ 
    switch (mode) { 
    case Inplied: return 1; 
    /* etc */ 
    } 
} 

, entonces tendrá una lista de todas las instrucciones que a su vez contiene una lista de todos los modos de direccionamiento.

Así que su analizador se convierte en algo parecido a esto:

char *line = ReadLine(); 
int nextStart = 0; 
int labelLen; 
char *label = GetLabel(line, &labelLen, nextStart, &nextStart); // may be empty 
int mnemonicLen; 
char *mnemonic = GetMnemonic(line, &mnemonicLen, nextStart, &nextStart); // may be empty 
if (IsOpcode(mnemonic, mnemonicLen)) { 
    AddressingModeInfo info = GetAddressingModeInfo(line, nextStart, &nextStart); 
    if (IsValidInstruction(mnemonic, info)) { 
     GenerateCode(mnemonic, info); 
    } 
    else throw new BadInstructionException(mnemonic, info); 
} 
else if (IsDirective()) { /* etc. */ } 
+0

¡Mi memoria es de aproximadamente 200 KiB y estoy tan contento de que hayas reemplazado el código anterior orientado a objetos con este! Ahora una pregunta de seguimiento (lo sé ...): ¿Fue su forma anterior de usar 'Instruction8051 {public string Mnemonic {get; conjunto; } public List Info {get; conjunto; }} 'el patrón de comando? ¿Y hay razones/situaciones en las que prefiero el patrón de comando para buscar tablas? – Hossein

+0

¿Patrón de comando? No, ambos habrían estado enfocados en las tablas de búsqueda. El código anterior de C# es uno que aprovecha el soporte del idioma para colecciones genéricas y propiedades automáticas. – plinth

3

Sí. La mayoría de los ensambladores usan una tabla de datos que describe las instrucciones: mnemónico, código de operación, formularios de operandos, etc.

Sugiero consultar el código fuente de as. Tengo algunos problemas para encontrarlo. Mire here. (Gracias a Hossein.)

+0

@wallyk: Ya pensé en una tabla de consulta que contiene mnemotécnicos, el número de parámetros para cada tecla de acceso, sus tipos, etc, pero la velocidad y la memoria son importantes para mí aquí, ya que se ejecutará en una máquina con poco ram y procesador lento. Entonces creo que será mi última opción. – Hossein

+0

@Hossein: una tabla de búsqueda probablemente no requeriría más memoria que tener varias copias del código del caso que tiene actualmente. En cuanto a rendimiento, probablemente no notará ninguna diferencia. Su código ya está utilizando comparaciones de cadenas y llamadas a funciones, por lo que no es como si estuviera optimizando un circuito interno cerrado. Recuerde la regla 80/20. – Anton

+1

El enfoque basado en tablas es muy probado y cierto para pequeñas memorias, implementaciones de procesadores lentos. Considere cuánto código se necesita para configurar cada comparación en su enfoque. En el enfoque basado en tablas, solo hay un bit de código que hace la comparación para, digamos, el mnemónico. Está dentro de un ciclo que no es más lento que una secuencia de comparaciones literales. Pruebe ambas cosas: estoy seguro de que encontrará que la mesa es superior en muchos aspectos. – wallyk

0

Creo que deberías mirar el patrón Visitor. Puede que su código no sea mucho más simple, pero reducirá el acoplamiento y aumentará la reutilización. SableCC es un marco de java para compilar compiladores que lo usan de forma extensiva.

+0

¿Estás seguro? Veo Visitantes principalmente usados ​​en árboles (como la T en AST). No necesita/tiene exactamente un árbol en un ensamblador, a diferencia de un compilador para un lenguaje más grande. – delnan

+0

No, pero podría tener una clase mnemónica que podría definir su código de salida a través de un patrón de visitante. Haces un bucle en los Mnemonics, les pasas el visitante, ellos devuelven su propia salida. Nunca diseñé un ensamblador, así que solo es una suposición, je. –

0

¿Has mirado el patrón "Command Dispatcher"?

http://en.wikipedia.org/wiki/Command_pattern

La idea general sería la creación de un objeto que se encarga de cada instrucción (comando), y crear una tabla de consulta que mapea cada instrucción a la clase de controlador. Cada clase de comando tendría una interfaz común (Command.Execute (* args) por ejemplo) que definitivamente le daría un diseño más limpio/más flexible que su declaración de conmutación enorme actual.

+0

Prefiero alejarme de la orientación a objetos "demasiado" debido a problemas de rendimiento. – Hossein

0

Cuando estaba jugando con una herramienta de emulador de microcódigo, convertí todo en descendientes de una clase Instruction. De Instruction fueron clases de categoría, como Arithmetic_Instruction y Branch_Instruction. Usé un patrón de fábrica para crear las instancias.

Lo mejor que puede hacer es obtener la especificación de la sintaxis del lenguaje ensamblador. Escriba lexer para convertir a tokens (** por favor, no use escaleras if-elseif-else). Luego, basado en la semántica, emita el código.

Hace mucho tiempo, los ensambladores tenían un mínimo de dos pases: el primero para resolver las constantes y formar el código del esqueleto (incluidas las tablas de símbolos). El segundo paso fue generar valores más concretos o absolutos.

¿Has leído el Libro del Dragón últimamente?

+0

¿El Libro del Dragón también habla sobre los ensambladores? Todavía estoy tratando de encontrar una versión descargable de la misma o de obtener una versión traducida pero teniendo éxito. – Hossein