ourMIPS-Online

Übersicht über ourMIPS

OurMIPS ist die beispielhaft verwendete Prozessorarchitektur. Sie besteht unter anderem aus folgenden Komponenten:

  • Programmspeicher: Er enthält die Befehle (auch: Instruktionen), die der Prozessor ausführt. Eine solche Instruktion enthält, codiert mit 32 Bit, Informationen über die auszuführende Operationen, sowie die verwendeten Register, Datenspeicherzellen und/oder weitere Werte. Eine Instruktion wird durch eine Programmadresse adressiert, welche durch 16 Bit codiert ist.
  • Befehlszähler: Der Befehlszähler (auch Instruktionszeiger oder Befehlszeiger) enthält und stellt die Adresse des nächsten Befehls zur Verfügung. Ein Befehl wird bei ourMIPS über die Zeitspanne genau eines Taktes ausgeführt, sodass der Zeiger nach jeweils einem Takt aktualisiert wird.
  • Registerbank: 32 Register enthalten jeweils 32 Bit an beliebigen Daten, die von Operationen geschrieben oder gelesen werden können. Bezeichnet werden die Register mit r0-31, wobei ebenfalls die bei MIPS üblichen Bezeichnungen gebräuchlich sind (siehe Vorlesung). Es ist wichtig zu wissen, dass r0 (das Nullregister) stets zu Beginn der Ausführung auf 0 gesetzt ist und darüber hinaus auch gar nicht verändert werden kann. r29 (der Stackpointer) ist zu Beginn auf 0x7FFFFFFF (dezimal 2147483647) gesetzt. Der Inhalt aller restlichen Register, soweit nicht vorbelegt, ist zu Beginn der Ausführung des Programms zufällig.
  • Datenspeicher: Dieser enthält 232 Zellen mit jeweils 32 Bit Breite für beliebige Daten. Eine Zelle kann also über eine durch 32 Bit codierte Adresse angesprochen werden. Im Gegensatz zur Registerbank befindet sich der Datenspeicher außerhalb des Prozessors. Aber auch hier sind die Zellen am Anfang zufällig belegt.
  • Overflow-Register: Bestimmte Operationen setzen dieses 1-Bit Register. Siehe dazu die Befehlsreferenz. Das Overflow-Register befindet sich nicht in der Registerbank. Es ist zu Beginn stets auf 0 gesetzt.

Weiterin gibt es die ALU (Arithmetic Logic Unit) für das Ausführen von Berechnungen, die Steuereinheit, welche andere Komponenten anhand der aktuellen Instruktion steuert, und weitere kleine Komponenten sowie Multiplexer und Datenleitungen, welche alle Komponenten verbinden.

Die Assemblersprache

Um den ourMIPS-Prozessor zu programmieren, wird hier eine Assemblersprache genutzt, die in der Vorlesung vorgestellt und nachfolgend nochmals erklärt wird. Der Compiler, welcher den eingegebenen Quellcode liest und das Maschinenprogramm generiert, heißt Assembler oder Assemblierer. Die Sprache selbst wird allerdings häufig ebenfalls als Assembler bezeichnet, sodass hier nur „Assemblierer“ als Bezeichung benutzt wird. Ein Assemblerprogramm wird fast eins-zu-eins in ein Maschinenprogramm übersetzt, also eine Abfolge von Bits, welche die Instruktion für den Prozessor codieren. Ein Befehl im Quellcode entspricht somit in der Regel einer Instruktion. In den Quellcode können allerdings auch spezielle Direktiven für den Assemblierer geschrieben werden.

Zur Einführung ein Ausschnitt aus einem Beispielprogramm. r2 enthält einen zuvor eingegebenen Wert.

