Browse Files

Aufbau der ULM

Die ULM besteht aus einem Speicher, einem Ein- und Ausgabegerät, die mit einem Bus an eine CPU angeschlossen ist. Der Aufbau im Groben kann wie folgt skizziert werden:

Speicher

Der Speicher besteht aus einer Anordnung (Array) von Speicherzellen, die beginnend bei 0, fortlaufend indiziert sind. Der Index einer Speicherzelle wird als deren Adresse bezeichnet. Beispielsweise kann man sich den Speicherbereich mit Adressen von 0 bis 16 wie folgt vorstellen (wobei die Adressen hier in der Hexadezimaldarstellung angegeben sind):

In jeder Speicherzelle kann ein Byte gespeichert werden, was bei der ULM (und heutzutage allen gängigen Computer Architekturen), als eine Datenmenge von 8 Bits festgelegt ist. Der Inhalt einer Zelle kann deshalb mit zwei Hexadezimalziffern dargestellt werden, wobei auf das Präfix “0x” verzichtet werden kann, da aus dem Kontext die Zahlendarstellung klar ist:

Unrealistisch bei der ULM ist die Größe des Speichers, der aus \(2^{64}\) Speicherzellen besteht. Dabei werden allerdings nur technische Details unter den Teppich gekehrt. In der Realität ist das die Größe des virtuellen Speichers bei einem 64-bit System, und vereinfacht ausgedrückt die Größe des Speichers aus Sicht der Programmierung.

Ein- und Ausgabegeräte

Das Eingabegerät kann man sich als eine Speicherzelle vorstellen deren Inhalt man nur auslesen kann (read only) und das Ausgabegerät als eine Speicherzelle in die man nur schreiben kann (write only). Gibt man auf der Tastatur ein Zeichen ein, dann kann anschließend aus dem Eingabegerät dessen ASCII Wert gelesen werden. Beispielsweise beim Zeichen 'A' der Wert 65 bzw. 0x41. Schreibt man ein Byte in das Ausgabegerät, dann erscheint das zugehörige Zeichen (bei 0x41 also ein 'A').

ALU und Statusregister

Die ALU (Arithmetical Logical Unit) der ULM besitzt 256 64-Bit-Register, die mit %0x00, %0x01, ..., %0xFF bezeichnet werden:

  • Die Register %0x01, ..., %0xFF sind Arbeitsregister (general purpose register).

  • Das Register %0x00 ist ein Spezialregister (special purpose register), das unveränderlich ein Null-Bitmuster enthält (Schreiben ins Register wird ignoriert, Lesen liefert immer das Null-Bitmuster).

Die Register der ALU sind mit einem Addierer/Subtrahierer verbunden, der nach jeder Rechenoperation die Flags ZF (Zero Flag), CF (Carry Flag), OF (Overflow Flag) und SF (Sign Flag) des Statusregisters setzt.

Bus

Mit dem Bus können Daten zwischen dem Speicher und der CPU übertragen werden:

  • Vom Speicher kann der Inhalt von 1, 2, 4 oder 8 Speicherzellen in ein Register geladen werden. Dies wird dann als Fetch-Operation bezeichnet. Da die Register der ALU 64 Bits (also 8 Bytes) speichern können muss das Bitmuster bei einer Fetch-Operation eventuell erweietert werden (dann wenn weniger als 8 Bytes geladen werden). Dafür stehen zwei Befehlsvarianten zur Verfügung, damit bei der Erweiterung der Wert einer Unsigned-Integer (hier wird mit Nullen erweitert) oder Signed-Integer (hier wird abhängig vom Vorzeichen mit Nullen oder Einsen erweitert) erhalten bleibt.

  • Umgekehrt können aus einem Register 1, 2, 4 oder 8 Bytes in den Speicher geschrieben werden. Dies wird als Store-Operation bezeichnet. Die Bytes, die aus dem Register kopiert werden sind dabei die Least-Significant-Bytes (LSB), also “die Bytes rechts”.

Da das Eingabe- und Ausgabegerät jeweils wie eine Speicherzelle betrachtet werden kann, handelt sind Ein- und Ausgabeoperationen eigentlich nur spezielle Fetch- und Storeoperationen.

Steuerwerk

Programme bestehen aus einer Folge von Bytes und werden vom Steuerwerk ausgeführt. Voraussetzung dafür ist allerdings, dass das Programm zuvor in den Speicher geladen wurde. Zuständig ist dafür eine Komponente der ULM, die in der Abbildung oben nicht dargestellt wurde, der Lader. Bei eurem eigenen Computer entspricht das dem BIOS oder der Firmware. Wird der Rechner gestartet wird von der Komponente von der Festplatte das Betriebssystem (das erste Programme das auch dem Rechner gestartet wird) geladen. Danach beginnt das Steuerwerk der CPU mit dessen Ausführung.

