Wir schreiben einen Emulator, der von niemandem benötigt wird

Guten Tag.


Vor langer Zeit gab es den Wunsch, einen Emulator eines Prozessors zu schreiben.
Und was gibt es Schöneres, als ein Fahrrad zu erfinden?


Der Name des Fahrrads ist V16, vom Verkleben des Wortes Virtual und in der Tat von Bittiefe.



Wo soll ich anfangen?


Und Sie müssen natürlich mit einer Beschreibung des Prozessors beginnen.


Am Anfang hatte ich vor, einen DCPU-16-Emulator zu schreiben, aber es gibt mehr als genug solcher Wunder im Internet. Deshalb habe ich mich entschlossen, mich nur darauf zu konzentrieren, das Grundlegendste mit DCPU-16 1.1 zu „lecken“.


Architektur


Speicher und Ports


  • V16 adressiert 128 KB (65536 Wörter) RAM, der auch als Gerätepuffer und Stapel verwendet werden kann.
  • Der Stapel beginnt mit der Adresse FFFF, daher hat RSP den Standardwert 0xFFFF
  • Die V16-E / A-Ports haben 256, alle haben eine Länge von 16 Bit. Das Lesen und Schreiben von ihnen erfolgt durch die Anweisungen IN b, a UND OUT b, a .

Register


V16 verfügt über zwei Sätze von Allzweckregistern: Primär- und Alternativregister.
Ein Prozessor kann nur mit einem Satz arbeiten, sodass Sie mithilfe der XCR Anweisung zwischen den Sätzen wechseln können.


Anleitung


Alle Anweisungen haben eine maximale Länge von drei Wörtern und werden zuerst vollständig definiert
Das erste Wort ist in drei Werte unterteilt: Das niedrige Byte ist der Opcode, das hohe Byte in Form von zwei 4-Bit-Werten ist die Beschreibung der Operanden.


Unterbrechungen


Interrupts sind hier nichts anderes als eine Tabelle mit Adressen, an die der Prozessor den CALL Befehl dupliziert. Wenn der Adresswert Null ist, tut der Interrupt nichts, er setzt einfach das HF-Flag zurück.


WertebereichBeschreibung
0x0 ... 0x3Fall als Wert
0x4 ... 0x7Registrieren Sie sich als Wert bei
0x8 ... 0xBRegister + Konstante als Wert an Adresse
0xCKonstante als Wert bei
0xDKonstante als Wert
0xERIP-Register als schreibgeschützter Wert
0xFRSP-Register als Wert

Ein Beispiel für Pseudocode und Wörter, in die all dies übersetzt werden sollte:


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

Zyklen


V16 kann einen Befehl in 1, 2 oder 3 Takten ausführen. Jeder Speicherzugriff ist ein separater Taktzyklus. Unterricht ist kein Takt!


Lass uns anfangen zu schreiben!


Implementierung grundlegender Prozessorstrukturen


  1. Eine Reihe von Registern. Es gibt nur vier Register, aber die Situation verbessert sich, dass der Prozessor zwei solcher Sätze enthält. Das Umschalten erfolgt mit der XCR Anweisung.


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

  2. Flaggen Im Gegensatz zu DCPU-16 verfügt V16 über bedingte Sprünge, Unterprogrammaufrufe und kehrt von derselben zurück. Im Moment hat der Prozessor 8 Flags, von denen 5 Bedingungsflags sind.


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

  3. Eigentlich der Prozessor selbst. Außerdem wird die Tabelle der Interrupt-Adressen beschrieben, die als Deskriptoren bezeichnet werden können und einen weiteren Verweis auf x86 finden.


     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. Operand. Wenn wir die Werte erhalten, müssen wir sie zuerst lesen, dann ändern und dann an die Stelle zurückschreiben, an der wir sie erhalten haben.


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


Funktionen zum Arbeiten mit Strukturen


Wenn alle Strukturen beschrieben sind, besteht Bedarf an Funktionen, die diesen Strukturen die magische Kraft von gelöschtem Code verleihen.


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

Ich habe auch keine große Aufzählung mit Operationscodes erwähnt, aber dies ist nicht notwendig und nur notwendig, um zu verstehen, was in all diesem Durcheinander passiert.


Tick ​​() Funktion


Es gibt auch Aufrufe von statischen Funktionen, die nur zum Aufrufen von 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; } 

Was ist als nächstes zu tun?


Um die Antwort auf diese Frage zu finden, habe ich den Emulator fünfmal von C nach C ++ und umgekehrt umgeschrieben.


Die Hauptziele können jedoch jetzt identifiziert werden:


  • Schnelle normale Interrupts (Anstatt nur eine Funktion aufzurufen und das Empfangen anderer Interrupts zu verbieten, führen Sie einen Funktionsaufruf durch und fügen Sie der Warteschlange neue Interrupts hinzu).
  • Schrauben Sie Geräte sowie Möglichkeiten zur Kommunikation mit ihnen, der Vorteil von Opcodes kann 256 sein.
  • Zu unterrichten Schreiben Sie sich keine Häresie über Habr Der Prozessor arbeitet mit einer bestimmten Taktrate von 200 MHz.

Fazit


Ich hoffe, dass dieser "Artikel" für jemanden nützlich sein wird, jemand wird ihn auffordern, etwas Ähnliches zu schreiben.


Meine Kuchen können auf Github angesehen werden .


Außerdem habe ich wegen Horror einen Assembler für die alte Version dieses Emulators (Nein, versuchen Sie es nicht einmal, der Emulator wird sich zumindest über das falsche ROM-Format beschweren).

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


All Articles