Auffällig am Rand stehen Kommentare in grün. Der Assemblierer ignoriert den Text, der hinter dem ; steht. Sie eignen sich besonders gut, um zu dokumentieren, was ein Teil des Programms tut und was dessen Nutzen ist. Weil Assemblersprachen im Gegensatz zu höheren Programmiersprachen nicht leicht nachzuvollziehen sind, muss immer darauf geachtet werden, den Quellcode gut und aussagekräftig zu kommentieren.

Eine Instruktionen besteht aus einer Operation und deren Argumente. Nach dem Übersetzen befinden sie sich alle von oben beginnend bei Adresse 0 im Programmspeicher. Der Prozessor fängt bei Adresse 0 an die Instruktionen auszuführen:

  • Zeile 1: Die addi-Operation („ADDition with Immediate value“) liest das erste angegebene Register, addiert auf dessen Wert den direkt angegebenen Wert 5 und schreibt das Ergebnis in das zweite angegebene Register. Weil r0 stets die Null enthält, hat r1 danach eine 5 als Inhalt.
  • Zeile 2: beq („Branch if EQual“) vergleicht die angegebenen Register und setzt, falls Gleichheit, den Befehlszähler auf eine neue Programmadresse. Ansonsten wird er, wie bei anderen Operationen auch, einfach um 1 erhöht. Weil es mühsam ist, Programmadressen von Hand zu zählen, wird sich eines Labels bedient. Es steht einfach für die Adresse der Instruktion, die sich nach dem Label befindet. In diesem Fall die Adresse 3, die dann in der beq-Instruktion als relative Angabe codiert wird. Der Prozessor kennt keine Labels, sie existieren nicht im Maschinenprogramm.
  • Zeile 3: add ist wie addi, nur zieht es als zweiten Summanden ein Registerinhalt und keinen direkten Wert heran. Es wird r2 auf den Wert r2+r2 gesetzt.
  • Zeile 4: Leere Zeilen generieren keine Instruktion und machen auch sonst nichts. Sie können allerdings die Übersichtlichkeit erhöhen.
  • Zeile 5: Ein Label mit Bezeichner gleichheit, es steht für Adresse 3 innerhalb des Programmspeichers, zeigt also auf die Instruktion in Zeile 5.
  • Zeile 6: Dieses addi setzt r3 auf den Wert r2+1. Es ist nicht relevant, ob eine Instruktion eingerückt ist oder nicht, allerdings kann man so den Quellcode besser strukturiert erfassen. Einrücken verbessert ebenfalls die Lesbarkeit.

Aus dem Eingabewert x in r2 berechnet das obige Programm zusammengefasst den Wert 2x+1 in r3, wenn x nicht 5 ist, ansonsten den Wert 6.

Wie sehen Assemblerprogramme grundsätzlich aus?

  • Kommentare dürfen mit ; oder # überall eingeleitet werden und gelten den Rest der Zeile.
  • Instruktionen im Quellcode haben immer das Format <Operation> <Argument1>, ..., wobei das Komma weggelassen werden darf (Komma und Leerzeichen dürfen aber nicht gemischt werden).
  • Register werden mit r0-r31 oder r[0]-r[31] oder mit den alternativen MIPS-Namen genannt. Die alternativen Bezeichnungen können unter Umständen die Verständlichkeit erhöhen.
  • Direkt angegebene Werte können gewohnt Dezimal (z.B. 420), Binär (z.B. 0b1110 und 10010b), Hexadezimal (z.B. 0x29A und z.B. ABCDh) geschrieben werden. Negative Zahlen mit Vorzeichen - sind ebenfalls erlaubt und werden im Zweierkomplement codiert.
  • Label stehen für die Adresse nach der vorherigen Instruktion und haben das Format <Name>:. Nach dem Doppelpunkt darf direkt die nächste Instruktion stehen. Labels dürfen nur mit Namen mit A-Z, a-z und _ sowie Ziffern 0-9, die nicht direkt am Anfang stehen, bezeichnet werden. größer-als-4: und 2big: sind also keine gültigen Label, wohingegen groesserals_4: eines ist.

