2010-03-02 9 views
6

Información general: En última instancia, me gustaría escribir un emulador de una máquina real como la Nintendo o Gameboy original. Sin embargo, decidí que tenía que comenzar en un sitio mucho, mucho más simple. Mi asesor/profesor de informática me ofreció las especificaciones para un procesador imaginario muy simple que él creó para emular primero. Hay un registro (el acumulador) y 16 códigos de operación. Cada instrucción consta de 16 bits, los primeros 4 de los cuales contienen el código de operación, el resto de los cuales es el operando. Las instrucciones se dan como cadenas en formato binario, por ejemplo, "0101 0101 0000 1111".Preguntas sobre la implementación de un simulador de CPU simple

Mi pregunta: En C++, ¿cuál es la mejor manera de analizar las instrucciones para el procesamiento? Por favor, tenga en mente mi objetivo final. He aquí algunos puntos que he considerado:

  1. No puedo proceso y ejecutar las instrucciones que los leo porque el código es auto-modificable: una instrucción puede cambiar una instrucción más tarde. La única forma en que puedo ver para evitar esto sería almacenar todos los cambios y para cada instrucción verificar si se debe aplicar un cambio. Esto podría conducir a una gran cantidad de comparaciones con la ejecución de cada instrucción, lo cual no es bueno. Y entonces, creo que tengo que recompilar las instrucciones en otro formato.

  2. Aunque podría analizar el código de operación como una cadena y procesarlo, hay casos en que la instrucción como un todo debe tomarse como un número. El código de operación de incremento, por ejemplo, podría modificar incluso la sección de código de operación de una instrucción.

  3. Si tuviera que convertir las instrucciones en enteros, no estoy seguro de cómo podría analizar solo el código de operación o la sección operando de la int. Incluso si tuviera que recompilar cada instrucción en tres partes, toda la instrucción como un int, el opcode como un int y el operando como un int, eso aún no resolvería el problema, ya que podría tener que incrementar una instrucción completa y luego analizar el código de operación afectado o el operando. Además, ¿tendría que escribir una función para realizar esta conversión, o hay alguna biblioteca para C++ que tenga una función que convierta una cadena en "formato binario" en un entero (como Integer.parseInt (str1, 2) en Java)?

  4. Además, me gustaría poder realizar operaciones como el cambio de bits. No estoy seguro de cómo se puede lograr eso, pero eso podría afectar la forma en que implemente esta recopilación.

¡Gracias por cualquier ayuda o consejo que pueda ofrecer!

Respuesta

5

Analice el código original en una matriz de números enteros. Esta matriz es la memoria de tu computadora.

Utilice las operaciones en modo bit para extraer los diversos campos. Por ejemplo, esto:

unsigned int x = 0xfeed; 
unsigned int opcode = (x >> 12) & 0xf; 

extraerá los cuatro bits más altas (0xf, aquí) de un valor de 16 bits almacenado en un unsigned int. Luego puede usar, p. switch() para inspeccionar el código de operación y tomar la acción apropiada:

enum { ADD = 0 }; 

unsigned int execute(int *memory, unsigned int pc) 
{ 
    const unsigned int opcode = (memory[pc++] >> 12) & 0xf; 

    switch(opcode) 
    { 
    case OP_ADD: 
    /* Do whatever the ADD instruction's definition mandates. */ 
    return pc; 
    default: 
    fprintf(stderr, "** Non-implemented opcode %x found in location %x\n", opcode, pc - 1); 
    } 
    return pc; 
} 

memoria modificación es sólo un caso de la escritura en su matriz de enteros, tal vez también el uso de un poco de matemática a nivel de bits, si es necesario.

+0

Tenía la esperanza de que alguien mencionara un concepto como este. Sin embargo, nunca lo había usado antes, así que tendré que investigar más. ¡Gracias! –

+0

¡Ahh, recuerdos de proyectos universitarios! – sdg

+0

+1. Este es el enfoque básico que debes tomar. El punto clave aquí, Brandon, en relación con su pregunta, es que para abordar esto normalmente, debe llegar al "código máquina", que será la matriz de bytes en una matriz que representa el espacio de direcciones de su computadora virtual. Entonces, si las instrucciones editan la memoria (código), no haces nada especial, simplemente sigues las instrucciones y deberían hacer lo correcto dentro de tu gran matriz de memoria virtual. IOW, necesita tanto el ensamblador (la herramienta que traduce cadenas de texto en bytes de instrucciones) como el emulador, lo que se ejecuta –

1

Creo que el mejor enfoque es leer las instrucciones, convertirlas en enteros sin signo, y almacenarlas en la memoria, y luego ejecutarlas desde la memoria.

  1. Una vez que haya analizado sintácticamente las instrucciones y los guarda en la memoria, la auto-modificación es mucho más fácil de almacenar una lista de cambios para cada instrucción. Puedes simplemente cambiar la memoria en esa ubicación (suponiendo que no necesites saber cuáles eran las instrucciones anteriores).

  2. Dado que está convirtiendo las instrucciones en enteros, este problema es irrelevante.

  3. Para analizar las secciones del código de operación y del operando, deberá usar el cambio de bits y el enmascaramiento. Por ejemplo, para obtener el código de operación, enmascara los 4 bits superiores y baja 12 bits (instruction >> 12). Puede usar una máscara para obtener el operando también.

  4. ¿Quiere decir que su máquina tiene instrucciones que cambian los bits? Eso no debería afectar la forma en que almacena los operandos. Cuando llega a ejecutar una de esas instrucciones, puede usar los operadores de desplazamiento de bits C++ << y >>.

+0

