So, hallo allerseits.
Ich begrüße euch ganz herzlich zur heutigen Übung in der Technischen
Informatik II.
Thema der heutigen Übung ist MIPS Assembler.
Vorher möchte ich aber auf einige Sachen hier eingehen, und zwar
einige Visualisierungen zum Stoff, was wir bis jetzt behandelt haben,
hier in der Vorlesung mal kurz vorstellen.
Diejenigen von euch, die regelmäßig auf die TI-Homepage gehen, solange
unser Web-Server funktioniert, die haben das vielleicht mal bemerkt.
Das erste ist die Simulation der Minimal Machinery, mit der wir hier
gearbeitet haben bis jetzt.
Dargestellt ist hier die MIMA, wie wir das ja schon kennen, also mit
den Registern, Instruction Address Register, IR-Register,
ALU mit den Eingangsregistern X und Y, mit dem Ausgangsregister Z,
Akku, ein 1-Register und dann die Speicheradressregister und
Speicherdatenregister.
Mithilfe von diesem Applet hier kann man die Abläufe, also die
Befehlsabarbeitung hier in der MIMA, mal visualisieren, anschauen.
Beispiel, hier kann man zunächst mal Befehl auswählen.
Implementiert sind die Befehle Add, Load Constantly in Accumulato,
Load Variable in Accumulato, NAT und STV, also Speicherwert an einer
bestimmten Adresse.
Nehmen wir mal LDC, das heißt irgendein Wert, eine Konstante C soll in
den Accumulato geladen werden.
Und dann gehen wir mal auf Start.
Hier sehen wir im Steuerwerk die einzelnen Phasen der Fetch-Phase,
also die einzelnen Taktzyklen, die die Fetch-Phase durchläuft.
Das sind die Taktzyklen 1 bis 5.
Im sechsten Taktzyklus wird der Befehl dekodiert durch das spezielle
Bit D, also das achte Bit im Mikrobefehlsformat, was wir ja kennen.
Und jetzt kann man Schritt für Schritt gehen und sehen, was da
passiert.
Das heißt, im ersten Schritt haben wir ja schon im zweiten Wert 1 nach
Y und R.
Die Anforderung an Speicher ist noch, also Leseanforderung liegt noch
an.
Hier wird addiert.
Hier wird das Ergebnis von Z in IAR geschrieben.
Und anschließend wird das geholte Datum, also die Konstante aus dem
Speicher, im Speicherdatenregister stehen und wird zum Instruction
Register IR transferiert.
Im nächsten Takt wird der Befehl dekodiert.
Und anschließend wird der Befehl dekodiert.
Das heißt, eine Konstante soll in den Akkumulator geladen werden.
Die Konstante ist im Befehl eingegeben.
Das heißt, die Konstante steht hier im Instruction Register.
Deshalb dauert die Ausführungsphase von diesem Befehl nur einen
Taktzyklus, in dem der Inhalt vom IR oder ein Teil dieses Inhalts hier
in den Akkumulator geschrieben wird.
Okay, so viel dazu.
Das ist die erste Stufe der MIMA.
Ich habe mich heute Abendmittag sehr über eine E-Mail gefreut von
einem Herrn Markus Westphal.
Ist er hier?
Nein?
Oh, das ist aber schade.
Er hat nämlich eine weitere Visualisierung für die MIMA implementiert.
Er hat einen kleinen Simulator für die MIMA geschrieben, der die
Abläufe der Mikrobefehlsebene simuliert.
Das heißt, es obliegt dem Benutzer, die Mikroprogramme erst einmal zu
schreiben, bevor dann Assemblerprogramme ausgeführt werden können.
Auch interessant, sehr interessant.
Ich kam noch nicht dazu, das Ganze richtig anzuschauen.
Aber hier ist zunächst einmal die Einleitung und wie man das aufrufen
wird.
Ich werde das Ganze auch zum Testen und zum Anschauen auf der TI
-Homepage bereitstellen.
So.
Die andere Sache betrifft ja die Adressierungsarten, die wir in der
Vorlesung behandelt haben.
Das heißt, die Berechnungsvorschriften, um die Adressen von Operanden
zu bekommen.
Wir haben gestern in der Vorlesung eine ganze Reihe von Möglichkeiten
von Berechnungsvorschriften kennengelernt.
Das Ganze ist hier auch auf der TI-Homepage visualisiert.
Und zwar für die Registeradressierungen und einstufigen
Speicheradressierungen.
Die zweistufigen Speicheradressierungen sind noch nicht visualisiert.
Falls sich jemand hier angesprochen fühlt, dann würde ich mich sehr
freuen, das hier einmal zu erweitern.
Das heißt zum Beispiel eine explizite Registeradressierung.
Hier steht immer die Beschreibung von dem Befehl, also DECREMENT R1.
Hier haben wir die Architektur von einem Mikroprozessor.
Das heißt mit Steuerwerk, mit einem Adresswerk, mit den Registern, die
wir ja schon kennen.
Adressbusbuffer, Datenbusbuffer, eine ALU, ein Registersatz, wobei das
Register R1 jetzt als Akku hier dient.
Und ein Speicher mit vier Speicherzellen mit den Adressen 0 bis 3.
Also der Befehl lautet DECREMENT R1.
Opcode ist 34 Hexadezimal.
Und hier beginnt jetzt die Animation.
Als erstes wird natürlich der Programmzähler gelesen, in den
Adressbusbuffer geschrieben.
Und dann wird der Befehl im Speicher adressiert.
Das heißt, das ist die 34, also der Befehl DECREMENT R1.
Als nächstes muss ja der Befehl hier vom Datenbusbuffer zum
Befehlsregister transferiert werden, wo das Ganze dekodiert wird vom
Steuerwerk.
Und bei einem mikroprogrammierten Steuerwerk hier das entsprechende
Mikroprogramm oder die Startadresse von dem Mikroprogramm ermittelt
wird.
In der Zwischenzeit kann natürlich hier auf den nächsten Wert
zugreifen werden im Speicher.
Das heißt, der Befehlszähler wurde hier automatisch inkrementiert.
Und dann hat man hier den nächsten Inhalt vom Speicher oder den
nächsten Befehl hier im Datenbusbuffer stehen.
In der Zwischenzeit wurde der Befehl hier dekodiert.
Das heißt, hier der Register R1 sollte ja inkrementiert werden.
Das heißt, dazu wurde eine 1 addiert.
Der Inhalt wurde hierher geholt.
Wir können mal einen Schritt zurück gehen, glaube ich.
Sollte ja funktionieren.
Also normalerweise tut es.
Jetzt eins weiter.
Naja, okay, und so weiter und so fort.
Gut, das Ganze kann man natürlich für die anderen Adressierungsarten,
die wir in der Vorlesung das letzte Mal behandelt haben oder gestern
behandelt haben.
So viel zum Thema Visualisieren.
Nun kommen wir zum Stoff der heutigen Übung.
Wir haben das letzte Mal mit MIPS Assembler angefangen.
Wir haben gesagt, dass wir hier einen Simulator, einen MIPS Simulator,
das ist der SPIM Simulator, verwenden werden.
Und wir haben angefangen, die Syntax der MIPS Assembler Sprache zu
besprechen und haben die MIPS Assembler Direktiven, die zum
Reservieren von Speicherblatt, zum Initialisieren von Operanten
dienen, behandelt.
Heute werden wir natürlich das Ganze fortsetzen.
Wir werden halt so weitermachen, dass wir unsere Assemblerprogramme in
MIPS Assembler schreiben können.
Bevor wir hier weitermachen, will ich jetzt auf diesen MIPS Simulator
eingehen, was man ja downloaden oder abrufen kann auf der TI Homepage.
Wenn man das Ganze hier startet, dann sieht man die unterschiedlichen
Segmente oder unterschiedliche Fenster, in denen hier alle Register
des Registersatzes zu sehen sind.
Das heißt, bei der Ausführung des Programms kann man diese
Registerinhalte R0 bis R31 hier sehen.
Beziehungsweise hier sind die Register sowohl durch ihre Nummern, also
R0 bis R31, als auch durch die Namen R0 beziehungsweise Zero, haben
wir das genannt, AT, V0, V1 und so weiter und so fort.
Außerdem sind hier die speziellen Register, die wir auch kennengelernt
haben bei der MIPS Architektur.
Das sind die Register Low und High, die zur Aufnahme des Ergebnisses
einer Multiplikation von 32-Bit-Operanten dienen, durch die
Multiplikationsdivisionseinheit im Prozessor, beziehungsweise zur
Aufnahme vom Quotient und vom Rest bei der Division.
Natürlich der Befehlszähler und die anderen speziellen Register von
dem Co-Prozessor, was ja zuständig ist für Unterbrechungen.
Das heißt Exception-Programm-Counter, Ursache der Exception und die
virtuelle Adresse, an der halt die Exception oder die Unterbrechung
stattgefunden hat.
Das werden wir noch genauer betrachten, wenn wir auf Ausnahmen und
Unterbrechungen zu sprechen kommen.
Gut, hier hat man das DEX-Segment, das heißt der Inhalt des Programms,
was hier normalerweise steht.
Hier Kernel-Segment, wie wir das ja schon kennengelernt haben bei der
Speicheraufteilung.
Und irgendwann mal haben wir ja hier ein Daten-Segment, Stack und
Kernel -Data.
Und außerdem haben wir hier weiterhin eine Konsoli, wo wir beim
Ausführen von Programmen unsere Eingaben machen, bzw.
wo dort die Ausgaben des Programms zu sehen sind.
Das Ganze funktioniert so, nachdem man das gestartet hat, ein
Assembler -Programm, was man in einem gescheiten Editor schreibt, also
nicht in Word zum Beispiel.
Also einen normalen Editor wie Emax beispielsweise.
Und ich werde hier irgendein Programm mal nehmen, das was wir ja schon
haben.
Das heißt, das Programm wird hier geladen und das Programm soll jetzt
hier zwei Zeilen addieren, subtrahieren, multiplizieren und
dividieren.
Beim Run kommt man hier die Start-Adresse bei der Ausführung des
Programms.
Das heißt, okay, und irgendwo sollte ich jetzt hier ein Fenster haben.
In diesem Fenster...
Hallo?
So, stopp.
Close.
Naja, starten wir das Ganze mal neu.
So, nochmal.
Das heißt, wir nehmen das hier, okay, und das jetzt ran, okay.
Das heißt, wir haben jetzt hier eine Konsoli, also ein Fenster
-Konsoli, wo wir die Zahl 1 eingeben.
Also es sollen zwei Integer zahlen, hier addiert, subtrahiert und so
weiter und so fort.
Die geben wir an und dann haben wir jetzt hier in diesem Fenster die
Ergebnisse.
Addition, Subtraktion, Multiplikation und Modulo Operation.
Also A Modulo B und der Rest A Div B.
So, hat jemand von euch den Simulator schon installiert?
Nein?
Hat jemand vor, den Simulator zu installieren?
Hand hoch.
Das ist sehr zu empfehlen.
Das ist vielleicht sehr mühselig am Anfang, in Assembler zu
programmieren, aber es macht wirklich viel Spaß.
Und man versteht vieles über die Abläufe im Prozessor, wenn man
anfängt, in Assembler zu programmieren.
Wer macht das Basis-Praktikum bei uns eigentlich?
Aha, naja, du musst, glaube ich, auch schon ein bisschen Assembler
programmieren, oder?
Gut, das waren die Messages, die Nachrichten oder die Sachen, die ich
heute hier vor Beginn dieser Übung bringen wollte.
Nun machen wir weiter mit dem Stoff.
Und zwar, wir
werden als nächstes hier ein Beispiel mal betrachten, wo wir diese
Verwendung von den Assembler-Direktiven mal ansehen.
Hier ist ein Beispiel angegeben.
Das heißt, mit der Direktive .data wissen wir, dass die folgenden
Daten jetzt die Daten, die demnächst folgen, im Datensegment sein
werden oder stehen werden.
Wir können hier zum Beispiel mit .ascii.z jetzt ein String, was ja mit
einem Nullbyte terminiert ist, definieren, Speicherplatz allokieren
für diesen String mit der Direktive .ascii.z.
Und das Ganze mit einem Nullbyte terminiert im Speicher ablegen.
Beispielsweise können wir Fließkomma-Zellen einfacher Genauigkeit auf
diese Art und Weise definieren oder Speicherplatz für vier Bytes hier
mit der Direktive .space reservieren.
Wir können natürlich hier Wörter, also Integer mit Vorzeichen in dem
Fall, auch definieren und mit bestimmten Werten initialisieren.
Also in dem Fall hier .h0, da haben wir praktisch ein Integer mit
Vorzeichen, 32-Bit-Wort, was ja mit Null initialisiert ist,
beziehungsweise mit 16 Dezimal in der Form oder 100 Hexadezimal bzw.
256.
Beide Varianten sind hier möglich, das heißt, dass man hier Integers
in dezimaler Schreibweise bzw.
hexadezimaler Schreibweise angeben kann.
Mit der Assembler-Direktive .txt, wissen wir, es folgt der
Programmcode, das heißt, das alles war jetzt der Datenteil.
Das waren hier die Daten.
Und ab jetzt hier, alles was nach .txt folgt, ist jetzt unser
Programmcode.
Und in unseren Programmen, MIPS Assembler-Programmen, haben wir eine
Marke, die als globales Symbol deklariert werden muss.
Das ist die Marke Main.
Das macht man hier halt mit dieser Assembler-Direktive .global-main.
Und dann irgendwann mal haben wir ja die Marke Main, wo die einzelnen
Befehle, zum Beispiel zum Addieren, Subtrahieren, Laden, Speichern von
Operanden stehen.
Bevor wir jetzt auf den Aufbau, auf die Befehle, MIPS Befehle
eingehen, wollen wir eine ganz hilfreiche Sache mal betrachten, und
zwar das Eingabe-Ausgabe-System von unserem Simulator.
Das heißt, wie wir jetzt gesehen haben vorher, hatten wir ja die
Möglichkeit, Zahlen einzugeben.
Das sollte ja auch so sein, ansonsten hat das Ganze keinen Sinn.
Und konnten wir die Ergebnisse der Berechnungen eines Programms in
einem Konsolifenster mal betrachten.
Und hierzu bietet uns der Simulator mithilfe von sogenannten System
-Calls oder System-Aufrufe die Möglichkeiten zur Eingabe von Werten,
also von Strings, von Integers, von Gleitkommazahlen, bzw.
zu deren Ausgabe aus dem Bildschirm.
Das Ganze funktioniert so, es gibt eine bestimmte Anzahl von System
-Aufrufen und um jetzt einen, also einer dieser Dienste hier in
Anspruch zu nehmen, muss man nur die Nummer von dem jeweiligen Dienst
in das Register V0 laden.
Das ist vielleicht jetzt ersichtlich, warum man dieses Register V0 als
Programmierer nicht benutzen sollte für andere Sachen.
Also die werden beispielsweise hier in das Register V0, diese Nummer
von dem Dienst hier geladen und anschließend wird der Befehl zu
Scroll, das ist ein Befehl von MIPS aufgerufen.
Das heißt, wenn wir hier LI V0,1 machen und dann zu Scroll, dann
werden wir auf dem Bildschirm die Zahl, oder wird auf dem Bildschirm
die Zahl ausgegeben, die im Register A0 steht.
Das heißt, die Zahl, die man ausgeben muss, muss man zunächst mal in
das Register A0 bringen und anschließend halt hier diesen System
-Aufruf betätigen oder in Gang setzen, damit man eine Integerzahl
ausgeben kann.
Das Gleiche für die Ausgabe von Floats, also von Gleitkommazahlen
einfacher Genauigkeit, da muss man dafür sorgen, das ist ja die Nummer
2, also der Dienst Nummer 2 und da muss man dafür sorgen, dass die
auszugebende Zahl im Register F12 steht.
Bei Gleitkommazahlen in doppelter Genauigkeit, das ist der System
-Aufruf Nummer 3 und hier muss die in doppelter Genauigkeit
auszugebende Gleitkommazahl, das heißt die 64-Bit-Zahl in den
Registern F12 und F13 stehen.
Wie wir vielleicht wissen, alle Register, die mit F anfangen, sind
Register von dem mathematischen Co-Prozessor des MIPS-Rechners.
Das heißt, der Co-Prozessor hat, wie wir das letzte Mal erwähnt haben,
32 Register für die Aufnahme von Floating-Point-Operanten und bei
einer Double-Zahl, also 64 Bits, müssen wir diese Zahl auf zwei
Register splitten bzw.
diese Zahl in zwei Registern hier speichern.
Wenn wir Strings, also Zeichenketten, ausgeben wollen, dann müssen wir
ja den Dienst Nummer 4 in Anspruch nehmen und hier müssen wir als
Parameter die Adresse von dem auszugebenden String in das Register A0
laden.
So, das muss man alles nicht auswendig lernen.
Hierzu sind die Folien da.
Man muss natürlich wissen, was die einzelnen Sachen sind und wie das
Ganze funktioniert.
Für die Eingabe haben wir die entsprechenden Möglichkeiten, eine
Integer -Zahl zu lesen bzw.
eine Gleitkommazahl zu lesen, die in einfacher bzw.
Doppeltegenauigkeit ist, oder einen String zu lesen.
Das sind hier die entsprechenden Nummern 5, 6, 7 und 8.
Und wenn wir jetzt Li, V0, $V0, 5 und dann Syscall machen, dann wird
jetzt eine Zahl von der Konsole eingelesen und diese Zahl steht
anschließend im Register V0.
Das heißt, wenn man mit dieser Zahl irgendwelche Operationen ausführen
will, dann muss man auf das Register V0 zugreifen und die
entsprechenden Operationen erledigen.
Ein Float, also eine Gleitkommazahl in einfacher Genauigkeit, wird
nach der Eingabe im Register F0 stehen und eine Gleitkommazahl in
Doppeltegenauigkeit wird in die beiden Register F0 und F1 stehen.
Bei einem String, das ist der Dienst mit der Nummer 8, das heißt Li,
V0, 8, hat man hier die Adresse der Zeichenkette in A0 und die
maximale Länge in A1.
Die Zeichenkette ist in dem Fall auch 0 terminiert, das heißt ein 0
-Byte wird hier angehängt und bei weniger angegebenen Zeichen wird die
Zeichenkette zusätzlich mit New Line abgeschlossen.
Sonstige Systemaufrufe, einmal SBRK, das heißt die Nummer 9 dient zur
dynamischen Allokierung von Speicherplätzen.
Hier gibt man die Anzahl der Bytes, die man dynamisch allokieren will,
in das Register A0 und die Anfangsadresse in das Register V0.
Ein weiterer Systemaufruf ist EXIT, also die Nummer 10, wo halt das
Programm beendet wird.
Das heißt, wir sind jetzt schon in der Lage, Zahlen, Integerzahlen,
Gleitkommazahlen einzulesen von der Konsole bzw.
auszugeben.
Das heißt, wir sind eigentlich in der Lage, unser erstes oder unsere
ersten kleinen Programme hier mal zu schreiben.
Das heißt, wenn wir jetzt unser typisches Hello MIPS Word Programm
hier bauen wollen oder implementieren wollen, dann müssen wir ja diese
Zeichenkette hier, was auszugeben ist, ablegen im Speicher.
Das können wir mit der Direktive .ascii.z machen.
Das heißt, das Ganze wird mit einem Null-Byte terminiert.
Und das Ganze liegt an der Adresse string hier.
In unserem Text, also im Code, müssen wir jetzt hier für die Ausgabe
von String, also Print String, wie wir wissen, das ist der
Systemaufruf mit der Nummer 4.
Das heißt, wir müssen die 4 in das Register V0 laden und anschließend
zu Scroll machen.
In dem Fall wird die Zeichenkette ausgegeben, also die an der Adresse
A0 steht.
Deshalb laden wir die Adresse von String in das Register A0, bevor wir
diesen Systemaufruf machen.
Das heißt, die Argumente in A0, also die Adresse von String in A0
laden, anschließend die Nummer von dem Systemaufruf und dann zu
Scroll.
So, soweit klar?
Wenn wir eine Integerzahl ausgeben wollen, dann mit einer neuen Zeile,
da müssen wir jetzt diese neue Zeile, wie wir wissen, auch so
definieren, also mit Slash N.
Und dann Print Integer, das heißt, hier haben wir die Nummer 1, das
heißt, die Zahl, die im Register A0 wird ausgegeben, nach dem Aufruf
von Syscall.
Und anschließend geben wir dieses Newline, das heißt, wir müssen die
Adresse von diesem String hier in L.A.
laden und wiederum den Systemaufruf mit der Nummer 4 aufrufen,
Syscall.
Und somit wird praktisch die Integerzahl anschließend Newline hier
ausgegeben.
Und das Programm wird dann fortgesetzt mit der nächsten Adresse.
Das braucht ihr jetzt noch nicht zu verstehen, das können wir
vergessen.
So, das sind zwei kleine Programme, die wir jetzt mit Hilfe von diesen
Mitteln, die wir kennengelernt haben, also mit Hilfe von diesem
Eingabe -Ausgabe-System aufrufen, implementieren können.
Die allgemeine Struktur von einem MIPS-Programm sieht wie folgt aus.
Wir haben zunächst mal die globalen Daten, die im Datensegment
abgelegt werden.
Anschließend haben wir ein Textsegment, in dem zunächst mal
Unterprogramme wie Print Integer oder Print String, also die beiden
kleinen, was wir schon jetzt realisiert haben.
Und dann die Deklaration von mir als eine globale Marke.
Und anschließend kommt ein Teil hier und ein Teil hier, was Standard
bei MIPS-Programmen ist.
Hier ist der eigentliche Code von unserem Programm.
Und was hier in diesen vier Zeilen geschieht, kann man hier anhand von
den Kommentaren ablesen.
Das heißt, wenn wir MIPS-Programme für einen echten MIPS-Rechner
schreiben, also nicht für den Simulator, müssen wir diese Sachen hier
machen.
Der SPIM-Simulator braucht das nicht.
Das wird vom Assembler automatisch erledigt.
Diese zwei Blöcke, sage ich mal hier, wo zunächst mal ein Stack, also
ein Rahmen auf dem Stack für das Main-Programm allokiert wird, in dem
die Rücksprung-Adresse zum Betriebssystem gesichert wird und der alte
Frame -Pointer gesichert wird.
Und anschließend, bevor das Programm beendet wird, werden die
Rücksprung -Adresse und der alte Frame-Pointer wiederhergestellt.
Wir werden das Ganze besser verstehen, wenn wir diese einzelnen
Befehle hier kennen.
Aber was eigentlich passiert, ist folgendes.
Das ist der Speicher, wie wir den kennen.
Der Stack-Pointer zeigt auf die RC-Freistelle auf dem Stack und wir
wissen, dass der Stack hier von oben nach unten wächst.
Das heißt, beim Aufruf eines Programms wie Main wird zunächst mal ein
Rahmen hier auf dem Stack allokiert.
Und dieser Rahmen ist hier acht Byte groß.
In diesen acht Byte werden zwei Register gesichert.
Einmal das Register RA und einmal das Register FP.
Also die Rücksprung-Adresse zum Betriebssystem, also von Main aus zum
Betriebssystem und der Frame-Pointer.
Und dieses Allokieren vom Speicher macht man, indem man von dem alten
Wert vom Stack-Pointer eine 8 subtrahiert.
Also hier steht 8 vom alten Stack-Pointer und speichert das Ergebnis
in SP.
Das heißt, der Stack-Pointer zeigt jetzt hier auf die RC-Freistelle.
Und dann sichert man jetzt den Inhalt von RA, die Rücksprung-RA, das
ist das Register, welches immer die Rücksprung-Adresse zum aufrufenden
Programm sozusagen enthält.
Das sichert man an der Stelle 0 plus SP, das heißt eine
Adressierungsart, bei der der Inhalt vom SP genommen wird.
Eine 0 dazu addiert, also das heißt RA wird hier abgelegt.
FP wird an der Stelle SP plus 4 gesichert.
Anschließend wird jetzt ein neuer Frame-Pointer definiert.
Und zwar, der ergibt sich immer aus dem neuen Stack-Pointer plus 8.
Das heißt, der Frame-Pointer zeigt eigentlich auf dem alten Stack
-Pointer.
Wird beim MIPS auch dieser Frame-Pointer nicht benutzt.
Aber prinzipiell funktioniert die Sache so, nachdem man das Ganze
gemacht hat, müssen diese Werte, die Rücksprung-Adressen und der alte
Frame -Pointer hier wiederhergestellt werden.
Das heißt, es wird der Inhalt der Speicherzelle SP plus 0 in RA
geladen.
Hier haben wir den Inhalt von RA in 0 plus SP gespeichert.
Und hier der Inhalt von SP plus 4 wird wiederum in FP geladen.
Und man addiert zum Stack-Pointer die 8 dazu, sodass dieser
Speicherplatz hier wieder freigegeben wird und macht weiter mit dem
nächsten Befehl.
Gibt es Fragen dazu?
Wir werden diese Sachen noch genauer verstehen, wenn wir das
Unterprogramm -Konzept in Assembler hier betrachten.
Das heißt, wenn jetzt das Main-Programm Unterprogramme aufruft, dann
müssen diese Unterprogramme natürlich gewisse Sachen machen.
Es müssen bestimmte Register gesichert werden und das macht man
natürlich auch auf dem Stack.
Das heißt, wenn jetzt hier innerhalb von diesem Hauptprogramm ein
Unterprogramm aufgerufen wird,
naja, was ja mit dem Befehl Jump & Link gemacht wird, also Jump & Link
Subroutine zum Beispiel, dann wird für diese Subroutine hier auf dem
Stack nochmal so ein Speicherbereich allokiert, in dem auf jeden Fall
die Rücksprungadresse zum Hauptprogramm abgesichert wird, der Frame
-Pointer gesichert wird und noch vielleicht bestimmte Enthalte von den
Registern S0 bis S7, die langlebige Variablen enthalten, falls diese
Register vom Unterprogramm verändert werden und die Enthalte dieser
Register aber nach dem Unterprogrammaufruf noch benötigt werden.
Das heißt, in dem Fall müssen derartige Register hier auf dem Stack
auch gesichert werden.
Wenn das Unterprogramm abgearbeitet wird, dann springt man zurück aus
dem Unterprogramm in das Main-Programm und dann muss dieser
Speicherbereich hier, der reservierte Speicherbereich auf dem Stack
wieder freigegeben werden.
Das werden wir noch ein bisschen genauer betrachten.
Wir machen hier weiter mit MIPS, damit wir unsere ersten einfachen,
aber kompletten Programme schreiben können.
Wir gucken uns mal die Adressierungsarten bei MIPS-Rechner.
Der MIPS-Rechner oder Prozessor unterstützt vier Adressierungsarten.
Das ist eine Besonderheit von diesen MIPS- bzw.
reinen RISC-Prozessoren, die eine wenige oder wenige Anzahl von
Adressierungsarten erlauben im Gegensatz zu CISC-Rechnern, die mit
einer Vielzahl, wie wir gestern gesehen haben, von Adressierungsarten
arbeiten, was natürlich halt vielleicht die Programme kürzer macht,
weil man dadurch komplizierte und sehr mächtige Befehle realisieren
kann.
Aber wir werden in der nächsten Vorlesung die Vorteile und Nachteile
beider Konzepte, also RISC- und CISC-Konzepte, näher erläutern.
Also die Adressierungsarten hier sind einmal die explizite
Registeradressierung, das heißt, der Operand steht in einem Register.
Die direkte Adressierung, das heißt, der Operand ist eine Konstante im
Befehlswort.
Eine Basisadressierung, der Operand ist im Speicher an der Adresse,
die sich durch Addition von einem Registerinhalt und einer Konstante
im Befehl ergibt.
Und eine Befehlszählerrelative Adressierung, das heißt, die Adresse
ergibt sich aus dem Wert des Befehlszählers und einer Konstante im
Befehl.
Hier ist das Ganze nochmal veranschaulicht, wie wir das gestern
kennengelernt haben.
Registeradressierung, das heißt, der Operand ist in einem Register
angegeben.
Eine direkte Adressierung, das ist im Befehl angegeben.
Basisadressierung, das heißt, die Adresse des Operanden ergibt sich
durch die Addition von einer bestimmten Konstante im Befehl zum Inhalt
eines Registers.
Und Befehlszählerrelative Adressierung, das heißt, die Adresse des
Operanden im Speicher ergibt sich durch die Addition von einem
Register zum Befehlszähler...
Entschuldigung...
durch die Addition von einer Konstante, einer 16-Bit-breiten Konstante
zum Befehlszähler.
So, das kennen wir alles.
Das ist nur hier speziell für den MIPS-Rechner.
Welche Datenformate sind hier vorhanden oder womit arbeiten wir hier?
Das sind Bytes, Halbwörter und Wörter, also 8, 16 und 32 Bits.
Die Ordnung von Bytes und Wörtern bzw.
Wörtern in mehrfachen Wortstrukturen ist bei dem MIPS-Simulator oder
der MIPS-Rechner unterstützt eigentlich beide Konventionen, beide Byte
-Anordnungen in Wörtern, und zwar sowohl Little als auch Big, Indian
oder.
Bei unserem Simulator hängt das davon ab, auf welchem Rechner der
Simulator läuft.
Das heißt, wenn der Simulator auf einem Entel-Rechner läuft, der mit
Little -Indian-Byte-Order arbeitet, dann hat man praktisch ein Little
-Indian -Order.
Wenn der Simulator auf anderen Maschinen läuft, auf denen halt die
Byte -Order Big-Indian ist, dann hat man diese Big-Indian-Byte
-Anordnung.
Vorzeichenbehaftete Zeilen werden in Zweier-Komplimentform
dargestellt.
Ganze Zeilen werden entweder vorzeichenlos, also anzeigend oder
vorzeichenbehaftet dargestellt.
Fließkomma-Zeilen, einmal einfache Genauigkeit, kennen wir ja mit 32
-Bit -Breite und doppelte Genauigkeit mit 64-Bit-Breite.
Was dabei zu beachten ist hier bei der Erstellung von Programmen, in
denen Fließkomma-Zeilen in doppelter Genauigkeit bearbeitet werden,
ist, dass alle Register von dem Floating-Point-Core-Prozessor 32-Bit
-Breite sind.
Das heißt, um einen Operanden aufnehmen zu können, braucht man zwei
Register und deshalb dürfen bei Arithmetik-Operationen in doppelter
Genauigkeit nur die Register mit geraden Registernummern verwendet
werden.
Also F0, F2, F4, F6, F8 und so weiter und so fort.
Das Speichermodell, das Wort hat in der MIPS-Welt eine Breite von 32
-Bit oder 4-Bytes.
Der Speicher ist byte-adressierbar, es werden zwar Wörter geschrieben
und geladen aus dem Speicher, aber einzelne Bytes adressiert.
Das heißt, man hat 2 hoch 32 Bytes mit Byte-Adressen von 0 bis 2 hoch
32 minus 1 bzw.
2 hoch 30 Wörter mit Byte-Adressen von 0 bis 2 hoch 32 minus 4.
Die Wörter sind immer ausgerichtet, das heißt, die Adressen oder die
zwei niedrigwertigen Bits einer Wort-Adresse haben immer die Werte 0.
Man muss sich das Ganze so vorstellen, dass der Speicher hier als ein
eindimensionales Array aus Speicherzellen mit Adressen besteht und die
Speicher -Adresse ist nichts anderes als der Index auf ein Element in
diesem Array.
Byte-Adressierung heißt in dem Fall, dass der Index auf ein Byte im
Speicher zeigt.
Das ist eigentlich ganz einfach und leicht, wenn wir das Ganze hier
nochmal ein bisschen veranschaulichen.
Hier ist der Register-Satz mit den 32 Registern von 0 bis 31, die
weiteren Register HLO und Programm-Counter und der Speicher mit den
Wort -Adressen von 0 bis 2 hoch 32 minus 4.
Die Speicher-Byte-Anordnung, wie gesagt, kann einmal sein, dass wir
hier das höchstwertige Byte an niedrigster Adresse haben,
beziehungsweise das höchstwertige Byte an höchster Adresse.
Das heißt, entweder Big- oder Little-Endian-Format und MIPS
unterstützt, wie gesagt, beide Varianten, je nachdem, welche Byte auf
dem zugrundeliegenden Rechner unterstützt oder realisiert wird.
Die Befehlsformate haben wir ja gestern kennengelernt.
Das sind Befehle vom Typ I, J und R.
Deshalb werde ich jetzt nicht hier nochmal genauer darauf eingehen.
Das ist die Beschreibung hier oder die Beschreibung für die
Abkürzungen, die man in diesen Befehlsformaten hat.
Wichtig ist einmal diese unmittelbaren Werte, die man in den
Befehlsformaten eingeben kann.
Die sind 16-Bit-breit, beziehungsweise 26-Bit-breite Sprungadressen.
Diese Shift-Amount, das heißt, das ist die Größe der Verschiebung bei
Rotations - und Verschiebungsbefehlen und Function gibt halt die
Funktionalität an.
Das Ganze wird klarer, wenn man hier die Befehlsformate von Add
betrachtet, was wir ja auch gestern gesehen haben.
Das heißt, anhand von diesem letzten Feld im Befehlsformat, die Add
-Befehle, die haben immer alle ein Opcode von 0 und anhand von diesem
letzten Function-Feld wird festgelegt, ob das ein Add-Befehl with
Overflow, without Overflow, ein Add-Befehl zu einem unmittelbaren
Wert, also Add-E, Add-U oder ein normaler, reiner Add-Befehl ist.
So, jetzt werde ich ganz schnell hier durch alle Befehle von MIPS
durchgehen.
Und ihr müsst euch ein bisschen konzentrieren, dann nehmt ihr
vielleicht das eine oder andere mit.
Das meiste kann man natürlich hier jetzt bei mir nicht lernen.
Man muss halt diesen Befehlssatz nehmen, wenn man anfängt, in
Assembler zu programmieren.
Man überlegt sich, was will ich jetzt für eine Operation machen?
Gibt es so einen entsprechenden Befehl, was dies für mich macht?
Das heißt, ihr müsst ja diese SPIM-Dokumentation, den Befehlssatz, was
auf der TI-Homepage liegt, downloaden, vielleicht drücken oder nicht.
Und dann mal gucken, was die einzelnen Befehle heißen, was brauche ich
jetzt?
Ich will addieren, ich will subtrahieren, multiplizieren, dividieren
und so weiter und so fort.
Hier gehen wir zunächst mal durch diese unterschiedlichen Arten von
Befehlen und wir fangen mit den arithmetischen Befehlen an.
Es gibt zunächst mal eine Möglichkeit, den Absolutwert von einem
Register.
Das heißt, wenn wir jetzt an den Absolutwert einer Vorzeichen
behafteten Zahl, die in einem Register steht, interessiert ist, dann
können wir mit dem Befehl ABS das machen.
Das ist wohlgemerkt kein echter MIPS-Befehl, sondern das ist ein
Zusatz, ein sogenannter Pseudo-Befehl, was der Assembler bereitstellt,
um den Absolutwert vom Register hier bilden zu können.
Für die Addition unterscheidet man zwischen unterschiedlichen
Varianten add, addu und addi.
Das gleiche auch für die Subtraktion, add, sub und subu.
Der Unterschied ist, dass bei dem add-Befehl hier
Bereichsoberschreitungen signalisiert werden.
Das heißt, wenn wir jetzt hier die Inhalte von zwei Register addieren,
das Ergebnis im Zielregister, was ja immer das erste Register ist,
schreiben, dann werden bei der add-Variante Bereichsoberschreitungen
signalisiert und erkannt, damit man sie entsprechend behandeln kann.
Bei addu, das heißt, das ist eine Addition von vorzeichenlosen Zellen
und da werden solche Bereichsoberschreitungen gar nicht
berücksichtigt.
Das ist eigentlich sehr irreführend und sehr falsch, meiner Meinung
nach, weil auch bei vorzeichenlosen Zellen Bereichsoberschreitungen
auftreten können.
Das ist ja klar, wenn wir zwei sehr große positive Zellen addieren,
dann noch eine größere positive Zell kriegen, die wir nicht als 32-Bit
-vorzeichenlose Zell darstellen können, dann haben wir auch eine
Bereichsoberschreitung.
Also aufgepasst hier, das heißt hier werden Bereichsoberschreitungen
nicht berücksichtigt oder werden ignoriert sozusagen.
Das gleiche kann man machen hier, da gibt es die Variante, statt zwei
Register zueinander zu addieren, also egal, ob jetzt Vorzeichen
behaftet, with overflow, without overflow, gibt es eine dritte
Variante, wo man halt eine bestimmte Konstante hier zu dem Inhalt von
einem Register addiert und das Ergebnis halt in diesem Register
herschreibt.
Das sind die drei gängigsten Varianten der Additionsbefehle in MIPS.
So, bei der Division, das gleiche gilt natürlich hier für die
Subtraktion.
Bei der Addition ist anzumerken, dass man hier bei der Addition nur
zwei Register im Befehlsformat eingibt, das heißt nur ein RS und ein
RT.
Das ist natürlich klar, weil die Addition nicht durch die ALU des
Prozessors ausgeführt wird, sondern durch die Divisions- und
Multiplikationseinheit und diese legt die Ergebnisse der Division in
den beiden Registern Low und High.
Das heißt hier, der Quotient wird in dem Low-Register stehen, in dem
speziellen Low-Register und der Rest steht in dem High-Register.
Um auf die Ergebnisse hier zugreifen zu können, gibt es spezielle
Befehle, die heißen Move from Low.
Das heißt, es ist ein Befehl, wo wir auf das Register Low vom
arithmetischen Co-Prozessor zugreifen und den Inhalt von diesem
Register in ein Universärregister, also 0 bis 31, laden können,
beziehungsweise 1 bis 31 laden können, weil wir 0 nicht verändern
können.
Ja, für die Multiplikation gilt ja das Gleiche.
Es gibt auch die unterschiedlichen Varianten.
Also bei Mult gibt es ja natürlich hier nur die zwei Operanden, die
miteinander multipliziert werden.
Das Ergebnis wird wiederum in Low und High stehen, beziehungsweise
Mult U für anzeigend, also für vorzeichenlose Zahlen.
Und dann gibt es vom Assembler her die Variante, dass man wirklich
jetzt zwei Register miteinander multipliziert und das Ergebnis in
einem dritten Register schreibt.
Das heißt, diese ganzen Sachen wie Move from Low und from High und so
weiter übernimmt der Assembler oder erledigt dieser Befehl hier Mult
von selbst.
Ja, die Negation, das heißt, der Inhalt von dem Register wird negiert
und in das C-Register hier geschrieben.
Wohlgemerkt, bei diesen Notationen, bei allen arithmetischen und
logischen Befehlen, ist das RC-Register im Befehlsformat immer das C
-Register.
Das heißt, wenn wir schreiben Add T3, T2, S1, dann bedeutet das, dass
T3 ist gleich T2 plus S1.
Das heißt, das RC-Register ist immer hier das C-Register bei allen
arithmetischen Operationen.
Gut, hier sind nochmal die unterschiedlichen Befehlsformate.
Das haben wir ja gestern oder habt ihr ja gestern in der Vorlesung
gesehen.
Das heißt, die beiden Befehle hier sehr interessant zu sehen.
Das sind alle eigentlich Befehle vom R-Typ, das heißt der Register
-Register -Befehle.
Und hier sind die einzelnen Bits für die beiden Quellregister der
Addition, für das C-Register.
Ob-Code ist hier 0 und hier 0.
Es gibt keine Verschiebung hier bei einem Additions-Operation, deshalb
ist dieses Shunt-Feld, also Shift-Amount 0.
Aber dieses Function-Feld ist natürlich hier in dem Fall 20 und im
anderen Fall 21.
Das heißt, wenn jetzt die Frage käme, wir haben bei den MIPS-Rechnern
ein Ob-Code aus 6 Bits.
Also der Ob-Code ist 6 Bit breit.
Theoretisch gesehen kann man mit diesem Ob-Code dann nur wie viele
Befehle realisieren oder mit 6 Bits?
64!
Aber wir werden natürlich sehen, und das ist natürlich durch diese
unterschiedlichen Varianten der Befehle hier, dass wir mehr als 64
Befehle hier haben.
Das sieht man hier alleine an diesem Fall hier, wo wir den gleichen Ob
-Code haben, aber unterschiedlichen, also die anderen Felder im
Befehlsformat, die dienen auch dazu, um die Befehle zu spezifizieren.
Ja, bei Addieren von unmittelbaren Werten hier haben wir hier die
Möglichkeit, Vorzeichenlose und Vorzeichenbehaftete Werte hier zu
einem Quellregister zu addieren und das Ergebnis in dem Zielregister
RT zu schreiben.
Zusammengefasst für die arithmetischen Befehle, alle Befehle haben
drei Operanden.
Operandereihenfolge ist fest, Zielregister kommt zuerst und die
Operanden einer arithmetischen Operation müssen im Register stehen und
alle Register sind, wie wir wissen, 32 Bit breit.
Wir wollen mal anfangen, hier Beispiele anzuschauen.
Im C-Code haben wir immer A ist gleich B plus C, das ist ein Fehler
hier, Semikolon.
Im MIPS-Code wäre das Add S0, S1 und S2.
Das heißt S0, das ist auf jeden Fall definitiv A und S1 zum Beispiel B
und C.
Das heißt S1 plus S2 wird addiert und das Ergebnis in drei
geschrieben.
Anders sieht es aus und komplizierter sieht es aus, wenn man solche
Sachen hat, die man in C zum Beispiel schreiben kann.
A ist gleich A plus B plus C plus D und E ist gleich F minus A.
Dann ist es klar, dass man zunächst mal hier C plus D bilden muss und
das Ergebnis in T0 schreibt.
Dann addiert man T0 zu B und das Ergebnis schreibt man in S0, das wäre
dann A.
Und hier subtrahiert man A oder F von A, das heißt das ist unser A,
das wäre F und das Ergebnis steht in E.
Also Besonderheit, hier können wir jetzt keine, also nicht mehr als
drei Operanden eingeben.
Zwei davon sind Quelloperanden und ein Zieloperand und in dem Fall
hier müssen wir natürlich das Ganze aufsplitten, um es zu realisieren.
Beispiel, wie wir jetzt mithilfe von diesen Arithmetikbefehlen erstes
Arithmetikprogramm schreiben können, wie das, was wir gesehen haben
bei der Demonstration von dem SPIM-Simulator.
Das sind irgendwelche Daten, die hier vorhanden sind, wie Newline oder
Anforderungen zum Eingeben einer Zahl beziehungsweise irgendwelche
Kommentare bei der Ausgabe von der Summe, von der Differenz vom
Produkt und bei der Division.
Und wir wissen, dass wir jetzt hier eine Integerzahl mit einer neuen
Zeile mithilfe von dem Text hier ausgeben können.
Das heißt, die Nummer des Systemaufrufs ist 1 bei Print Integer to
School und anschließend Newline.
Das heißt, wir müssen ja die Adresse von diesem Newline von CR-String
in A 0 laden, anschließend die Nummer für Print String in V0 und zu
School.
Und natürlich haben wir jetzt hier weitere Sachen, die wir ja
kennengelernt haben, wie Print String.
Das heißt, wenn wir jetzt die Aufforderung zur Angabe der Zahl A haben
oder haben möchten, dann können wir hier natürlich die Adresse von
einem String, bitte Integerzahl A eingeben, hier in das Register oder
in A 0 laden an entsprechender Stelle und anschließend das Ganze hier
ausgeben.
So, den Block hier haben wir schon behandelt.
Jetzt wollen wir zum Beispiel zwei Zeilen addieren, eine Zahl A und
eine Zahl B.
Deshalb müssen wir zunächst mal die Zahl A holen, das heißt, laden die
Adresse von Eingabe A.
Eingabe A war ja eine Marke für, bitte geben Sie die Integerzahl A
ein.
Wir laden die Adresse in L A.
Wir gehen zu Unterprogramm Print String, das heißt Jump and Link, das
heißt, es wird zu diesem Unterprogramm Print String gesprungen und die
Rücksprungadresse zum Hauptprogramm wird im Register R A abgelegt.
Und anschließend haben wir praktisch die Adresse von dem String in A
0.
Das heißt, wir wollen jetzt hier einen String ausgeben, das heißt, das
ist die Nummer 5.
Und anschließend, Entschuldigung, nicht einen String lesen, sondern
wir möchten eine Integerzahl lesen.
Das heißt, nach diesem Systemaufruf würde die Integerzahl in V 0
stehen.
Wir wissen, dass wir eine weitere Zahl einlesen wollen, deshalb
sollten wir die erste Zahl A zum Beispiel von V 0 in ein Register
speichern, was wir bei diesen Systemaufrufen nicht benötigen, zum
Beispiel S 0.
Das heißt, unsere Zahl A steht in S 0.
Anschließend die Zahl B, die legen wir zum Beispiel auf die gleiche
Art und Weise in S 1.
Und dann für die Ausführung der Operation Add brauchen wir nichts
anderes zu machen als Add S 0 und S 1.
Das heißt, die Summe von S 0 und S 1 wird direkt im Register A 0
abgelegt hier, sodass man direkt das Ganze anschließend, also alle
Vorbereitungen, man hat damit alle Vorbereitungen getroffen, damit man
das Ergebnis nachher mit Hilfe von Print Int ausgeben kann.
Print Int erwartet, die auszugebende Integerzahl im Register A 0 und
das ist damit hier gemacht.
Ja, für die Differenz, für die Multiplikation und die Vision ist das
Ganze analog.
Interessant ist hier, sind bei der Division hier die Varianten, dass
man jetzt die beiden Zahlen, eine ganzzahlige Division der beiden
Zahlen hier ausführt und anschließend hier move from low, das heißt,
der Quotient steht im Register L 0, das wird ja in A 0 abgelegt, in
das Universal Register A 0 abgelegt und anschließend ausgegeben und
hier move from high, das heißt, der Rest wird wiederum auch in A 0
hier geladen und anschließend mit Hilfe von diesem Print Int
ausgegeben.
So, eigentlich sollten wir in der Lage sein, jetzt Arithmetik, also
Programme, für die wir Arithmetikbefehle benötigen, hier zu schreiben.
Das ist eine Schablone, die man hier sieht, wie das hier für die
Grundrechenoperationen realisiert ist.
Man nimmt das und man kann damit vieles realisieren.
Gut, natürlich gibt es nicht nur arithmetische Befehle, sondern es
gibt logische Befehle, logische Befehle wie zum Beispiel das logische
UND von den beiden Registern R S und R T.
Das Ergebnis wird in R D stehen, beziehungsweise ein AND mit einem
Immediate -Wert, das heißt R S mit dem Wert hier UND verknüpfen und
das Ergebnis in R D schreiben.
Ein NOR, das gleiche, ein NOT, das sind ja klar... Entschuldigung, da
ist hier ein Fehler.
Das heißt, das sind logische Operationen, die man hier realisieren
kann auf zwei Register.
XOR auf zwei Register, das Ergebnis in dem Register hier,
beziehungsweise auf ein Register und einen direkten Wert, den man hier
angeben kann.
Das ist immer gekennzeichnet durch dieses I für Immediate.
So, natürlich gibt es auch die Befehle zum Rotieren und zum Schieben,
das heißt ROLL LEFT und RIDE, also ROLL und ROAR.
Das steht für RIDE, das steht für LEFT, dieses L, und zwar hier die
Notation angegeben, beziehungsweise die SHIFT Befehle, und zwar einmal
das logische Schieben und das arithmetische Schieben.
Das heißt SHIFT LEFT logisch, und zwar um den Wert, den Registerinhalt
R S um den Wert I M und SHIFT LEFT logisch um einen variablen Wert,
was ja in dem Register R D steht.
Also einmal um einen konstanten Wert, was ja hier durch die Konstante
angegeben ist, und einmal durch einen variablen Wert, was ja im
Register R D steht.
Beim SHIFT RIDE arithmetisch und SHIFT LEFT natürlich arithmetisch,
beziehungsweise SHIFT RIDE logisch, variabel und so weiter und so
fort, gelten ja die entsprechenden Notationen hier.
Man muss natürlich darauf achten, bei dem arithmetischen Schieben,
dass man das Vorzeichen, das letzte Bit von dem Register nicht einfach
mit Nullen auffüllt, wie bei dem logischen Schieben, sondern je
nachdem, ob das eine positive Zahl oder eine negative Zahl ist, mit
Nullen oder mit Einsen, weil die Operanden oder die Werte halt in
zweier Komplement bereitstehen.
So, die nächste Klasse sind LADI-Befehle.
Das sind Befehle zum Laden von Konstanten in ein Register.
Das heißt, wie kann ich den Register mit einem bestimmten Wert
initialisieren.
Da gibt es halt hier LI, das Register, und den Wert hier,
beziehungsweise LUI.
Gehen wir später darauf ein.
Vergleichsbefehle, gibt es eine ganze Reihe von Vergleichsbefehlen,
die der MIPS Assembler bereitstellt.
Vergleichsbefehle bei dem MIPS Rechner sind eigentlich nur solche
Befehle realisiert, die auf Vergleich kleiner basieren.
Das heißt, es gibt nichts wie kleiner gleich, großer gleich und so
weiter und so fort.
Aber alle Varianten lassen sich aus dem Vergleich auf kleiner
herleiten.
So, hier sind einige Befehle zum Vergleichen von zwei Registern.
Das sieht sehr wild aus, aber die Namen der Befehle, die lassen sich
eigentlich sehr gut merken, oder kann man sehr gut merken.
SL, SLT, SEQ, SGE, SGT, SEL, SNE.
Also, hier werden zwei Register miteinander verglichen, und wie man
sieht, hier gibt es ein drittes Register im Spiel.
So, der Befehl SLT sagt set less than.
Das heißt, im Register RD wird eine 1 stehen, wenn RS kleiner als RT,
ansonsten eine 0.
Hier set equal, also wenn die beiden Register gleich sind, dann wird
der hier gesetzt, das heißt auf 1.
Wenn die beiden hier, wenn einer größer als der andere, dann wird der
hier gesetzt, also set greater than, äh, greater equal,
Entschuldigung, großer gleich.
Hier genau größer, hier kleiner gleich und hier ungleich.
Also set not equal.
Es gibt natürlich eine ganze Menge von Befehlen, die halt den Inhalt
eines Registers mit 0 vergleichen.
So, das heißt, der Befehl zum Beispiel SLT, set less than, ist sehr
interessant, um eine dieser Kontrollstrukturen, die wir kennengelernt
haben in der Vorlesung, zu realisieren, eine If-Else-Anweisung.
Das heißt, wenn S1 kleiner als S2 ist, dann soll das Register T0 mit
dem Wert, oder der Wert 1 in Register T0 geschrieben werden, ansonsten
soll im Register T0 der Wert 0 geschrieben werden.
Also, diese gesamte Kontrollstruktur hier, oder Abfrage, kann man
realisieren mithilfe von dem Befehl, set less than, wenn S1 kleiner
als S2 ist, dann steht hier eine 1, ansonsten steht hier eine 0.
Befehlsformat, wie man sieht hier, ist ähnlich mit dem Befehlsformat
für arithmetische Befehle, bis auf die Funktionalität hier.
Die Funktion war bei dem Add-Befehl zum Beispiel 5 oder 6, hier ist es
2a.
So, Kontrollflussbefehle, das heißt Befehle zum Realisieren von
bedingten Sprüngen, beziehungsweise unbedingten Sprüngen.
Hierzu gibt es einmal die Variante Jump zu einer symbolischen Marquee
Target, beziehungsweise die Branch-Befehle auch zu einer Marquee
Label.
Es gibt ja die Variante, unbedingtes Verzweigen zu einer Adresse und
Sicherung der nachfolgenden Adresse im Register RA, das realisiert man
mit dem Befehl Jump and Link Target.
Das heißt, ich springe zu der Adresse Target und mache dort weiter mit
dem nächsten Befehl.
Die Rücksprung-Adresse zum Hauptprogramm ist aber im Register RA
gesichert.
Dann Verzweigungsbefehle, wenn Bedienungsflex im Co-Prozessor wahr
oder falsch sind.
Und Co-Prozessor bezieht sich hier auf sowohl das arithmetische Co
-Prozessor als auch auf den Co-Prozessor für Ausnahmen und
Unterbrechungen.
Das heißt, sowohl Co-Prozessor mit der Nummer 0 als auch Co-Prozessor
mit der Nummer 1.
Z ist hier die Nummer von dem Co-Prozessor und hier wird verzweigt zu
der Marquee Label hier, wenn jetzt ein Bedienungsflex im Co-Prozessor
0 z.B.
true ist.
Also Branch Co-Prozessor 0 true oder false.
Also wenn das Gesetz oder nicht Gesetz ist.
Verzweigungsbefehle, wenn ein Register größer, kleiner als ein
anderes.
Branch to Label, wenn der hier größer als der hier bzw.
der hier kleiner als der hier und die entsprechenden Varianten mit
unsigned.
Und so weiter und so fort.
Verzweigung, wenn zwei Register gleich bzw.
ungleich sind.
Gehe zur Marquee Label, wenn RS gleich RT bzw.
Branch equal und Branch if not equal RS und RT.
Verzweigen, wenn ein Register größer und kleiner 0.
Branch greater than 0 bzw.
less than 0.
Verzweigen, wenn ein Register gleich und gleich 0.
Branch equal 0 und Branch if not equal 0.
Das heißt, anhand von diesen Befehlen, wenn man die englischen
Bezeichnungen für die Befehle und die ersten Buchstaben nimmt, dann
kann man eigentlich jeden Befehl verstehen und wissen, was damit
gemeint ist.
So, hier sind die Befehlsformate für diese unterschiedlichen
Sprungbefehle.
Einmal die Jump-Variante und einmal die Branch-Variante, wie man das
hier sieht.
Das heißt, bei der Branch-Variante hat man ein Offset von 16 Bit, ein
vorzeichenbehaftetes Offset, wo man eine Distanz zum aktuellen
Programmzähler addieren kann.
Das heißt, mit diesem Offset ist ein Sprung um 2 hoch 15 minus 1
Befehle vorwärts und 2 hoch 15 Befehle rückwärts vom aktuellen
Befehlszähler im Programm möglich.
Bei den Jump-Befehlen ist hier ein 32-Bit-Adressfeld angegeben.
Das heißt, hier ist direkt eine absolute Sprungadresse angehbar.
Gut, nun haben wir ja in der Vorlesung eine ganze Reihe von
Kontrollstrukturen kennengelernt.
Wir wollen ja sehen, wie das Ganze hier mit Hilfe von
Assemblerbefehlen realisiert ist oder realisiert werden kann.
Zum Beispiel diese ganzen Vergleichsbefehle und Verzweigungsbefehle,
die können wir hier ausnutzen, um derartige einstufige oder
mehrstufige Verzweigungen zu realisieren.
Das heißt hier, einstufige Verzweigung wird durch einen bedingten
Sprung realisiert.
Wenn wir hier IF Register S1 gleich 0, dann sollen bestimmte IF
-Aktionen oder IF-Anweisungen ausgeführt werden.
Ansonsten muss hier irgendwas anderes gemacht werden.
Wie macht man das hier in Assembler?
Ja, Vergleich auf 0, also Registerinhalt wird mit 0 verglichen.
Das heißt Branch equals 0, S1, wenn S1 gleich 0 ist, geht man zu Marke
1.
In Marke 1 sind eigentlich die IF-Aktionen oder die IF-Anweisungen.
Ansonsten geht man zum nächsten Teil, das ist der Jump to Marke 2.
Das ist natürlich hier notwendig an der Stelle, weil der Assembler das
Programm zeilenweise bearbeiten muss.
Das heißt, wenn wir hier dieses Jump nicht hätten und direkt hier
Marke 1, also ohne irgendwas dazwischen, dann würde jetzt Marke 1
anschließend ausgeführt oder ausgewertet von dem Assembler.
Das heißt hier, den nächsten Teil müssen wir auch mit dieser
zusätzlichen Marke 2 hier realisieren.
Zweistufige Realisierungen, das sind IF-ELS-Anweisungen.
Das heißt, wenn Register S1 0 ist, dann soll das Blaue hier gemacht
werden.
Ansonsten soll das Grüne Zeug gemacht werden und dann weiter mit dem
Next.
Und hier vergleicht man natürlich hier das Register S1 auf 0.
Also der Wert wird hier mit 0 verglichen, wenn das gleich 0 ist.
Das heißt, wenn das 0 ist, dann muss man zu Marke 1 springen, wo die
IF -Anweisungen stehen.
Ansonsten muss man ja die ELS-Anweisungen erledigen.
Das heißt, wenn das Register S1 ungleich 0 ist und anschließend hier
mit Marke 2 weitermachen, die halt den Next-Teil vom Programm
darstellt.
Bedingte Verzweigungen, das sind zum Beispiel hier Sachen wie, wenn i
gleich j ist, dann sollen wir h ist gleich i plus j.
Das heißt, wenn i gleich j ist, dann sollen wir die beiden addieren
und in h schreiben.
Das kann man natürlich hier machen, einfach realisieren, entweder mit
branch not equal oder branch equal.
Das heißt, hier habe ich das mit ungleich.
Das heißt, nehmen wir mal an, dass hier in S0 i steht, in S1 j.
Wenn sie ungleich sind, dann gehen wir zu Marke label.
Ansonsten werden die beiden hier addiert und das Ergebnis in S3
geschrieben, also das heißt in h.
Unbedingte Verzweigungen, also jump to label.
Beispiel, wenn i ungleich j ist, dann soll i plus j berechnet werden,
ansonsten i minus j.
Das heißt hier, das sind die beiden Variablen i und j, die sind in den
Registern S4 und S5.
Und wenn sie gleich sind, dann gehen wir zu 1 und subtrahieren.
Also hier, wenn sie ungleich sind, addieren wir, wenn sie gleich sind,
subtrahieren wir.
Das heißt, wenn sie gleich sind, dann wird hier subtrahiert, ansonsten
wird addiert und weiterhalt hier mit Marke 2, also mit jump to label 2
gemacht.
So, die nächste Klasse von Befehlen sind die Ladebefehle.
Das sind hier ganz spezielle Befehle.
Wie wir wissen, die MIPS-Architektur ist eine Lade-Speicher
-Architektur.
Das heißt, alle Operationen erfolgen auf Register.
Und wenn man etwas aus dem Speicher laden bzw.
im Speicher speichern will, dann muss man das mit Hilfe von speziellen
Registern, speziellen Befehlen zunächst mal im Register laden und
anschließend die Operationen ausführen.
Zunächst mal hier eine Pseudo-Instruktion zum Laden einer Adresse in
ein Register.
Das heißt, in das Register hier wird eine bestimmte Adresse geladen.
Ansonsten gibt es hier die unterschiedlichen Befehle zum Laden von
Daten in unterschiedlicher Breite.
Also zum Laden von Bytes, von Wörtern und von Doppelwörtern.
Und zwar einmal die Varianten mit LB, also Load Byte, von der Adresse
hier, die angegebenen Adressen in das Register RT.
Und einmal hier Speicher ein Byte von dem Register RT an der Adresse,
die hier angegeben ist.
Und dann gibt es hier die Variante, wo dieses Laden von einem
einzelnen Byte fortzeichenlos praktisch gemacht wird.
Das heißt hier, man lädt ein Byte aus der Adresse in das Register.
Bei LB werden die höchstwertigen Bits in dem Registerfortzeichen
richtig erweitert.
Das heißt, wenn das höchstwertige Bit in diesem Byte eine 1 ist, dann
werden die höchstwertigen Bits von dem Register alle mit 1 gefüllt.
Ansonsten bei LB U ist das nicht der Fall.
Die restlichen Bits oder die höchstwertigen Bits, die haben immer den
Wert 0.
Das gleiche für Laden von Halbwörtern bzw.
Laden von Wörtern und Speichern von Wörtern.
Das heißt, lade hier das Wort an der Adresse in das Register RT und
speichere das Wort in dem Register RT an der angegebenen Adresse.
Laden und Speichern von Co-Prozessorregistern.
Lade das Wort Co-Prozessor 1 z.B.
bzw.
speichere das Wort von Co-Prozessor 1 z.B.
bei der FBU.
Es gibt natürlich hier Befehle zum Laden und Speichern von Daten an
nicht ausgerichteten Adressen.
Das heißt, hier wird das linke Wort von der Adresse hier in das
Register RT geladen bzw.
das rechte Wort beginnend von der Adresse hier in das Register RT
geladen.
Das gleiche natürlich für Halbwörter und für Wörter.
Hier die unterschiedlichen Befehlsformate für Lade- und
Speicherbefehle.
Also Lade das 32-Bit-Wort an der Adresse in das Register RT und
speichere das 32-Bit-Wort im Register RT an der Adresse.
Das heißt, die Richtungen sind ja umgekehrt.
Wie man sieht hier, beide haben ein Befehlsformat.
Ob-Code ist unterschiedlich, ein Offset aus 16-Bit.
Das heißt,
was heißt das?
Ist das irgendwie ein Problem oder hat niemand irgendwas mitbekommen?
Heißt das, wir können jetzt hier Adressen von 16-Bit-Breiten eingeben
oder wie ist das zu verstehen?
Ja.
Und was machen wir mit dem Rest der Adresse?
Naja, okay.
Denkt mal darüber nach, was das sein kann und was diese Register hier
für eine Bedeutung haben.
Das ist ja dieses RT-Register, das steht hier und der Rest taucht hier
nirgendwo auf.
Ist vielleicht mal eine sehr interessante Hausaufgabe für euch.
Später.
So, Lade- und Speicherebefehle.
Hier zum Beispiel betrachten wir mal ein Array in C.
Das heißt, wir wollen hier zum Element A mit dem Index 8 einen Wert H
hinzuaddieren und das Ergebnis wiederum an der gleichen Stelle in
diesem Array schreiben, was man in C durchaus so realisieren kann.
In MIPS-Code ist es so, dass man zunächst mal hier natürlich diese
Operanden, das heißt hier dieses Array-Element, man kann keine
Operation auf dieses Array-Element im Speicher ausführen.
Das heißt, man muss dieses Array-Element zunächst mal in ein Register
laden und anschließend zu diesem Array-Element den Inhalt von H, also
von S2 addieren.
Also S2, das ist ja H.
Und das ist ja das Element, wenn wir das Ganze angefangen bei der
Adresse 0 betrachten, dann die Basisadresse von diesem Array steht ja
beispielsweise im Register S3.
Dann hat das Element hier die Adresse 32 plus S3.
Das heißt, das 32-Bit-Breite-Datum von der Adresse 32 plus S3 wird in
T0 geladen.
Somit haben wir dieses Element hier in ein Register der CPU geladen.
Zu diesem Element hier, das ist also A8 alt, das hier, zu diesem
Element addieren wir ja S2.
In S2 soll ja H zum Beispiel stehen und das Ergebnis schreiben wir in
T0.
Und dann wollen wir ja die Addition oder die Summe wiederum an der
gleichen Stelle im Speicher schreiben.
Das heißt, speichere den Wert, was in T0 steht, an der Adresse, die
sich aus der noch alten Basisadresse des Arrays besteht, also S3, die
gleiche plus 32.
Ich habe es vorher vielleicht ganz schnell erklärt, aber den
Unterschied hier zwischen LB und LBU sieht man hier am besten anhand
von diesem Beispiel.
Nehmen wir mal an, wir hätten hier ein Wort an der Adresse result und
wir greifen hier im Hauptprogramm einmal mit LoadByte unsigned von
dieser Adresse hier in das Register A0 beziehungsweise LoadByte
signed, also mit Vorzeichen, Vorzeichen richtig erweitert, von der
Adresse result in das Register A0.
Dann würde eigentlich hier in dem Register A0 bei LBU eine 8,9 unter
der Voraussetzung, dass wir hier BigEndianByteOrder haben, das heißt,
das höchstwertige Byte an niedrigster Adresse, das heißt, die 8,9 wird
hier geladen und bei LBU werden jetzt die restlichen Bits in dem
Register A0, was ja 32 Bit breit ist, das Register, mit Nullen
erweitert.
LBU werden die restlichen Bits, Vorzeichen richtig erweitert.
Das heißt, die 8,9, das ist ja nichts anderes als 1,0,0, 1,0,0,1.
Das heißt, die 8 hat als letztes Bit hier eine 1, deshalb werden die
restlichen Bits im Register hier im Fall von LB mit Einsen gefüllt,
sodass man hier FFF bekommt.
Und im anderen Fall werden diese restlichen Bits hier mit Nullen
gefüllt.
Im Unterschied dazu, wenn wir hier den Wert hier hätten, an der
Adresse Result, also nicht 8,9 abcdef, sondern 7,9, dann würden wir
natürlich hier bei 7, 7 hätte ja die Dualdarstellung, wie vielleicht
einige von euch wissen,
0,1,1,0, das heißt, das höchstwertige Bit ist ja hier eine 0 und
deshalb würde hier in beiden Fällen hier in dem Register A0 der Wert
79 stehen und nicht wie im ersten Fall hier diese Erweiterung mit FFF.
So, das ist natürlich jetzt hier das Speichermodell nochmal, das
Speichermodell vorausgesetzt hier Big-Endian-Byte-Ordnung, das heißt,
wenn wir hier etwas aus der Adresse Result laden, dann wird praktisch
hier das höchstwertige Byte, steht ja im Speicher in der niedrigsten
Adresse, das höchstwertige Byte wird jetzt in A0 stehen, an der
Adresse Result Plus 1 wird ja die nächsten Stellen sein und so weiter
und so fort.
Und wie bereits erwähnt, in SPIM wird die Konvention des
unterliegenden oder zugrundeliegenden Rechners verwendet, das heißt,
wenn der Rechner Big-Endian-Byte-Order hat, dann wird Big-Endian
verwendet.
Bei den meisten von euch, die Windows-Rechner benutzen mit Intel
-Prozessoren, wird das ein Little-Endian-Byte-Order sein.
So, wir haben ja diese ganzen Ladebefehle betrachtet und wir haben
gesehen, dass bei den Ladebefehlen von Operanden in Registern, dass
wir auch ein 16-Bit-Offset haben.
Das bedeutet eigentlich, man könnte nur 16-Bit-Operanden in Register
direkt laden.
Beispiel, wenn wir jetzt hier ein 32-Bit-Register, also das
Befehlsformat erlaubt nur die Angabe von 16-Bit-Breite-Operanden, die
man direkt in Register laden kann.
Und wenn die Aufgabe besteht, dass man hier jetzt ein 32-Bit-Operand
in das Register T0 laden will, dann gibt es die Möglichkeit, dass man
hier mithilfe von Load Upper Immediate zunächst mal das höchstwertige
Teil von diesem Operanden in das Register T0 zu laden.
Das heißt, die höchstwertigen 16-Bit hier werden in T0 geladen.
Und anschließend hat man in T0 einmal diese 16-Bits hier und die
niedrigwertigen 16-Bits, die werden mit 0 ausgefüllt.
Anschließend wird hier ein ORI, also logisches OR, mit einem
unmittelbaren Wert und der unmittelbare Wert hier ist auch 16-Bit
-breit und ist nichts anderes als die niedrigwertigen 16-Bit von dem
32 -Bit-Operanden.
Das heißt, hier wird ein OR zwischen T0 und diesem Wert hier
ausgeführt, das ist ja T0 von oben und das ist der Wert hier erweitert
auf 32-Bit, also lautet es so.
Und wenn man das gesamte Ergebnis hier betrachtet, dann bekommt man
natürlich hier das Gleiche und hier das Gleiche und somit hat man
praktisch diesen 32-Bit-Operanden in das Register T0 geladen.
Es gibt ein Register, das wir ja kennengelernt haben, das ist der
Globale Zeiger, also das Register GP.
Und wie wir gesehen haben, wir können ja hier ein Wort aus einer
Adresse in das Register RT laden.
Das Ganze funktioniert ja so, da wir in diesem Befehlsformat von den
Loadbefehlen nur 16-Bit-breite Offsets eingeben können, funktioniert
jetzt so, indem man hier zunächst mal eine Basisadresse halt in das
Register AT lädt und anschließend ein Offset hier zu dieser
Basisadresse angibt und anschließend das Wort von dieser gesamten
Adresse in das Register RT lädt.
Eine bessere Lösungsmöglichkeit ist gegeben mit Hilfe von diesem
globalen Pointer oder globalen Zeiger, der immer einen Wert in der
Mitte vom Datensegment im Speicher enthält, das heißt 1000 und 8000
und erlaubt mit Hilfe von dieser Adressierungsmöglichkeit durch diesen
Offset hier einen schnellen Zugriff auf die Daten zwischen den
Adressen, die hier eingegeben sind.
Beispiel, vielleicht vereinfacht das Ganze.
Wenn wir hier das Wort von der Adresse hier in das Register V0 laden
wollen, dann würden wir jetzt folgendes machen.
Wir nehmen unser LUI, Load Upper Immediate und wir laden die
höchstwertigen 16-Bit hier in das Register AT.
Anschließend laden wir das Wort von der Adresse 8000 plus AT, also
plus 1000 hier, wie wir das vorher gesehen haben, in das Register V0.
Mit Hilfe von diesem Register GB, der ja immer die Adresse hier 1000,
8000 enthält, können wir eigentlich einen schnelleren Zugriff auf
diese Daten hier erreichen, indem wir immer relativ zu diesem globalen
Zeiger adressieren.
Das heißt, wir wissen, dass wir hier die Adresse haben und wir müssen
nur hier eigentlich den Offset zu dieser Adresse eingeben, sodass wir
diesen schnellen Zugriff auf Daten in diesem Adressraum erreichen
können.
So, vielleicht ganz zuletzt die Transportbefehle zum Verschieben von
Registerinhalten von einem Register zum anderen.
Einmal Move und einmal von den Low- und High-Registern, haben wir ja
schon mal erklärt, beziehungsweise von den Registern der Co
-Prozessoren in allgemeine Register der CPU.
Und die Befehle für Fließkomma-Arithmetik, das sind ja die gleichen
Befehle wie für Integer-Arithmetik, mit dem Unterschied, dass man hier
abs.d, beziehungsweise hier add.d, sub.d und für die Vergleiche
natürlich gilt ja hier das Gleiche.
Es gibt noch eine, oder die letzten Befehle, die wir heute kurz mal
behandeln wollen, sind Return from Exception, das heißt ein Befehl,
mit dem der Status des Statusregister wiederhergestellt wird nach
einer Unterbrechung, beziehungsweise der Syscall, den wir
kennengelernt haben zur Eingabe und Ausgabe, die Software-Breaks für
Debuggen, beziehungsweise Nop-Operationen oder Dummy-Operationen, die
nichts machen.
So, ich bedanke mich für heute für die Aufmerksamkeit, wünsche euch
noch einen schönen Tag und nochmal nächste Woche findet am Dienstag
wiederum eine Vorlesung, am Mittwoch eine Übung.