Bereits weiter oben wurde ein Bereich des Speichers gezeigt, der ein geladenes Programm enthält:

Um das Programm “zu sehen” muss man zunächst das Befehlsformat und (zumindest teilweise) den Befehlssatz kennen.

Befehlsformat

Befehle für die ULM bestehen aus 32 Bits, haben aber im Prinzip das gleiche Format wie die Befehle der Mini-ALU:

. . . Op X Y Z

Die Felder Op, X, Y, Y bestehen aus jeweils 8 Bits also einem Byte. Wie bei der Mini-ALU bezeichnet Op den Opcode, also den Befehlstyp und X, Y und Z werden als Operanden benutzt.

Im oben abgebildeten Speicher liegen 4 Befehle:

  • Bei Addresse 0x00 liegt der Befehl

    mit Op = 0x56, X = 0x00, Y = 0x05 und Z = 0x01. Im Befehlssatz wird der Befehl mit Opcode 0x56 wie folgt beschrieben:

    \[\bigl(u(\text{%00}) + u(\text{XY})\bigr) \bmod 2^{64} \to u(\text{%}Z)\]

    In diesem Fall bedeutet dies

    \[\bigl(u(\text{%00}) + u(\text{0x0005})\bigr) \bmod 2^{64} \to u(\text{%}0x01).\]

    Da %0x00 das spezielle Null-Register ist, gilt stets \(u(\text{%00})=0\). Folglich wird mit diesem Befehl einfach der Wert 5 in das Register %0x01 geschrieben.

  • Bei Addresse 0x04 liegt der Befehl

    mit Op = 0x56, X = 0x00, Y = 0x08 und Z = 0x02. Da der Befehl den gleichen Opcode besitzt wie der vorige Befehl leitet man leicht ab, dass damit der Wert 8 in das Register %0x02 geschrieben wird.

  • Bei Addresse 0x08 der Befehl

    . . . 30 01 02 03

    mit Op = 0x30, X = 0x01, Y = 0x02 und Z = 0x03. Im Befehlssatzt ist dieser mit

    \[\bigl(u(\text{%}Y) + u(\text{%}X)\bigr) \bmod 2^{64} \to u(\text{%}Z)\]

    festgelegt. In diesem Fall als die Operation

    \[\bigl(u(\text{%}0x02) +u(\text{%}0x01)\bigr) \bmod 2^{64} \to u(\text{%}0x03).\]

    Damit werden also die Register %0x01 und %0x02 addiert und das Ergbnis in %0x03 geschrieben. Zusätzlich werden noch vom Addierer die Statusflags gesetzt.

  • Bei Addresse 0x0C der Befehl

    mit Op = 0x01, X = 0x03, Y = 0x00 und Z = 0x00. Der Befehl mit Obcode 0x01 ist ein halt Befehl. Damit wird die Programmausführung beendet. Zusätzlich wird das LSB aus Register Z (hier Register `%0x03) als Exit-Code verwendet. Damit wird nachempfunden, dass in der Praxis jedes Program mit solch einem Rückgabewert siganlisieren kann, ob die Ausführung erfolgreich war oder nicht. Bei diesem beispiel werden wird sehen, dass der Exit-Code gerade das Ergebnis einer Rechnoperation ist. Wir ersparen uns damit zu Beginn die etwas kompiziertere Ausgaben von Werten auf dem Ausgabegerät (der Zahlenwert müsste dazu zuerst in eine Folge von Ziffern konvertiert werden).

    Etwas heimtückisch ist, dass der Rückgabe immer ein Wert aus dem Bereich von 0 bis 255 ist (da nur das LSB des Regsiters verwendet wird). Dies ist der Tatsache geschuldet, dass die ULM nachbildet was in der Praxis gängig ist. Aus technischer Sicht späche nichts dagegen, dass alle 64 Bit für den Exit-Code verwendet werden.

Nachdem geklärt wurde aus welchen Befehlen das (Mini-)Programm besteht, und was diese bei der Ausführung tun, muss noch geklärt werden in welcher Reichenfolge diese ausgeführt werden.

Sequentielle Programmausführung

Technisch wird bei der ULM die Ausführung eines Programmes mit Hilfe von zwei Registern im Steuerwerk realisiert:

  • Das Register %IP (für Instruction Pointer) dient als Befehlszeiger, d.h. der Inhalt ist die Adresse an dem der Befehl steht, der als nächstes ausgeführt wird. Da es \(2^{64}\) möglich Adressen gibt ist dies ein 64-Bit-Register. Nach dem Laden eines Programmes wird dieses Register mit Null initialisiert.

  • Das Register %IR (für Instruction Register) ist das Befehlsregister in den ein Befehl aus dem Speicher zunächst geladen wird, dann dekodiert und schliesslich ausgeführt wird. Da Befehle aus 32-Bit bestehen ist dies ein 32-Bit Register.

Die sequentielle Ausführung eines Programmes kann mit folgendem Flußdiagramm beschrieben werden, das einen sogenannten Von-Neumann-Zyklus beschreibt:

In einem Durchlauf dieses Zykluses wird genau ein Befehl (stufenweise) ausgeführt. Beendet wird ein Programm, wenn in der Ausführungsstufe (Stufe C) ein Halt-Befehl ausgeführt wird.

Allerdings kann die ULM auch “abstürzen”, wenn ein Befehl einen nicht unterstützen Opcode enthält. Beispielsweise ist 0x00 solch ein illegaler Opcode, weshalb dieses Program mit Hurra bei der Ausführung des vierten Befehls die arme ULM zum Absturz bringt:

Mit diesem Beispiel soll deutlcih werden, dass dem Computer keineswegs bewusst ist, ob der Speicherinhalt tatsächlich einen Befehl darstellt. Der Rechner führt Programme stupide nach obigem Muster aus, er denkt nicht mit (alles andere würde sich in der Praxis kaum effizient in Hardware umsetzen lassen). Beim Programmieren muss deshalb festgelegt werden, dass immer nur als Befehle vorgesehene Speicherinhalt von der ULM bei der Ausführung geladen werden.

Aufgabe für die Mini-UL

Testet beide Mini-Programme auf der Mini-ULM:

  • Das erste Programm besteht aus der Byte-Folge

    1
    56 00 05 01 56 00 08 02 30 01 02 03 01 03 00 00
    

    Ihr könnt diese direkt Byte für Byte in den Speicher schreiben (also die Aufgabe des BIOS übernehmen):

    Oder zunächt im Eingabefeld ganz unten auf der Seite eingeben (bzw. obigen Code mit Copy&Paste übernehmen).

    Mit dem Button Step könnt ihr das Programm Befehl für Befehl ausführen. Alternativ könnt ihr aber auch einen Befehl stufenweise ausführen lassen. Dann könnt ihr beobachten wie in Stufe A der Befehl geladen, in Stufe B dekodiert wird, etc.

    Nach dem vierten Befehl sollte oben dann der Exit-Code das Ergebnis der Addition anzeigen.

  • Das zweite Programm besteht aus der Byte-Folge

    1
    56 00 05 01 56 00 08 02 30 01 02 03 00 03 00 00
    

    Hier solltet ihr sehen, dass die ULM beim letzten “Befehl” abbricht.

Aufgabe, um zu Testen, ob ihr auf die richtige ULM zugreifen könnt

Die Mini-ULM wurde aus der Not heraus geboren und unterstüzt deshlab aktuell nur eine Teilmenge des Befehlssatzes. Die “richtige” ULM existiert auf den Uni-Rechnern an denen wir normalerweise gearbeitet hätten, der theon. Und hier wird es etwas kompliziert, deshalb testen und berichten ob es klappt oder melden, wenn es Probleme gibt.

Für Windows-Benutzer: Cygwin installieren und ssh verwende

Um sich auf der theon von einem Windows_rechner einloggen zu können, empfiehlt es sich Cygwin zu installieren. Wie das geht und was es dabei zu beachten gibt zeigt euch meine Kollegin Laura Burr:

Für MacOS-Benutzer

Bis auf einen X11-Server (der in dieser Session nicht benütigt wird) sollte eigentlich alles bereits installiert sein

Für Linux-Benutzer

Hier sollte eigentlich alles bereits installiert sein.

Einloggen auf theon und testen, ob alles klappt

Loggt euch mit ssh auf theon ein. Gebt dazu im Terminal

1
ssh login@theon.mathematik.uni-ulm.de

ein, wobei login mit dem Login des SLC-Accounts zu ersetzen ist. Mit einem Texteditor schreibt ihr dann das erste Programm in eine Datei prog01. Ihr könnt dazu einfach im Terminal

1
nano prog01

eintippen. Dann tippt ihr das Programm

1
56 00 05 01 56 00 08 02 30 01 02 03 01 03 00 00

ein. Mit Control-O kann die Datei gespeichert werden und mit Control-X der Editor verlassen werden. Anschliessend könnt ihr das Programm auf der ULM ausführen und euch den Exit-Code anzeigen lassen:

theon$ ulm prog01
theon$ echo $?
13
theon$ 

Mit ulm prog01 wird das Programm ohne graphischen Schnickschnack geladen und ausgeführt (im Normalfall will man ja nicht sehen wie das Programm ausgeführt wird, sondern was es tut). Da das Programm aber keine Ausgabe hat seht ihr zunächst gar nichts. Mit dem zweiten Befehl echo $? wird aber der Exit-Code des letzten Programmes angezeigt (das funktioniert bei Unix allgemein so). Und damit seht ihr, dass mit der ULM tatsächlich gerechnet wurde.

Auf unserem Server theon ist die richtige ULM als Kommandozeilen-Programm installiert. Auf dem Linux-Rechner heim