Entwicklungsmodell am Beispiel einer stapelbasierten CPU

Haben Sie jemals die Frage gehabt, wie der Prozessor funktioniert? Ja, ja, genau das, was sich in Ihrem PC / Laptop / Smartphone befindet. In diesem Artikel möchte ich ein Beispiel für einen selbst erfundenen Prozessor mit einem Design in Verilog geben. Verilog ist nicht genau die Programmiersprache, nach der es aussieht. Dies ist die Hardwarebeschreibungssprache. Der geschriebene Code wird von nichts ausgeführt (es sei denn, Sie führen ihn natürlich im Simulator aus), sondern wird zum Entwurf der physischen Schaltung oder in der vom FPGA (Field Programmable Gate Array) wahrgenommenen Form.


Haftungsausschluss: Dieser Artikel ist das Ergebnis der Arbeit an einem Projekt an der Universität, daher war die Arbeitszeit begrenzt und viele Teile des Projekts befinden sich noch in der Anfangsphase der Entwicklung.


Bitte beachten Sie, dass der in diesem Artikel erstellte Prozessor wenig mit modernen, weit verbreiteten Prozessoren gemein hat, aber ich habe versucht, mit seiner Erstellung ein etwas anderes Ziel zu erreichen.


Um den Programmierprozess wirklich zu verstehen, müssen Sie sich vorstellen, wie jedes der verwendeten Tools funktioniert: ein Compiler / Interpreter der Sprache, eine virtuelle Maschine, falls vorhanden, Zwischencode und natürlich der Prozessor selbst. Sehr oft sind Leute, die Programmieren studieren, lange Zeit in der ersten Phase - sie denken nur darüber nach, wie die Sprache und ihr Compiler funktionieren. Dies führt häufig zu Fehlern, deren Lösungen dem unerfahrenen Programmierer unbekannt sind, da er keine Ahnung hat, woher die Wurzeln dieser Probleme stammen. Ich selbst habe mehrere Live-Beispiele gesehen, bei denen die Situation in etwa der obigen Beschreibung entsprach, und habe mich daher entschlossen, diese Situation zu beheben und eine Reihe von Dingen zu erstellen, die Anfängern helfen, alle Phasen zu verstehen.


Dieses Kit besteht aus:


  • Eigentlich erfundene Sprache
  • Markieren Sie das Plugin für VS Code
  • Compiler dazu
  • Befehlssatz
  • Ein einfacher Prozessor, der diese Anweisungen ausführen kann (geschrieben in Verilog).

Ich erinnere Sie noch einmal daran, dass dieser Artikel nichts beschreibt, was einem modernen realen Prozessor ähnelt. Er beschreibt ein Modell, das leicht zu verstehen ist, ohne auf Details einzugehen.


Dinge, die Sie brauchen, wenn Sie es selbst tun möchten:


Zum Ausführen einer CPU-Simulation benötigen Sie ModelSim, das Sie von der Intel-Website herunterladen können.


Zum Ausführen des OurLang-Compilers benötigen Sie die Java-Version> = 8.


Links zu Projekten:
https://github.com/IamMaxim/OurCPU
https://github.com/IamMaxim/OurLang


Erweiterung:
https://github.com/IamMaxim/ourlang-vscode


Um den Verilog-Teil zu erstellen, verwende ich normalerweise ein Bash-Skript:


#/bin/bash vlib work vlog *.v vsim -c testbench_1 -do "run; exit" 

Dies kann jedoch über die GUI wiederholt werden.


Es ist praktisch, Intellij IDEA zu verwenden, um mit dem Compiler zu arbeiten. Die Hauptsache ist, zu verfolgen, welche Module das Modul hat, das Sie in den Abhängigkeiten benötigen. Ich habe die vorgefertigte .jar-Datei nicht für den Open Access veröffentlicht, da ich erwarte, dass der Leser den Quellcode des Compilers liest.


Gestartete Module sind Compiler und Interpreter. Mit dem Compiler ist alles klar. Interpreter ist nur ein Simulator von OurCPU in Java, aber wir werden es in diesem Artikel nicht berücksichtigen.


Befehlssatz


Ich denke, es ist besser, mit dem Befehlssatz zu beginnen.


Es gibt verschiedene Befehlssatzarchitekturen:


  • Stapelbasiert wird im Artikel beschrieben. Eine Besonderheit besteht darin, dass alle Operanden auf den Stapel geschoben und aus dem Stapel entfernt werden, was die Möglichkeit einer Parallelisierung der Ausführung sofort ausschließt. Dies ist jedoch einer der einfachsten Ansätze für die Arbeit mit Daten.
  • Akkumulatorbasiert - unter dem Strich gibt es nur ein Register, in dem ein Wert gespeichert ist, der durch Anweisungen geändert wird.
  • Registerbasiert wird in modernen Prozessoren verwendet, da Sie durch die Verwendung verschiedener Optimierungen, einschließlich Parallelisierung der Ausführung, Pipelining usw., maximale Leistung erzielen können.

Unser Prozessor-Befehlssatz enthält 30 Befehle


Als nächstes schlage ich vor, einen Blick auf die Prozessorimplementierung zu werfen:


