Johnny-Simulator/Ein einfaches Modell
Wie schon im Vorwort gesagt, sieht man einem modernen Rechner seine Funktion nicht an. Um besser zu verstehen, wie Rechner funktionieren, greifen wir deshalb auf eine Simulation zurück. Der Simulator „Johnny“ ist nach dem genialen Entwickler der heutigen Rechnerstruktur, John von Neumann (1903-1957) benannt: Privat war von Neumann für seine Partys berühmt und sein Spitzname war wohl auch deshalb „Good Time Johnny“, worauf der Name des Simulators anspielt.
Der Johnny-Simulator konzentriert sich auf CPU mit Rechenwerk und Steuerwerk und den Arbeitsspeicher. Die Benutzungsoberfläche entspricht dabei etwa dem obigen Blockdiagramm:
Beim Start ist die Struktur des Steuerwerks (Control Unit) noch verdeckt, um die Dinge zunächst einfach zu halten. Der Inhalt des RAM kann im Simulator gelöscht, aus einer Datei geladen bzw. in eine Datei geschrieben werden.
Vieles ist beim Johnny-Simulator einfacher als bei einem richtigen Rechner. So rechnen praktisch alle Rechner im Binärsystem, während der Simulator in unserem gewohnten Dezimalsystem rechnet. Auch die Funktion des Bus-Systems ist gegenüber echten Rechnern vereinfacht: Normalerweise können sich Bus-Syteme keine Daten merken, sondern sind lediglich Verbindungswege.
All diese Vereinfachungen sollen den Benutzern des Simulators die Bedienung vereinfachen, die Grundprinzipien des Rechners bleiben jedoch erhalten.
Der Simulator kann vom Bildungsserver des Landes Rheinland-Pfalz, vom Server des Heise-Verlags oder direkt vom SourceForge-Server ohne Kosten heruntergeladen werden. Das Programm ist ein OpenSource-Projekt und kann unter der Programmierumgebung Lazarus selbst weiterentwickelt werden.
Zahlen im Arbeitsspeicher (RAM)
Betrachten wir zunächst den Arbeitsspeicher. Dieser ist im Simulator als eine Tabelle dargestellt, was seiner tatsächlichen Struktur recht nahe kommt. Jede Zeile der Tabelle hat eine Nummer, die so genannte Adresse (Spalte „Adr“). Statt von Zeilen spricht man auch von Speicherstellen. In jeder Speicherstelle kann eine Zahl zwischen 0 und 19999 abgelegt werden. Aus technischen Gründen sind dabei die 10000er- und die 1000er-Stelle in einer gesonderten Spalte „Hi“, die 100er-, die 10er- und die 1er-Stelle „Lo“ abgelegt. Die hinteren beiden Spalten „Asm“ und „Op“ werden später eine Rolle spielen, sie sind aber nur Kommentare. Der eigentliche RAM-Inhalt sind die Spalten „Hi“ und „Lo“.
Die Inhalte der einzelnen Speicherstellen kann man (im Simulator) entweder durch Anklicken der entsprechenden Zeile, oder aber über das Bus-System des Simulators ändern.
Zunächst muss die richtige Zeile, also Adresse gewählt werden. Dies tut man, indem man die Adresse der betreffenden Zeile am so genannten Adressbus oben links im Textfeld eingibt, mit dem nebenstehenden Knopf diese Zahl auf den Adressbus legt. Danach legt man analog die Zahl, die in den RAM geschrieben werden soll, auf den Datenbus und drückt auf den Knopf „db->ram“.
Rechnen mit Johnny
Betrachten wir nun das Rechenwerk (Arithmetic Logic Unit). Diese kann mit Daten, die auf dem Datenbus liegen, rechnen. Kernstück des Rechenwerks ist der so genannte Akkumulator (lat.: „Sammler“). In ihm sammeln sich die Rechenergebnisse an. Drückt man z.B. auf die Taste plus, so wird der Wert auf dem Datenbus zum Wert des Akkumulators addiert und das Ergebnis wieder im Akkumulator gespeichert. Johnny beherrscht von sich aus nur die beiden Rechenoperationen plus und minus, alles andere muss man ihm sozusagen beibringen (mehr dazu später). Der Wert des Akkumulators kann auch wieder auf den Datenbus kopiert werden (mit acc->db). Daneben gibt es noch ein paar andere Möglichkeiten, den Akkumulator zu manipulieren, die Sie aber auch leicht selbst herausfinden können. Wichtig für später ist auch die Kontrolllampe links neben dem Akkumulator. Diese zeigt an, ob der Inhalt des Akkumulators gerade den Wert 0 hat.
Herunterladen und Inbetriebnahme von JOHNNY Falls der Simulator "JOHNNY" auf Ihrem Rechner noch nicht installiert ist, laden Sie ihn von einer der oben genannten Download-Seiten herunter. Unter Windows-Betriebssystemen muss "JOHNNY" muss nicht installiert werden, es muss lediglich die heruntergeladene ZIP-Datei in einen beliebigen Ordner ausgepackt und die Datei "johnny.exe" ausgeführt werden.
Anmerkung für Linux-Benutzer:
Wenn Sie "Johnny" unter einem Linux-Betriebssystem ohne Emulation betreiben möchten, kann man die Quelldateien herunterladen und unter Verwendung der Entwicklungsumgebung Lazarus kompilieren.
Mikrobefehle im Handbetrieb
Bis hierher hat Ihnen das Skript nur Dinge beschrieben. Vieles jedoch können Sie auch selbst herausfinden.
a) Die Bedeutung eniger Mikrobefehle
Die Knöpfe des Simulators entsprechen Aktionen, die man als „Mikrobefehle“ bezeichnet; sie sind die kleinsten Befehlseinheiten, mit denen der Prozessor gesteuert werden kann.Probieren Sie die folgenden Mikrobefehle aus und ordnen Sie die Lösungen zu:
db->ram | Businhalt auf die aktive Adresse des RAM legen |
ram->db | RAM-Inhalt der aktiven Adresse auf den Bus legen |
acc:=0 | Akkumulator auf 0 setzen |
acc++ | Inhalt des Akkumulators um 1 erhöhen |
acc-- | Inhalt des Akkumulators um 1 erniedrigen |
db->acc | Inhalt des Datenbusses in den Akkumulator kopieren |
plus | Inhalt des Datenbusses zum Akkumulator hinzuaddieren |
minus | Inhalt des Datenbusses vom Akkumulator subtrahieren |
acc->db | Inhalt des Akkumulators auf den Datenbus kopieren |
b) Rechnen mit Mikrobefehlen
Angenommen, die Adresse „42“ würde bereits auf dem Adressbus stehen. Welche Mikrobefehle muss man nacheinander ausführen, um den Wert in der Speicherstelle mit der Adresse 42 zu verdoppeln (unabhängig davon, welche Zahl vorher gerade im Akku steht).
Zum Testen mit dem Simulator sollte man vorher die Speicherstelle 42 mit einem Wert >0 belegen, denn sonst kann man die Verdopplung natürlich nicht gut erkennen.
ram->db
db->acc
plus
acc->db
db->ram
Makrobefehle als praktische Abkürzung
Wiederkehrende Befehlsfolgen
Mikro- und Makrobefehle
Beschreiben Sie in der mittleren Spalte, mit welcher Folge von Aktionen man mit dem Simulator die Zahlen in den Speicherstellen 10,11 und 12 addieren und dann die Zahl in Speicherstelle 14 davon subtrahieren kann. Das Ergebnis soll in der Speicherstelle mit der Adresse 15 abgespeichert werden. (Bei den letzten drei Zeilen kann auch eine andere Lösung als die "Musterlösung" richtig sein, aber diese Lernumgebung ist nicht ganz so flexibel ;-).
Neben den schon bekannten Mikrobefehlen brauchen wir zusätzlich noch eine Anweisung wie „Lege Adressse ... auf den Adressbus“ wobei „...“ für die entsprechende Adresse, also die Nummer der Zeile im RAM steht.
1 | Lege Adresse 10 auf den Adressbus | TAKE 10 |
2 | ram->db | |
3 | db->acc |
4 | Lege Adresse 11 auf den Adressbus | ADD 11 |
5 | ram->db | |
6 | plus |
7 | Lege Adresse 12 auf den Adressbus | ADD 12 |
8 | ram->db | |
9 | plus |
10 | Lege Adresse 14 auf den Adressbus | SUB 14 |
11 | ram->db | |
12 | minus |
13 | Lege Adresse 15 auf den Adressbus | SAVE 15 |
14 | acc->db | |
15 | db->ram |
Man nennt ein Programm bestehend aus solchen Abkürzungen auch Maschinenprogramm, die einzelnen Befehle darin Makrobefehle, da sie mehrere Mikrobefehle zu größeren Einheiten zusammenfassen.
Der Speicher als Programmspeicher
Am Arbeitsauftrag 5 merkt man: Maschinenprograme aus Makrobefehlen lassen sich leichter verstehen und vor allem viel kompakter hinschreiben als eine Folge von Mikrobefehlen. Die Frage ist nur: wo genau hinschreiben. In den ersten Rechnern wie dem Zuse Z3 waren die Programme z.B. auf Lochstreifen gespeichert. John von Neumann hatte die Idee, nicht nur Zahlen sondern auch Programme in ein und demselben Arbeitsspeicher abzulegen. Man spricht in diesem Zusammenhang von der von-Neumann-Architektur.
Wie aber lassen sich Makrobefehle im Speicher (RAM) ablegen? Wir sehen, dass der RAM nur Zahlen – in unserem Simulator zwischen 0 und 19999 – ablegen kann.
Wie also kann man einen Befehl wie ADD 12 im RAM speichern?
Zunächst überlegt man sich hierfür, dass dieser Befehl eigentlich zweigeteilt ist, einmal in die eigentliche Operation „ADD“ und dann in die Adresse, auf die sich der Befehl bezieht. Der Speicher unseres Simulationsrechners enthält 1000 Speicherstellen mit Adressen zwischen 0 und 999. Um die Adressen darzustellen brauchen wir also die 1er-, die 10er- und die 100er-Stelle einer Zahl. Die verbliebenen zwei Stellen, die 1000er- und die 10000er-Stelle können wir verwenden, um die Operation selbst zu beschreiben, nicht mit einer Zeichenfolge wie „Add“ sondern mit einer Zahl etwa 20000.
Damit wird aus dem Befehl „ADD 12“ nun einfach die Zahl „20012“. Damit man sich als Benutzer des Simulators die Zahlen für die einzelnen Operationen nicht merken muss, kann man bei der Eingabe von Werten in den RAM ein Auswahlmenü anklicken, dass direkt die einzelnen Operationen mit ihren entsprechenden Zahlen zur Auswahl stellt.
Auf diese Weise kann man ohne Auswendiglernen der entsprechenden Zahlen (der so genannten Operation Codes, kurz Opcodes genannt) ein Maschinenprogramm eingeben. Neben dem eigentlichen Speicherinhalt, der – wie wir wissen – nur aus den Zahlen besteht, zeigt der Simulator als Kommentar die zugehörigen Makrobefehle an.
Erste Maschinenprogramme
000: | TAKE | 010 |
001: | ADD | 011 |
002: | ADD | 012 |
003: | SUB | 014 |
004: | SAVE | 015 |
Testen Sie das Programm, in dem sie unter den verschiedenen Adressen 10,11,12 und 14 verschiedene Zahlen vor dem Programmlauf speichern.
b) Weitere Befehle
Neben den Befehlen TAKE, ADD, SUB und SAVE gibt es noch einige andere. Finden Sie anhand des folgenden Programms heraus, was die darin enthaltenen Befehle bewirken und kreuzen Sie die richtige Beschreibung der Funktion an.
000: | INC | 010 |
001: | INC | 010 |
002: | DEC | 010 |
003: | INC | 010 |
004: | INC | 010 |
005: | NULL | 010 |
Sprünge und Bedingungen
Bisher sind unsere Programme immer von einem Befehl zum nächsten gegangen. Für kompliziertere Aufgaben reicht das jedoch nicht. Wie Sie wahrscheinlich aus anderen Programmiersprachen wissen, muss es auch Verzweigungen (if ... then ... else ...) oder Schleifen (etwa while ... do ...) geben können.
a) Kreuz und quer durch's Programm
Testen Sie das folgende Programm und finden Sie heraus, was die Befehle JMP und TST bewirken. Setzen Sie vor Ausführung des Programms die Speicherstelle Nr. 10 auf den Wert 5 und die Speicherstelle Nr. 11 auf den Wert 42.
000: | INC | 010 |
001: | DEC | 010 |
002: | TST | 010 |
003: | JMP | 001 |
004: | TAKE | 011 |
005: | SAVE | 012 |
Beschreiben Sie die Funktion der beiden Befehle JMP und TST in kurzer Form:
b) Malnehmen - eigentlich gar nicht eingebaut
Nun wird es komplizierter: Konstruieren Sie ein Maschinenprogramm, das die Zahlen in den Speicherstellen 10 und 11 miteinander multipliziert und das Ergebnis in der Speicherstelle 12 abspeichert.
Tipp: Das Programm soll folgendes tun:
- Die Speicherstelle 12 zunächst auf 0 setzen.
- Den Wert der Speicherstelle 12 in den Akku holen, den Wert der Speicherstelle 10 addieren das Ergebnis und wieder in Speicherzelle 12 abspeichern.
- Nach einer solchen Addition den Wert der Speicherstelle 11 um 1 erniedrigen.
- Das Verfahren ab dem 2. Punkt so lange wiederholen Sie, bis die Speicherstelle 11 den Wert 0 hat.
Testen Sie Ihr Programm, ob es auch funktioniert. Hierfür sollten die Speicherstellen 10 und 11 auf kleine, von Null verschiedene Werte gesetzt werden, also z.B. 5 und 3.
000 : NULL 012 001 : TAKE 012 002 : ADD 010 003 : SAVE 012 004 : DEC 011 005 : TST 011 006 : JMP 001 007 : HLT 000
(Der Befehl HLT 000 stoppt den Programmablauf im Simulator ab).
c) Dividieren - einfache Version
Schreiben Sie ein Programm, das eine Zahl durch eine andere Zahl teilt. Betrachten Sie dabei zunächst nur den Fall, dass die größere Zahl in Speicherzelle 10 tatsächlich durch die kleinere in Speicherzelle 11 ganzzahlig teilbar ist.
d) Größer, kleiner oder gleich
Wir betrachten zwei Zahlen in den Speicherstellen 10 und 11. JOHNNY soll entscheiden, ob die Zahl in Speicherstelle 10 größer ist als die in Speicherstelle 11, gleich groß ist, oder aber kleiner. Je nachdem soll in Speicherstelle 12 nach der Untersuchung der Wert 1, 2 oder 3 stehen.
Tipp:
e) Dividieren - komplizierte Version
Schreiben Sie ein Programm, das eine Zahl durch eine andere Zahl teilt. Und zwar in der Art, wie wir vermutlich alle das in der Grundschule gelernt haben:
17:5 = 3 Rest 2.
Die Zahl in Speicherstelle 10 soll dividiert werden durch die Zahl in Speicherstelle 11. Der ganzzahlige Quotient soll danach in Speicherstelle 12, der Rest in Speicherstelle 13 gespeichert sein.
Tipp: Vielleicht hilft Ihnen das Ergebnis aus Aufgabe d)
Programme, die sich selbst ändern
Wie bereits gesagt, ist das besondere an von Neumanns Idee zur Speicherung von Programmen, dass Programmcode („Was soll gemacht werden?“) und Programmdaten („Womit soll etwas gemacht werden?“) in ein und dem selben Arbeitsspeicher gespeichert sind.
Mehr noch: Das Programm besteht selbst aus Daten, der einzige Unterschied ist, dass diese Daten, die einem Programm entsprechen, von der Maschine als Befehle interpretiert und ausgeführt werden.
Dies hat eine wichtige Konsequenz, die zunächst ein wenig abwegig klingt: Programme können sich selbst (oder andere Programme im Arbeitsspeicher) verändern, indem sie die entsprechenden Daten im Speicher manipulieren. Compiler z.B. lesen einen Text in einer höheren Programmiersprache und übersetzen ihn in ein Maschinenprogramm. So etwas geht in dieser Weise nur dank der von-Neumann-Architektur.
Einen Compiler zu bauen würde den Rahmen dieses Kurses sprengen, allerdings wollen wir uns ein Programm anschauen, das tatsächlich sich selbst, also seinen eigenen Programmcode ändert:
a) Mut zur Selbst-Veränderung
Untersuchen Sie das folgende Programm und beschreiben Sie seine Funktion:b) Ein weiteres Programm, das sich verändert
Modifizieren Sie das Programm so, dass es nicht ständig ein und die selbe Zahl schreibt, sondern fortlaufende Zahlen, also z.B. 42, 43, 44, 45 usw. Außerdem soll das Programm diese Zahlen nur in die ungeraden Speicherstellen ab 100 schreiben, also in die 101, die 103 usw.
000 : TAKE 010 001 : SAVE 101 002 : INC 001 003 : INC 001 004 : INC 010 005 : DEC 011 006 : TST 011 007 : JMP 000 --- 010 : 42 011 : 13