Neben den Assemblerinstruktionen können (und müssen) sogenannte Systemaufrufe verwendet werden, die durch das (hier versteckte und nicht näher erläuterte) Betriebssystem „bereitgestellt“ werden. Es stehen folgende Systemaufrufe zur Verfügung:

  • systerm: Beendet das Programm. Es muss darauf geachtet werden, dass Programme mit systerm stets korrekt beendet werden, da der Prozessor sonst versucht, den Rest des Programmspeicher auszuführen.
  • sysout "<Text>": Gibt einen Text aus, der zwischen " steht. Zeichenketten müssen immer in " umschlossen werden.
  • sysout <Register>: Interpretiert den Inhalt des Registers im Zweierkomplement und gibt den Wert aus.
  • sysin <Register>: Wartet auf eine Nutzereingabe und schreibt den Wert codiert im Zweierkomplement in das Register.

Die Webanwendung

Zur Linken sind verschiedene Buttons (nicht gleichzeitig) zu sehen, welche näher erläutert werden:

Hilfeseite: Verlinkt zu dieser Seite.
Darstellung umschalten: Wenn Inhalte von Registern, Speicherzellen und Weiterem angezeigt werden, lässt sich deren Darstellung als Zahl nach Bedarf auswähen: Vorzeichenlos binär/dezimal/hexadezimal oder dezimal mit Vorzeichen. Adressen hingegen werden üblicherweise immer hexadezimal ausgedrückt (sowie auch dezimal).
Testumgebungen: Es ist möglich, die Inhalte der Register und des Datenspeichers vorab zu initialisieren sowie nach erfolgreichem Programmlauf mit geforderten Ergebnissen zu vergleichen.
Programm übersetzen: Übersetzt das Programm und generiert das Maschinenprogramm. Fehler und zusätzliche Informationen werden in der Konsole unten links angezeigt.
Statische Analyse: Gibt Informationen und Hinweise zu dem Programm unter dem aktuell ausgewählten Test aus.
Programm debuggen: Das Programm muss übersetzt sein. Initialisiert den Prozessor und wartet vor der ersten Instruktion. Es wird danach an Breakpoints gehalten.
Programm starten: Das Programm muss übersetzt sein. Startet die Ausführung. Es wird nicht an Breakpoints gehalten.
Ausführung anhalten/pausieren.
Ausführung fortführen
Einen Takt ausführen.
Ausführung stoppen.

Tastenkombinationen und Drag&Drop

  • Strg+C/V im Editor: Kopieren/Einfügen
  • Strg+Z/Y im Editor: Rückgängig/Wiederholen
  • Strg+S: Inhalt des Editors speichern
  • Datei auf den Editor ziehen: Inhalt der Datei in den Editor einfügen
  • Datei auf die Zahnräder ziehen: Tests aus Datei laden

Ausführen und Debuggen

Das Programm lässt sich ausführen, wenn der Assemblierer keine Syntaxfehler im Quellcode gefunden hat. Während der Ausführung kann die Ausführung angehalten oder gestoppt werden. Wenn die Ausführung angehalten wurde, kann der aktuelle Zustand des Prozessors eingesehen werden. Wenn die Ausführung mit „Programm debuggen“ gestartet wurde, wird ebenfalls automatisch an Breakpoints gehalten. Breakpoints können durch Klicken auf den Rand des Editors gesetzt werden. Der Prozessor wird dann direkt vor der entsprechenden Instruktion angehalten. Angehalten kann das Programm fortgesetzt oder ein einzelner Takt durchlaufen werden.

Inhalt des Programmspeichers

Zu jeder Instruktion des Programmspeichers wird deren Adresse, die Zeile, aus welcher die Instruktion im Quellcode stammt, sowie die Binärdarstellung und die Interpretation dargestellt. Die Bits sind dabei in zusammengehörende Teile gruppiert und nicht relevante Bits ausgeblendet. Dabei geben die vorderen Bits (Bits 26-31) den Op-Code an, der die Operation der Instruktion codiert.

