Escribimos un emulador que nadie necesita

Buen dia


Hace mucho tiempo había un deseo de escribir un emulador de algún procesador.
¿Y qué podría ser mejor que inventar una bicicleta?


El nombre de la bicicleta es V16, por pegar la palabra Virtual y, de hecho, profundidad de bits.



Por donde empezar


Y debe comenzar, por supuesto, con una descripción del procesador.


Al principio, planeaba escribir un emulador DCPU-16, pero hay más que suficientes milagros en Internet, así que decidí centrarme solo en "lamer" lo más básico con DCPU-16 1.1.


Arquitectura


Memoria y puertos


  • V16 direcciona 128Kb (65536 palabras) de RAM, que también se puede usar como búfer de dispositivo y la pila.
  • La pila comienza con la dirección FFFF, por lo tanto, RSP tiene un valor estándar de 0xFFFF
  • Los puertos de E / S V16 tienen 256, todos tienen una longitud de 16 bits. Leer y escribir de ellos se lleva a cabo mediante las instrucciones IN b, a AND OUT b, a .

Registros


V16 tiene dos conjuntos de registros de propósito general: primario y alternativo.
Un procesador puede funcionar con un solo conjunto, por lo que puede cambiar entre conjuntos utilizando la instrucción XCR .


Instrucciones


Todas las instrucciones tienen una longitud máxima de tres palabras y están completamente definidas primero
La primera palabra se divide en tres valores: el byte bajo es el código de operación, el byte alto en forma de dos valores de 4 bits es la descripción de los operandos.


Interrupciones


Las interrupciones aquí no son más que una tabla con direcciones en las que el procesador duplica la instrucción CALL . Si el valor de la dirección es cero, la interrupción no hace nada, simplemente restablece el indicador de HF.


Rango de valoresDescripción
0x0 ... 0x3Caso como valor
0x4 ... 0x7Registrarse como un valor en
0x8 ... 0xBRegistro + constante como valor en la dirección
0xCConstante como un valor en
0xDConstante como valor
0xERegistro RIP como valor de solo lectura
0xFRegistro RSP como valor

Un ejemplo de pseudocódigo y palabras en el que todo esto debería traducirse:


 MOV RAX, 0xABCD ; 350D ABCD MOV [RAX], 0x1234 ; 354D 1234 

Ciclos


V16 puede ejecutar una instrucción en 1, 2 o 3 medidas. Cada acceso a la memoria es un ciclo de reloj separado. ¡La instrucción no es tacto!


¡Comencemos a escribir!


Implementación de estructuras básicas de procesador.


  1. Un conjunto de registros. Solo hay cuatro registros, pero la situación mejora porque hay dos conjuntos de este tipo en el procesador. El cambio ocurre usando la instrucción XCR .


     typedef struct Regs { uint16_t rax, rbx; //Primary Accumulator, Base Register uint16_t rcx, rdx; //Counter Register, Data Register } regs_t; 

  2. Banderas A diferencia de DCPU-16, V16 tiene saltos condicionales, llamadas de subrutina y retornos de la misma. Por el momento, el procesador tiene 8 indicadores, 5 de los cuales son indicadores de condición.


     //  ,    stdbool.h typedef struct Flags { bool IF, IR, HF; bool CF, ZF; bool EF, GF, LF; } flags_t; 

  3. En realidad, el procesador en sí. También describe la tabla de direcciones de interrupción, que se puede llamar descriptores y encontrar otra referencia a x86.


     typedef struct CPU { //CPU Values uint16_t ram[V16_RAMSIZE]; //Random Access Memory uint16_t iop[V16_IOPSIZE]; //Input-Output Ports uint16_t idt[V16_IDTSIZE]; //Interrupt vectors table (Interrupt Description Table) flags_t flags; //Flags regs_t reg_m, reg_a; //Main and Alt register files regs_t * reg_current; //Current register file uint16_t rip, rsp, rex; //Internal Registers: Instruction Pointer, Stack Pointer, EXtended Accumulator //Emulator values bool reg_swapped; //Is current register file alt bool running; //Is cpu running uint32_t cycles; //RAM access counter } cpu_t; 

  4. Operando. Al obtener los valores, primero debemos leer, luego cambiar y luego escribir el valor donde lo obtuvimos.


     typedef struct Opd { uint8_t code : 4; uint16_t value; uint16_t nextw; } opd_t; 


Funciones para trabajar con estructuras.


Cuando se describen todas las estructuras, surge la necesidad de funciones que doten a estas estructuras del poder mágico del código apagado.


 cpu_t * cpu_create(void); //   void cpu_delete(cpu_t *); //   void cpu_load(cpu_t *, const char *); // ROM   void cpu_rswap(cpu_t *); //   uint16_t cpu_nextw(cpu_t *); //RAM[RIP++]. Nuff said void cpu_getop(cpu_t *, opd_t *, uint8_t); //  void cpu_setop(cpu_t *, opd_t *, uint16_t); //  void cpu_tick(cpu_t *); //   void cpu_loop(cpu_t *); // ,    

Además, no mencioné una gran enumeración con códigos de operación, pero esto no es necesario y es necesario solo para comprender lo que está sucediendo en todo este desastre.


Función Tick ()


Además, hay llamadas a funciones estáticas destinadas únicamente a llamar desde tick() .


 void cpu_tick(cpu_t *cpu) { //    HLT,      if(cpu->flags.HF) { //      ,      if(!cpu->flags.IF) { cpu->running = false; } return; } //       uint16_t nw = cpu_nextw(cpu); uint8_t op = ((nw >> 8) & 0xFF); uint8_t ob = ((nw >> 4) & 0x0F); uint8_t oa = ((nw >> 0) & 0x0F); //     //   opd_t opdB = { 0 }; opd_t opdA = { 0 }; //    cpu_getop(cpu, &opdB, ob); cpu_getop(cpu, &opdA, oa); //        -  uint16_t B = opdB.value; uint16_t A = opdA.value; uint32_t R = 0xFFFFFFFF; //    bool clearf = true; //       ? //   ! switch(op) { //     . ,   ,    R } //   if(clearf) { cpu->flags.EF = false; cpu->flags.GF = false; cpu->flags.LF = false; } //  ,  32-   16-  //  0xFFFF0000,   0xFFFF << 16 //        32-  if(R != 0xFFFFFFFF) { cpu_setop(cpu, &opdB, (R & 0xFFFF)); cpu->rex = ((R >> 16) & 0xFFFF); cpu->flags.CF = (cpu->rex != 0); cpu->flags.ZF = (R == 0); } return; } 

¿Qué hacer a continuación?


En un intento por encontrar la respuesta a esta pregunta, reescribí el emulador cinco veces de C a C ++, y viceversa.


Sin embargo, los objetivos principales se pueden identificar ahora:


  • Fije las interrupciones normales (en lugar de simplemente llamar a una función y prohibir recibir otras interrupciones, realice una llamada a la función y agregue nuevas interrupciones a la cola).
  • Los dispositivos de tornillo, así como las formas de comunicarse con ellos, el beneficio de los códigos de operación puede ser 256.
  • Para enseñar No te escribas ninguna herejía sobre Habr El procesador funciona a una velocidad de reloj específica de 200 MHz.

Conclusión


Espero que este "artículo" sea útil para alguien, alguien les pedirá que escriban algo similar.


Mis pasteles se pueden ver en github .


Además, sobre el horror, tengo un ensamblador para la versión anterior de este emulador (No, ni siquiera lo intentes, el emulador al menos se quejará del formato ROM incorrecto)

Source: https://habr.com/ru/post/457144/


All Articles