Der Code besteht aus mehreren Modulen:


  • CPU
  • RAM
  • Module für jede Anweisung

RAM ist ein Modul, das direkt den Speicher selbst sowie eine Möglichkeit zum Zugriff auf Daten enthält.


CPU - ein Modul, das den Fortschritt des Programms direkt steuert: Anweisungen liest, die Steuerung auf die gewünschte Anweisung überträgt, die erforderlichen Register speichert (Zeiger auf die aktuelle Anweisung usw.).


Fast alle Anweisungen funktionieren nur mit dem Stapel. Befolgen Sie sie einfach. Einige (z. B. putw, putb, jmp und jif) haben ein zusätzliches Argument in der Anweisung selbst. Sie müssen die gesamte Anweisung übergeben, damit sie die erforderlichen Daten lesen können.


Hier ist ein allgemeiner Überblick über die Funktionsweise des Prozessors:



Allgemeine Prinzipien des Gerätedesigns auf Anweisungsebene


Ich denke, es ist Zeit, das Gerät direkt von den Programmen selbst kennenzulernen. Wie Sie dem obigen Diagramm entnehmen können, wechselt die Adresse nach jeder Anweisung zur nächsten. Dies gibt dem Programm einen linearen Verlauf. Wenn diese Linearität (Bedingung, Schleife usw.) unterbrochen werden muss, werden Verzweigungsbefehle verwendet (in unserem Befehlssatz sind dies jmp und jif).


Beim Aufrufen von Funktionen müssen wir den aktuellen Status von allem speichern. Dazu gibt es Aktivierungsdatensätze - Datensätze, in denen diese Informationen gespeichert sind. Sie sind in keiner Weise an den Prozessor oder die Anweisungen gebunden, sondern nur ein Konzept, das der Compiler beim Generieren von Code verwendet. Der Aktivierungsdatensatz in OurLang hat die folgende Struktur:



Wie aus diesem Diagramm ersichtlich ist, werden auch lokale Variablen im Aktivierungsdatensatz gespeichert, sodass Sie die Adresse der Variablen im Speicher zur Kompilierungszeit und nicht zur Laufzeit berechnen und so die Programmausführung beschleunigen können.


Für Funktionsaufrufe bietet unser Befehlssatz Methoden zum Arbeiten mit zwei im CPU-Modul enthaltenen Registern (Operationszeiger und Aktivierungsadresszeiger) - putopa / popopa, putara / popara.


Compiler


Schauen wir uns nun den Teil an, der dem endgültigen Programmierer am nächsten liegt - den Compiler. Im Allgemeinen besteht der Compiler als Programm aus 3 Teilen:


  • Lexer
  • Parser
  • Compiler

Der Lexer ist dafür verantwortlich, den Quellcode des Programms in lexikalische Einheiten zu übersetzen, die der Parser versteht.


Der Parser erstellt aus diesen lexikalischen Einheiten einen abstrakten Syntaxbaum.


Der Compiler durchläuft diesen Baum und generiert eine Art Code, der aus Anweisungen auf niedriger Ebene besteht. Dies kann entweder ein Bytecode oder ein Binärcode sein, der vom Prozessor ausgeführt werden kann.


Im OurLang-Compiler werden diese Teile jeweils durch Klassen dargestellt


  • Lexer.java
  • Parser.java
  • Compiler.java

Sprache


OurLang steckt noch in den Kinderschuhen, das heißt, es funktioniert, aber bisher sind nicht viele Dinge darin enthalten, und selbst der Kern der Sprache wurde noch nicht fertiggestellt. Um die Essenz des Compilers zu verstehen, reicht der aktuelle Status aus.


Als Beispiel für ein Programm zum Verständnis der Syntax wird dieses Codefragment vorgeschlagen (es wird auch zum Testen der Funktionalität verwendet):


 // single-line comments /* * Multi-line comments */ function print(int arg) { instr(putara, 0); instr(putw, 4); instr(add, 0); instr(lw, 0); instr(printword, 0); } function func1(int arg1, int arg2): int { print(arg1); print(arg2); if (arg1 == 0) { return arg2; } else { return func1(arg1 - 1, arg2); }; } function main() { var i: int; i = func1(1, 10); if (i == 0) { i = 1; } else { i = 2; }; print(i); } 

Ich werde mich nicht auf die Sprache konzentrieren, ich werde sie für Ihr Studium belassen. Natürlich über den Compiler-Code;).


Beim Schreiben habe ich versucht, selbsterklärenden Code zu erstellen, der ohne Kommentar klar ist, sodass es kein Problem geben sollte, den Compiler-Code zu verstehen.


Das Interessanteste ist natürlich, Code zu schreiben und dann zu beobachten, was daraus wird. Glücklicherweise generiert der OurLang-Compiler Assembly-ähnlichen Code mit Kommentaren.
Das hilft, nicht verwirrt darüber zu werden, was im Inneren passiert.


Ich empfehle auch, die Erweiterung für Visual Studio Code zu installieren, da dies die Arbeit mit der Sprache erleichtert.


Viel Glück beim Lernen des Projekts!

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


All Articles