En relación con 1: En ese caso, todavía tendría que volver a compilar el conjunto de instrucciones antes de ejecutarlas en lugar de ejecutarlas como se leen, ¿correcto? Eso no es un problema, solo me preguntaba si podría haber una buena forma de lograr el método de no recompilación. // Respecto a 4: Correcto, eso es lo que quise decir. Pensé que afectaría cómo almacenaría las piezas de instrucción, ya que tendría que considerar cada pieza por separado. Pero este poco de matemáticas parece que podría lograrlo. // Lo que has mencionado tiene sentido conceptualmente. Lo intentaré cuando llegue a casa hoy. ¡Gracias! –

+0

No sé a qué te refieres con "recompilar" aquí. Una CPU (simple) no "recompila" nada: tiene los bits, y hace lo que dicen. ¿Puedes aclarar a qué te refieres con eso? – Ken

+0

Según tengo entendido, los emuladores a menudo funcionan recompilando las instrucciones en otro formato que puede ser procesado más fácilmente por la máquina del emulador. Estoy usando el término aquí, pero creo que es la misma idea. Me preguntaba si existe una forma eficiente de ejecutar las instrucciones sin tener que almacenar las instrucciones en otro lugar de la memoria en otra forma (por ejemplo, sin firmar). –

0

Por si acaso esto ayuda, este es el último emulador de CPU que escribí en C++. En realidad, es el único emulador que he escrito en C++.

El lenguaje de la especificación es un poco peculiar, pero es una simple descripción perfectamente respetable VM, posiblemente bastante similar a VM de su prof:

http://www.boundvariable.org/um-spec.txt

Aquí está mi (algo más de la ingeniería) de código, que debe dar algunas ideas Por ejemplo que muestra cómo implementar los operadores matemáticos, en la sentencia switch gigante en um.cpp:

http://www.eschatonic.org/misc/um.zip

Puede tal vez encontrar otras implementaciones para la comparación con una búsqueda en Internet, ya que un montón de personas participaron en el concurso (Yo no era uno de ellos: lo hice mucho más tarde). Aunque no muchos en C++, supongo.

Si yo fuera usted, solo almacenaría las instrucciones como cadenas para comenzar, si esa es la forma en que la especificación de su máquina virtual define las operaciones en ellas. Luego, conviértalos en enteros según sea necesario, cada vez que quiera ejecutarlos. Va a ser lento, pero ¿y qué? La suya no es una máquina virtual real que va a utilizar para ejecutar programas de tiempo crítico, y un intérprete lento como un perro todavía ilustra los puntos importantes que necesita saber en esta etapa.

Es posible, sin embargo, que la máquina virtual realmente define todo en términos de números enteros, y las cadenas están ahí para describir el programa cuando se carga en la máquina. En ese caso, convierta el programa a enteros al inicio. Si la VM almacena programas e información en conjunto, con las mismas operaciones actuando en ambos, entonces este es el camino a seguir.

La manera de elegir entre ellos es mirar el opcode que se utiliza para modificar el programa. ¿La nueva instrucción se le proporciona como un entero o como una cadena? Cualquiera que sea, lo más simple para comenzar es probablemente almacenar el programa en ese formato. Siempre puede cambiar más tarde una vez que está funcionando.

En el caso de la UM descrita anteriormente, la máquina se define en términos de "platos" con espacio para 32 bits.Claramente, estos se pueden representar en C++ como enteros de 32 bits, así que eso es lo que hace mi implementación.

+0

Antes de hablar con mi profesor, busqué algo simple para emular y encontré ese concurso. Todavía era demasiado complicado para mí comenzar, pero ese podría ser mi siguiente paso. Revisaré tu código en busca de ideas. ¡Gracias! –

0

Creé un emulador para un procesador criptográfico personalizado. Me explotado el polimorfismo del C++ mediante la creación de un árbol de clases base:

struct Instruction // Contains common methods & data to all instructions. 
{ 
    virtual void execute(void) = 0; 
    virtual size_t get_instruction_size(void) const = 0; 
    virtual unsigned int get_opcode(void) const = 0; 
    virtual const std::string& get_instruction_name(void) = 0; 
}; 

class Math_Instruction 
: public Instruction 
{ 
    // Operations common to all math instructions; 
}; 

class Branch_Instruction 
: public Instruction 
{ 
    // Operations common to all branch instructions; 
}; 

class Add_Instruction 
: public Math_Instruction 
{ 
}; 

También tuve un par de fábricas. Al menos dos serían útiles:

  1. Fábrica para crear instrucciones a partir del texto .
  2. de fábrica para crear instrucciones de opcode

Las clases de instrucción deben tener métodos para cargar sus datos desde una fuente de entrada (por ejemplo std::istream) o texto (std::string). El corolario métodos de salida también deben ser compatibles (como el nombre de la instrucción y el código de operación).

Tenía la aplicación crear objetos, desde un archivo de entrada, y colocarlos en un vector de Instruction. El ejecutor método ejecutaría el método 'execute() `de cada instrucción en la matriz. Esta acción goteó hasta el objeto hoja de instrucciones que realizó la ejecución detallada.

Existen otros objetos globales que también pueden necesitar emulación. En mi caso, algunos incluyeron el bus de datos, registros, ALU y ubicaciones de memoria.

Pase más tiempo diseñando y pensando en el proyecto antes de codificarlo. Me pareció todo un desafío, especialmente la implementación de un depurador y GUI con capacidad single-step.

¡Buena suerte!

+0

Por cierto, en mi alma mater, uno de los profesores hizo que sus alumnos escribieran funciones para emular componentes de hardware para la clase * Diseño de microprocesador *. Tuvimos algunas buenas discusiones sobre la emulación del procesador. :-) –