Assembler-Programmierung mit dem RISC Simulator

Assembler ist eine etwas besser leserliche Form der Maschinensprache (Sprache, die die CPU 'versteht'). Mit Assembler hast du bereits im Little Man Computer Bekanntschaft gemacht.

Befehle in Maschinensprache werden im Binärsystem ausgedrückt. Selbst für relativ kleine Zahlen resultiert dies daher in ziemlich langen Ausdrücken. Deshalb schreibt man diese oft ins Hexadezimalsystem um, was folgende Vorteile hat:

  • viel kompakter
  • einfache Umrechnung Binärsystem ↔ Hexadezimalsystem (einfacher als z.B. ins Dezimalsystem)
  • Die Umrechnung Dezimalsystem ↔ Hexadezimalsystem erfolgt wie die Umrechnung Dezimalsystem → Binärsystem mit dem Restwertverfahren
  • Der Online-Umrechner gibt's wie gewohnt hier: https://zahlensysteme-rechner.de

Das Hexadezimalsystem ist das Zahlensystem mit:

  • Basis $16$
  • Nennwerten $0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F$
  • Typischerweise wird Präfix $0x$ vor Hexadezimalzahl geschrieben

Beispiele:

  • Umrechnung Hexadeimalsystem → Dezimalsystem $0xB3 = 11 \cdot 16^1 + 3 \cdot 16^0 = 179$
  • Umrechnung Hexadeimalsystem → Binärsystem $0xB3 = 11 \cdot 16^1 + 3 \cdot 16^0 = 1011 0011$
  • Umrechnung Binärsystem → Hexadezimalsystem $1100 1011 = 1100 \cdot 16^1 + 1011 \cdot 16^0 = (8+4) \cdot 16^1 + (8+2+1) \cdot 16^0 = C \cdot 16^1 + B \cdot 16^0 = 0xCB$
Aufgaben:

Stelle folgende Zahlen im Binärsystem dar und wandle die ins Dezimalsystem um:

  • 0xFF
  • 0x100
  • 0x3A

Stelle folgende Zahlen im Hexadezimalsystem dar und wandle sie ins Dezimalsystem um:

  • 1010 1010
  • 0111 1101
  • 1111 1110

Lösung

Den ARM-Risc-Simulator von Peter Higginson findet man hier: https://peterhigginson.co.uk/RISC/ Eine kleine Hilfe und die ganze Versionsgeschichte findet man direkt mit Help

Das ist die Übersicht aller möglichen Befehle: https://www.peterhigginson.co.uk/RISC/instruction_set.pdf

Der Simulator läuft direkt im Browser und besteht aus drei Teilen:

  • Rot: Programmeditor
  • Blau: CPU, Logik, Busse
  • Grün: RAM
  • Dazu kommt noch die Steuerung über die Buttons links unten.
Aufgabe 1:
  • Stelle zunächst die RAM-Darstellung von Dezimal (wie unprofessionell!) auf Hexadezimal um. Das machst du in den Options.
  • In Select findest du mehrere Beispielprogramme. Wähle add aus. Das Programm wird im Editor angezeigt und gleichzeitig ab Adresse 000 in den Speicher geladen.
  • Lass das Programm mit Step schrittweise ablaufen und beobachte, welche Daten in welcher Reihenfolge fliessen.
  • Du kannst die Animation mit den beiden Pfeiltaste « und » bremsen oder beschleunigen. Mit def fast in den Options kannst du die Animation auch komplett unterdrücken.
  • Später kannst du das ganze Programm mit Run auf einmal laufen lassen.
  • Versuche, die beiden Zahlen nicht zu addieren, sondern zu multiplizieren.
Aufgabe 2:
  • Sieh dir das Beispielprogramm example an.
Aufgabe 3:
  • Schreibe ein Programm, dass die beiden Dezimalzahlen 48801 und 53214 addiert.
  • Lege das Ergebnis an den Speicheradressen 102 und 103 ab.

Lösung

Aufgabe 4:
  • Schreibe ein Programm, dass die beiden Dezimalzahlen 48801 und 53214 multipliziert.
  • Lege das Ergebnis an den Speicheradressen 102 und 103 ab.
  • Verwende den Befehl MLX.
  • Beachte genau, was in den Registern passiert!

Lösung

Unsere Simulator-CPU kann nur 16-Bit-Zahlen. Was war nochmals die grösste darstellbare unsigned Zahl? Wir können wir zwei 32-Bit-Zahlen addieren oder sogar multiplizieren?

