-1-
Im Folgenden sei ein (arg) simplifizierter Emulator vorgestellt - es "emuliert" eine fiktive zentrale Recheneinheit (NS16 bezeichnet), die über acht 16-Bit-Registers, sowieein Status-Register (ein Byte), Stack-Pointer(2-byte), Program-Counter (2-byte), einen RAM (~16kb, von-Neumann-Architektur), und einen VRAM (wird später vielleicht mal Anwendung finden) verfügt.
Ferner wird eine Dekodier-Methode zur Dekodierung und Verarbeitung von mehr als 15 Op-Codes bereitgestellt.
In der Main-Methode des Projekt findet sich ein exemplarisches Programm wieder - nebst mit möglichen Assembler-Äquivalenten (manche Operationen sind fiktiv, z.B. "get").
Arbeitsweise:
Programm wird in den RAM geladen (Adressen 0x000 - 0x0fff für Programm-Code reserviert), "CPU" holt sich in jeweils einem Zyklus den nächsten Op-Code (2-byte), dann den Register-Index (1-byte) und darauffolgend 2-byte große, beliebige Daten.
Eventualiter wird künftig ein Assembler folgen - primär ging es mir in diesem Projekt darum, die Funktionsweise eines Computers zu verstehen; im Grunde ist diese simpel. Erst das Zusammenspiel mehrerer Komponenten (Decoder, ALU, Registers, RAM, Interrupts) und insbesondere die Abstraktion letzterer, machen Computer zu Computern.
Auch wollte ich damit das sogenannte "Bootstrapping" nachvollziehen - quasi wie der erster Compiler konstruiert worden sein könnte.
Möchte man exemplarisch einen rudimentären "Computer" in Minecraft (-Redstone) realisieren, so heißt das konkret (andere Ansätze sind möglich):
- Clock
- RAM-Einheit bauen (z.B. pro Zelle (Breite) 1 byte, wächst in die Tiefe (RAM-Kapazität)
- Register (z.B. 2-byte groß), damit gemeint ist auch der Programm-Counter; für simple Additions/Subtraktions-"Programme" (z.B. mov r0, #10, mov r1, #11, add r0, r1) genügen wenige bytes, deshalb kann der Programm-Counter durchaus 1-byte groß sein (255 Adressen könnte man also im RAM lesen/schreiben)
- ALU
- Decoder, der zu einer beliebige Bit-Anordnung (z.B. 010010) eine bestimmte Funktionalität zuordnet (man definiere z.B. 0xad 0x01 0x05 als: addiere (0xad) Datum (im Sinne des Singulars von Daten) 0x05 (also die Zahl 5) in den Register 1 (0x01) - der Decoder würde also auf das Signal reagieren, ein anderes Signal generieren, der in das Register 1 dann 0x05 addiert (stark vereinfacht).)
- Einige Interrupts (z.B. für die Screen-Ausgabe, oder clear-screen)
- Eine Screen-Ausgabe selbst könnte dann erneut über einen Binary-Decoder realisiert werden.
Natürlich sind das gerade lediglich Denkansätze - das dann zu entwickeln ist eine andere Herausforderung.
Möchte dazu sagen, dass ich keine Garantie für die Akkuratesse meiner Darstellung bieten kann - das habe ich mir zumindest für mich soweit durch das Projekt' erschlossen.
Ein Beispiel-Programm:
C#-Quellcode
- byte[] src = new byte[]
- {
- 0xad, 0x00, 0x00, 0x00, 0x0a, // ; add %rax, 0x000a
- 0xbd, 0x00, 0x00, 0x00, 0x05, // ; sub %rax, 0x0005
- 0xcd, 0x00, 0x00, 0x00, 0x0a, // ; mul %rax, 0x000a
- 0x55, 0x00, 0x0a, 0x00, 0x01, // ; set %sig, 1 ; Signum = 1 => negative Zahl
- 0x11, 0x11, 0x05, 0x00, 0x00, // ; mov %rex, %rax ; Speichere den Wert von %rax in %rex
- 0x11, 0x11, 0x00, 0x00, 0x0a, // ; mov %rax, %sig ; Rufe den Wert des Sign-Registers ab und speichere es in %rax
- 0x55, 0x00, 0x0a, 0x00, 0x00, // ; set %sig, 0
- 0x0d, 0x11, 0x02, 0xff, 0xff, // ; mov %rcx, 0xffff ; Kopiere 0xffff in %rcx
- 0x0a, 0x00, 0x05, 0x00, 0x00, // ; add %rex, %rax
- 0xee, 0xee, 0x04, 0x00, 0x10, // ; get %rdx, %pc
- 0x44, 0x00, 0x04, 0x00, 0x00, // ; push %rdx
- 0x44, 0x01, 0x06, 0x00, 0x00, // ; pop %rfx
- 0xff, 0xff // ; halt
- };
Das Projekt:
NaSmSharp.zip
Und Gott alleine weiß alles am allerbesten und besser.