Testumgebung

Es können eine Menge von Tests aus einer bereitgestellten Datei geladen werden. Ein Test enthält eine Belegung einiger Register sowie des Datenspeichers vor Beginn der Ausführung und eine Belegung, die nach Ende des Programm zu erreichen ist. Nachdem ein Programm ausgeführt wurde, kann auf der Oberfläche der Ist-Zustand mit dem Soll-Zustand verglichen werden. Die statische Analyse bezieht sich auf den aktuell ausgewählten Test, wenn vorhanden.

Fortgeschrittene Techniken

Alias für Register und Konstanten

Es ist manchmal sinnvoll, Register passend der aktuellen Situation zu benennen oder „magischen Zahlen“ einen Namen zu geben. Deshalb gibt es die alias-Direktive. Ebenfalls kann man arithmetische Ausdrücke formen. Diese werden wohlgemerkt beim Übersetzen ausgewertet und nicht zur Laufzeit.

Makros

Makros (auch Pseudoinstruktionen genannt) können definiert und an beliebigen Stellen wie eine Instruktion benutzt werden. Sie sind praktisch, wenn man mehrere Operationen zusammenfassen und nicht ständig ausschreiben oder ihnen eine sofort ersichtliche Bedeutung geben möchte. Beispielsweise lässt sich eine mov-Pseudoinstruktion schreiben, die ein Register kopiert. Makros dürfen, wie im Beispiel, mehrere Platzhalter haben, in denen etwas eingesetzt wird. Es ist wichtig zu verstehen, dass Makros beim Übersetzen durch deren Inhalt ersetzt und die Assemblerinstruktionen darin ausgeschrieben werden.

Befehlsreferenz

Nachfolgend werden alle Befehle des ourMIPS-Prozessors vorgestellt. ri, rj, rk stehen dabei für beliebige Register, v für eine beliebige Zahl und a für eine beliebige Programmadresse, ausgedrückt als Label. Grundsätzlich setzen alle Befehle, welche die ALU verwenden, das Overflow-Flag, wenn bei Addition oder Subtraktion ein Overflow auftrat.