Aufgabe 5:
  • Schreibe ein Programm, das die beiden Dezimalzahlen 2'596'896'414 und 3'123'455'219 addiert.
  • Lege das Ergebnis an den Speicheradressen 104, 105 und 106 ab.

Lösung

Aufgabe 6:
  • Schreibe ein Programm, dass die beiden Dezimalzahlen 2'596'896'414 und 3'123'455'219 multipliziert.
  • Lege das Ergebnis an den Speicheradressen 104-107 ab.

Tipps:
Zerlege die beiden Zahlen in je zwei Teile, nämlich ein oberes und ein unteres Word (1 Word = 2 Bytes): $$2'596'896'414 = \text{0x}\,\text{9AC9}\,\text{7E9E} = \text{0x}\,\text{9AC9} \cdot 2^{16} + \text{0x}\,\text{7E9E} \cdot 2^0$$ $$3'123'455'219 = \text{0x}\,\text{BA2C}\,\text{24F3} = \text{0x}\,\text{BA2C} \cdot 2^{16} + \text{0x}\,\text{24F3} \cdot 2^0$$ Nun kommt Binom ;-) $$\text{0x}\,\text{9AC9}\,\text{7E9E} \cdot \text{0x}\,\text{BA2C}\,\text{24F3} = \text{0x}\,\text{9AC9} \cdot \text{0x}\,\text{BA2C} \cdot 2^{32} + \text{0x}\,\text{9AC9} \cdot \text{0x}\,\text{24F3} \cdot 2^{16} + \text{0x}\,\text{BA2C} \cdot \text{0x}\,\text{7E9E} \cdot 2^{16} + \text{0x}\,\text{7E9E} \cdot \text{0x}\,\text{24F3} \cdot 2^0$$ Jetzt kann man die einzelnen Summanden einfach richtig im Speicher verteilen:

$$2^0$$ 204
$$2^{16}$$ 205
$$2^{32}$$ 206
$$2^{48}$$ 207

Aber Achtung! Es gibt immer wieder Überträge, die in die Additionen für die einzelnen Speicheradressen mit einfliessen müssen.

Lösung

Aus fast allen Programmiersprachen gibt es Bedingungen, die entweder Verzweigungen (if…else…) oder Schleifen (for… oder while…) steuern. Damit ein Compiler oder Interpreter diese Anweisungen in Maschinensprache übersetzen kann, muss es das also auch dort geben.

Aufgabe 7:
  • Öffne in Select das Beispiel ascii.
  • Dieses Programm gibt in einer Schleife alle druckbaren Zeichen (32-127) der ASCII-Tabelle aus.
  • Lass das Programm mit Step schrittweise ablaufen und beobachte, wo genau sich die Schleife befindet.
  • Welche Zeile ist die Bedingung? Welche der Rücksprung? Wohin geht der Rücksprung?
  • Was genau macht das Programm, nachdem die Schleife verlassen wird?
Aufgabe 8:
  1. Schreibe ein Programm, das einen Countdown 10,9,…1,0 ausgibt.
  2. Schreibe ein Programm, das Zahlen 1 bis 100 addiert und am Ende die Summe ausgibt.

Lösung

Aufgabe 9:
  • Öffne in Select das Beispiel max.
  • Dem Kommentar kannst du entnehmen, was das Programm macht.
  • Probiere es mit drei Zahlenpaaren aus. Welche Ausgabe erwartest du?
    • Erste Zahl > zweite Zahl
    • Erste Zahl < zweite Zahl
    • Erste Zahl = zweite Zahl
  • Wie wird die Verzweigung (if…else…) implementiert?

Sobald ein Programm gestartet wird, erhält es vom Betriebssystem RAM zugewiesen. Dieses RAM ist in zwei Blöcke unterteilt, die für unterschiedliche Operationen eingesetzt werden: Die Heap und der Stack (Wikipedia). In unserem Simulator beginnt das Programm bei Adresse 0000 und der Stack bei Adresse 0400 (s. Register SP). Der Stack wird absteigend aufgefüllt und belegt so den unteren Teil im Main Memory. Alles dazwischen kann als Heap verwendet werden…

