Nous écrivons un émulateur dont personne n'a besoin

Bonjour.


Il y a très longtemps, il y avait un désir d'écrire un émulateur d'un processeur.
Et quoi de mieux que d'inventer un vélo?


Le nom du vélo est V16, du collage du mot virtuel et, en fait, de la profondeur de bits.



Par où commencer?


Et vous devez bien sûr commencer par une description du processeur.


Au tout début, j'avais prévu d'écrire un émulateur DCPU-16, mais il y a plus qu'assez de tels miracles sur Internet, j'ai donc décidé de me concentrer uniquement sur "lécher" les plus basiques avec DCPU-16 1.1.


L'architecture


Mémoire et ports


  • V16 adresse 128 Ko (65 536 mots) de RAM, qui peuvent également être utilisés comme tampons de périphérique et la pile.
  • La pile commence par l'adresse FFFF, par conséquent, RSP a une valeur standard de 0xFFFF
  • Les ports d'E / S V16 en ont 256, ils ont tous une longueur de 16 bits. Leur lecture et leur écriture s'effectuent à travers les instructions IN b, a ET OUT b, a .

Registres


V16 possède deux ensembles de registres à usage général: primaire et alternatif.
Un processeur ne peut fonctionner qu'avec un seul ensemble, vous pouvez donc basculer entre les ensembles à l'aide de l'instruction XCR .


Instructions


Toutes les instructions ont une longueur maximale de trois mots et sont entièrement définies en premier
Le premier mot est divisé en trois valeurs: l'octet bas est l'opcode, l'octet haut sous la forme de deux valeurs de 4 bits est la description des opérandes.


Interruptions


Les interruptions ne sont ici rien de plus qu'une table avec des adresses auxquelles le processeur duplique l'instruction CALL . Si la valeur d'adresse est nulle, alors l'interruption ne fait rien, elle réinitialise simplement l'indicateur HF.


Plage de valeursLa description
0x0 ... 0x3Case comme valeur
0x4 ... 0x7Inscrivez-vous en tant que valeur à
0x8 ... 0xBRegistre + constante comme valeur à l'adresse
0xCConstante comme valeur à
0xDConstante comme valeur
0xERegistre RIP en tant que valeur en lecture seule
0xFRegistre RSP comme valeur

Un exemple de pseudocode et de mots dans lesquels tout cela devrait être traduit:


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

Cycles


V16 peut exécuter une instruction en 1, 2 ou 3 mesures. Chaque accès mémoire est un cycle d'horloge distinct. L'instruction n'est pas du tact!


Commençons à écrire!


Implémentation de structures de processeur de base


  1. Un ensemble de registres. Il n'y a que quatre registres, mais la situation s'améliore car il y a deux de ces ensembles dans le processeur. La commutation se produit à l'aide de l'instruction XCR .


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

  2. Drapeaux Contrairement à DCPU-16, V16 a des sauts conditionnels, des appels de sous-programme et des retours de la même. À l'heure actuelle, le processeur dispose de 8 indicateurs, dont 5 sont des indicateurs de condition.


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

  3. En fait, le processeur lui-même. Il décrit également le tableau des adresses d'interruption, qui peuvent être appelées descripteurs et trouver une autre référence à 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. Opérande. Lors de l'obtention des valeurs, nous devons d'abord lire, puis modifier, puis réécrire la valeur à l'endroit où nous l'avons obtenue.


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


Fonctions de travail avec les structures


Lorsque toutes les structures sont décrites, le besoin se fait sentir de fonctions qui doteront ces structures de la puissance magique du code désactivé.


 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 *); // ,    

De plus, je n'ai pas mentionné une grande énumération avec les codes d'opération, mais ce n'est pas nécessaire et n'est nécessaire que pour comprendre ce qui se passe dans tout ce gâchis.


Fonction Tick ()


En outre, il existe des appels à des fonctions statiques destinées uniquement à appeler à partir de 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; } 

Que faire ensuite?


Afin de trouver la réponse à cette question, j'ai réécrit l'émulateur cinq fois de C en C ++, et vice versa.


Cependant, les principaux objectifs peuvent être identifiés maintenant:


  • Fixez les interruptions normales (au lieu d'appeler simplement une fonction et d'interdire la réception d'autres interruptions, effectuez un appel de fonction et ajoutez de nouvelles interruptions à la file d'attente).
  • Les dispositifs à vis, ainsi que les moyens de communiquer avec eux, l'avantage des opcodes peut être 256.
  • Enseigner Ne t'écris aucune hérésie sur Habr Le processeur fonctionne à une vitesse d'horloge spécifique de 200 MHz.

Conclusion


J'espère que cet "article" sera utile à quelqu'un, quelqu'un le poussera à écrire quelque chose de similaire.


Mes tartes peuvent être consultées sur github .


Aussi, à propos de l'horreur, j'ai un assembleur pour l'ancienne version de cet émulateur (non, n'essayez même pas, l'émulateur se plaindra au moins du mauvais format de ROM)

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


All Articles