Lade- und Speicherbefehle
Op-CodeAssemblercodeBeschreibung
111000ldd ri, rj, vLädt den Inhalt der Speicherzelle mit Adresse ri+v in rj.
111001sto ri, rj, vSchreibt den Inhalt von rj in die Speicherzelle mit Adresse ri+v.
Arithmetische Operationen mit direktem Wert
Op-CodeAssemblercodeBeschreibung
011000shli ri, rj, vSchiebt alle Bits von ri um v mod 32 Stellen nach links, füllt rechts mit Nullen auf und legt das Ergebnis in rj ab.
011001shri ri, rj, vSchiebt alle Bits von ri um v mod 32 Stellen nach rechts, füllt links mit Nullen auf und legt das Ergebnis in rj ab.
011010roli ri, rj, vSchiebt alle Bits von ri um v mod 32 Stellen nach links und rechts wieder herein und legt das Ergebnis in rj ab.
011011rori ri, rj, vSchiebt alle Bits von ri um v mod 32 Stellen nach rechts und links wieder herein und legt das Ergebnis in rj ab.
011100subi ri, rj, vSubtrahiert von dem Wert in ri den Wert v und legt das Ergebnis in rj ab. Die Codierung von v wird zuerst von 16 Bit auf 32 Bit vorzeichenerweitert. Setzt das Overflow-Flag genau dann, wenn im Zweierkomplement das Vorzeichen beider Operanden verschieden war, aber das Ergebnis ein anderes Vorzeichen als ri hat.
011101addi ri, rj, vAddiert auf den Wert in ri den Wert v und legt das Ergebnis in rj ab. Die Codierung von v wird zuerst von 16 Bit auf 32 Bit vorzeichenerweitert. Setzt das Overflow-Flag genau dann, wenn im Zweierkomplement das Vorzeichen beider Summanden gleich war, aber das des Ergebnis' davon verschieden.
Arithmetische Operationen mit Registern
Op-CodeAssemblercodeBeschreibung
010000shl ri, rj, rkSchiebt alle Bits von ri um rj mod 32 Stellen nach links, füllt rechts mit Nullen auf und legt das Ergebnis in rk ab.
010001shr ri, rj, rkSchiebt alle Bits von ri um rj mod 32 Stellen nach rechts, füllt links mit Nullen auf und legt das Ergebnis in rk ab.
010010rol ri, rj, rkSchiebt alle Bits von ri um rj mod 32 Stellen nach links und rechts wieder herein und legt das Ergebnis in rk ab.
010011ror ri, rj, rkSchiebt alle Bits von ri um rj mod 32 Stellen nach rechts und links wieder herein und legt das Ergebnis in rk ab.
010100sub ri, rj, rkSubtrahiert von dem Wert in ri den Wert in rj und legt das Ergebnis in rk ab. Setzt das Overflow-Flag genau dann, wenn im Zweierkomplement das Vorzeichen beider Operanden verschieden war, aber das Ergebnis ein anderes Vorzeichen als ri hat.
010101add ri, rj, rkAddiert auf den Wert in ri den Wert in rj und legt das Ergebnis in rk ab. Setzt das Overflow-Flag genau dann, wenn im Zweierkomplement das Vorzeichen beider Summanden gleich war, aber das des Ergebnis' davon verschieden.
Bitweise Operationen
Op-CodeAssemblercodeBeschreibung
010111..00or ri, rj, rkVerknüpft die jeweiligen Bits in ri mit denen aus rj durch Oder und legt das Ergebnis in rk ab.
010111..01and ri, rj, rkVerknüpft die jeweiligen Bits in ri mit denen aus rj durch Und und legt das Ergebnis in rk ab.
010111..10xor ri, rj, rkVerknüpft die jeweiligen Bits in ri mit denen aus rj durch exklusives Oder und legt das Ergebnis in rk ab.
010111..11xnor ri, rj, rkVerknüpft die jeweiligen Bits in ri mit denen aus rj durch exklusives Nicht-Oder und legt das Ergebnis in rk ab.
Sprungbefehle
Op-CodeAssemblercodeBeschreibung
100000jmp aSetzt den Befehlszähler auf die direkt angegebene Adresse (symbolisiert durch das angegebene Label), sodass im nächsten Takt die Instruktion an Adresse a ausgeführt wird.
100010beq ri, rj, aSpringt zu Adresse a, wenn ri=rj. In der Instruktion wird die Adresse relativ zur Adresse dieses Sprungbefehls codiert.
100011bneq ri, rj, aSpringt zu Adresse a, wenn rirj. In der Instruktion wird die Adresse relativ zur Adresse dieses Sprungbefehls codiert.
100100bgt ri, rj, aSpringt zu Adresse a, wenn die Werte ri>rj. Die Bits in den Registern werden dabei jeweils als einen vorzeichenlosen Wert interpretiert. In der Instruktion wird die Adresse relativ zur Adresse dieses Sprungbefehls codiert.
100101bo ri, rj, aSpringt zu Adresse a, wenn das Overflow-Flag gesetzt ist. In der Instruktion wird die Adresse relativ zur Adresse dieses Sprungbefehls codiert.
Stackbefehle
Op-CodeAssemblercodeBeschreibung
001000ldpc riDer Inhalt des (16 Bit) Befehlszählers, also die Adresse dieser Instruktion, wird in die unteren 16 Bit von ri geschrieben. Die oberen 16 Bit werden auf 0 gesetzt.
001001stpc riDie unteren 16 Bit von ri werden in den Befehlszähler geschrieben und geben somit die Adresse der nächsten auszuführenden Instruktion an.