Bisher haben wir Daten jeweils an einer bestimmten Adresse im Speicher abgelegt. Bei dieser Methode muss man immer auch die Speicheradresse kennen, sonst sind die Daten nicht mehr auffindbar. Den Adressbereich im Speicher, der dafür zur Verfügung steht, nennt man die Heap. Auf der Heap werden einerseits Daten gespeichert, die über einen grossen Teil des Programm benötigt werden, und andrerseits alle Objekte, Arrays sowie Elemente, deren Grösse sich ändern kann. Da unterschiedliche Datentypen unterschiedliche Speichergrössen haben, wird die Heap sehr unübersichtlich, und die Verwaltung der Speicheradressen ist extrem wichtig und ziemlich aufwändig. Werden einzelne Speicherbereiche nicht mehr gebraucht, sollten sie freigegeben werden, und später sollten die so entstandenen Lücken durch sog. Defragmentierung wieder geschlossen werden.

Der Stack ist ein Speicher, der als Stapel angeordnet ist: Es kann immer nur auf das oberste Element zugegriffen werden, und beim Zugriff wird das oberste Element gewöhnlich auch gerade entfernt, sodass nun das zweioberste Element wieder gelesen werden kann. Somit müssen die Elemente, die auf dem Stack abgelegt sind, in exakt umgekehrter Reihenfolge wieder gelesen werden. Der Prozessor hat für den Stack spezielle Befehle, die schnell arbeiten und wenig Verwaltung erfordern: PSH (Push) und POP. Der Stack wird eingesetzt für

  • die Speicherung der Parameter bei Funktionsaufrufen
  • funktionslokale Variablen.

Funktionen sind Programmteile, die in genau gleicher Art immer wieder benötigt werden. Anstatt überall die Codezeilen erneut einzufügen, wird sie einmal geschrieben und von überall her aufgerufen.

Aufgabe 10:
  • Schreibe ein Programm, das
    • zwei Zahlen einliest und auf dem Stack ablegt
    • die Funktion 'Addition' aufruft
    • das Ergebnis vom Stack holt und ausgibt.
  • Schreibe (weiter unten) eine Funktion 'ADDTN', die
    • die beiden Zahlen vom Stack holt
    • addiert
    • und das Ergebnis wieder auf den Stack legt.

Lösung

Aufgabe 11:
  • Erweitere das Programm aus Aufgabe 10, dass es
    • noch eine dritte Zahl einliest
    • mit der Funktion 'ADDTN' zur Summe der ersten beiden Zahlen addiert.
    • das Ergebnis vom Stack holt und ausgibt.

Lösung

Aufgabe 12:
  • Schreibe ein Programm, das
    • eine (kleine!) Zahl einliest
    • die Zahl an die Funktion 'FCLT' übergibt
    • den Rückgabewert ausgibt.
  • Schreibe (weiter unten) eine Funktion 'FCLT', die die Fakultät der übergebenen Zahl berechnet und zurück gibt.

Lösung

Beachte dazu den Artikel in Wikipedia, vor allem die Begriffserklärung und den Abschnitt 3. Die Fakultät kann man nicht nur wie in Aufgabe 12 iterativ, sondern auch rekursiv berechnen: Die Funktion Fakultät einer natürlichen Zahl $n \geq 1$ ist definiert als das Produkt der Zahlen 1 bis $n$: $$n! = 1\cdot 2 \cdot 3 \dotsm n$$ Das lässt sich auch als Zahlenfolge und somit rekursiv darstellen: $$(n+1)! = (n+1)\cdot n!$$ Das lässt sich auch genauso programmieren.

In C# könnte eine solche Methode Fakultät() etwa so aussehen:

    static long Fakultät(long Zahl)
    {
      long lRet = 1;
      if (Zahl > 1)
      {
        lRet = Zahl * Fakultät(Zahl - 1);
      }
      return lRet;
    }
Aufgabe 13:
  • Schreibe ein Programm, das
    • eine (kleine!) Zahl einliest
    • die Zahl an die Funktion 'FCLT' übergibt
    • den Rückgabewert ausgibt.
  • Schreibe (weiter unten) eine Funktion 'FCLT', die die Fakultät der übergebenen Zahl berechnet und zurück gibt
    • Die Berechnung soll rekursiv erfolgen, dh. FCLT ruft sich selber immer wieder auf.
    • Dabei muss zwingend ein Abbruchkriterium programmiert werden!

Lösung

  • ef_informatik/assembler.1727169737.txt.gz
  • Zuletzt geändert: 2024-09-24 09:22
  • von sca