Andreas Rozek
[ Impressum ]   [ Datenschutzerklärung ]   [ Kontakt ]   [ ]

Assembler-Simulator

Assembler-Simulator

Zu den fundamentalsten Formen der Computer-Programmierung gehört die Verwendung eines Assemblers.

Marco Schweighauser hat einen sehr schönen Simulator für einen einfachen 8-Bit Prozessor mit integriertem Assembler geschrieben, der einen guten Einblick in die Welt der maschinennahen Programmierung ermöglicht.

Falls Sie die Vorlesung "Grundlagen der Informatik" an der HFT Stuttgart hören, finden Sie hier neben den dort bereits erläuterten Beispielen weitere Programme zur Vertiefung der Thematik und für eigene Experimente.

Der Simulator im Überblick

Der Simulator kann online genutzt werden, eine Installation ist nicht erforderlich. Alle wichtigen Informationen zu dem System finden Sie auf Github.

Die Bedienoberfläche umfasst

  • einen großen Editor für das auszuführende Programm
    Nota bene: vergessen Sie im Anschluss an eine Änderung nicht, das Programm erneut zu assemblieren - erst dadurch wird Ihre Änderung auch wirksam!
  • einen Ausgabebereich ("Konsole") für bis zu 24 Zeichen,
  • eine Anzeige für die vier Mehrzweck-Register, den "Instruction Pointer" und den "Stack Pointer" sowie die drei Flags ("Zero", "Carry", "Failure") des simulierten Prozessors,
  • eine Anzeige für die 256 Bytes des simulierten Speichers und schließlich
  • eine Liste der im Assembler-Programm definierten "Labels", mitsamt der konkreten Adresse und dem aktuellen Inhalt.

Explizite Eingaben gestattet der Simulator nicht, alle Daten müssen Teil des Assembler-Programmes sein.

Die Anzeige von Register- und Speicher-Inhalten kann wahlweise hexadezimal oder dezimal erfolgen, außerdem können erkannte Befehle in der Speicheranzeige farblich markiert werden.

Falls ein Register eine Adresse beinhaltet, kann die referenzierte Speicherzelle auf Wunsch farblich markiert werden, so wie es für "Instruction Pointer" und "Stack Pointer" standardmäßig bereits geschieht.

Die Konsole wird (im Sinne einer "memory-mapped IO") auf einen Bereich im Hauptspeicher abgebildet, die betreffenden Zellen sind grau hinterlegt.

Das erste Programm

Direkt nach dem Laden der zugehörigen Web-Seite enthält der Editor des Assembler-Simulators bereits ein "Hello World!"-Programm.

Drücken Sie auf Assemble, um dieses Programm zu übersetzen (und achten Sie darauf, wie der Speicher mit den Daten und Instruktionen aus dem Programm gefüllt wird.

Anschließend klicken Sie auf die grüne Run-Taste und sehen Sie dem Prozessor dabei zu, wie der Ausgabebereich mit dem Text "Hello World!" gefüllt wird - danach bleibt der Prozessor stehen.

Nota bene: Falls Sie ein bereits gelaufenes Programm noch einmal starten möchten, müssen Sie den Prozessor zunächst mithilfe der Reset-Taste zurücksetzen.

Die ersten eigenen Programme

Der Simulator wäre nur halb so schön, könnte man nicht selbst aktiv werden und eigene Programme laufen lassen.

Die folgenden Beispiele sind dafür gedacht, mit möglichst wenig Befehlen sichtbare Ausgaben zu produzieren.

  • ein einzelner Stern auf der Konsole
    Das erste Beispiel schreibt lediglich den ASCII-Code für einen Stern (*) in den Speicherbereich, der für die Konsole gedacht ist.
    Leeren Sie den Editor und kopieren Sie den Quelltext für dieses Beispiel hinein, klicken Sie auf Assemble, danach auf Run und beobachten Sie den Ausgabebereich
  • zwei Sterne auf der Konsole
    Die Anzeigefelder der Konsole belegen aufeinanderfolge Speicherzellen. Möchte man also einen zweiten Stern anzeigen, so muss der ASCII-Code dafür einfach in die auf 0xE8 folgende Speicherzelle geschrieben werden
  • zwei Sterne auf der Konsole (zweite Variante)
    Man muss die Adressen der Ausgabefelder nicht explizit angeben, man kann sie auch berechnen lassen und mithilfe einer "indirekten Adressierung" auf die Zellen zugreifen
  • "Mein Gott, es ist voller Sterne"
    Interessant ist die Adressberechnung bei der Verwendung von Schleifen - auf diese Weise kann man leicht die gesamte Konsole mit Sternen füllen.
    Da der Speicherbereich für die Ausgabe bis zur Adresse 0xFF reicht, erkennt man das Ende z.B. an einem Bereichsüberlauf nach dem Inkrementieren der Ausgabeadresse.
  • ...und falls man einen Fehler macht?
    Syntaktische (und auch den einen oder anderen semantischen) Fehler kann der Assembler selbst erkennen. Im allgemeinen hilft aber nur ein gezieltes Ausprobieren (d.h. "Testen"), um die Korrektheit eines Programmes zu zeigen.
    Zu den am schwierigsten zu findenden Fehlern gehört das versehentliche (bisweilen auch gezielte) Überschreiben des Programmes selbst. Probieren Sie aus was passiert, wenn Sie im vorherigen Programm anstelle des bedingten Sprunges einen unbedingten setzen...

Lernen Sie den Prozessor kennen

Für die Assembler-Programmierung ist es wichtig, das Verhalten des Prozessors sowie die Auswirkungen der einzelnen Befehle auf die Flags zu kennen.

Die folgenden Beispiele mögen deshalb vielleicht trivial wirken (sie sind es ja auch), tragen aber dennoch zum Verständnis des Systems bei. Werfen Sie deshalb am Ende jedes der folgenden Programme einen Blick auf die Register-Inhalte und Flag-Zustände!

  • MOV lässt Flags unangetastet
    Zu den wichtigsten Eigenschaften des MOV-Befehles gehört das Beibehalten aller Flag- Zustände: kaum ein Assembler-Programm könnte funktionieren, wenn sich ein Prozessor in dieser Hinsicht anders verhalten würde.
    Im Gegenzug bedeutet dies aber auch, dass das simple Laden einer 0 in ein Register nicht zum Setzen des "Zero Flag" führt - man muss den Prozessor schon mit einem CMP reg,0 explizit zu einem Test zwingen
  • INC und DEC verändern Flags - aber Vorsicht!
    im Allgemeinen benötigt man entweder CMP oder einen arithmetischen bzw. logischen Befehl, um die Flags zu aktualisieren - aber nicht immer verhält sich der Prozessor wie erwartet:
    • Inkrementieren des Wertes 255
      setzt das Carry und löscht das Zero Flag, obwohl das verwendete Register 0 zeigt - aus semantischer Sicht ist dieses Verhalten aber absolut korrekt

    • Dekrementieren einer 1
      setzt das Zero Flag, so wie es auch zu erwarten war

    • Dekrementieren einer 0
      setzt das Carry Flag, welches jetzt die Funktion eines "Borrow" übernimmt

  • NOT ist fehlerhaft
    Ebenso wie "echte" Prozessoren, beinhaltet auch der Simulator ein paar Fehler:
    • NOT setzt stets das Carry Flag
      ...und zwar unabhängig vom zu invertierenden Wert - ein solches Verhalten wird eher nicht erwartet.

      Allerdings kann diese Eigenheit durchaus auch praktisch genutzt werden: wenn man das Carry Flag (z.B. im Rahmen einer 16-Bit-Arithmetik) explizit setzen möchte, genügen zwei NOT-Befehle hintereinander (auf ein beliebiges, aber dasselbe Register angewendet), und das Carry Flag ist gesetzt, der Register-Inhalt jedoch unverändert

    • NOT invertiert 0xFF falsch
      Leider liefert NOT, auf 0xFF angewandt, nicht den Wert 0x00, sondern 0x100 (!) - also einen vollkommen ungültigen Wert - eine zweifache Invertierung liefert zwar wieder das ursprgl. 0xFF (wodurch der NOT-Befehl weiterhin zum Setzen des Carry Flag taugt), für eine einfache Invertierung ist der Befehl jedoch ungeeignet!

    • XOR anstelle von NOT
      Abhilfe (für die Invertierung, nicht aber für das Setzen des Carry Flag) bietet in diesem Fall der XOR-Befehl: die XOR-Verknüpfung eines beliebigen Registers mit dem konstanten Wert 0xFF bewirkt de facto eine Invertierung des Registerinhaltes

  • arithmetische Befehle
    Die arithmetischen Befehle verhalten sich wie erwartet - probieren Sie sie aus!

    Testen Sie durchaus auch einmal die Division durch 0
  • logische Befehle
    Die logischen Befehle bergen ebenfalls keine Überraschungen
  • Stack-Operationen
    Sehr angenehm am Simulator ist auch die Unterstützung eines Stapelspeichers ("Stack") und die Art und Weise, wie der Stack in der Speicheranzeige dargestellt wird.
    Lassen Sie die folgenden Beispiele laufen und sehen Sie sich den jeweiligen Stack-Zustand an (z.B. die Richtung, in der der Stack wächst, und was nach dem Abräumen des Stack im Speicher verbleibt)!

16-Bit Arithmetik mit dem 8-Bit-Prozessor

Die folgenden Beispiele beschäftigen sich nicht mehr mit dem Prozessor selbst, sondern lösen konkrete arithmetische Aufgaben. Nutzen Sie die Programme, um Ihre Kenntnisse im Umgang mit binären Zahlen zu vertiefen!

Mangels anderer Eingabemöglichkeiten müssen die zu bearbeitenden Zahlen direkt in das jeweilige Programm eingegeben werden - durch Füllen der beteiligten Register (A...D) mit den höher- bzw. niederwertigen Bytes der 16-Bit großen Operanden.

Auch die Ausgabe des (bzw. der) Ergebnisse erfolgt der Einfachheit halber (zumeist) über Register. Werfen Sie deshalb am Ende jeder Berechnung einen Blick auf die Registeranzeige im Simulator.

Ein- und Ausgabe erfolgen in der "big endian"-Reihenfolge: zuerst kommt das höherwertige, anschließend das niederwertige Byte (MSB vor LSB): A = MSB, B = LSB, C = MSB, D = LSB.

  • Vergleich zweier 16-Bit-Zahlen
    Das einfachste Beispiel vergleicht zwei 16-Bit große Zahlen miteinander.
    Tragen Sie die erste Zahl in die Register A und B, die zweite in die Register C und D ein. Nach Durchlaufen des Programmes zeigt die Konsole die Beziehung zwischen den beiden Registerpaaren AB und CD an: < bedeutet, dass AB kleiner als CD ist, = zeigt die Gleichheit der beiden Zahlen an, und > erscheint, falls AB größer als CD ist.
  • Inkrementieren und Dekrementieren einer 16-Bit-Zahl
    Die einfachsten Rechenoperationen sind das Erhöhen bzw. Erniedrigen einer Zahl um 1.
    Tragen Sie den gewünschten Operanden in die Register A und B ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht in dem Registerpaar auch das Rechenergebnis.
    Testen Sie insbesondere auch folgende Operanden: 0x0000, 0x00FF und 0xFFFF - achten Sie auch auf den zustand des Carry Flag am Ende der Berechnung!
  • Bilden des 2er-Komplements für eine 16-Bit-Zahl
    Von der Inkrementierung zur Bildung eines 2er-Komplements ist es nicht mehr weit.
    Tragen Sie den gewünschten Operanden in die Register A und B ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht in dem Registerpaar auch das Rechenergebnis.
    Testen Sie insbesondere auch folgende Operanden: 0x0000, 0x0001 und 0xFFFF!
  • Addieren und Subtrahieren zweier 16-Bit-Zahlen
    Das Addieren und Subtrahieren zweier 16-Bit breiter Zahlen ist schon ein wenig aufwändiger - insbesondere kommt hier auch zum erstenmal der "Trick" für das explizite Setzen des Carry Flag zum Einsatz.
    Tragen Sie die gewünschten Summanden in die Registerpaare AB bzw. CD ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar AB auch das Rechenergebnis.
    Achten Sie bei der Wahl der Zahlen für Ihre Tests insbesondere auch auf die Problemfälle des Verfahrens - nämlich die Überläufe nach der Verarbeitung der beiden LSBs sowie den endgültigen Überlauf nach Verarbeitung der MSBs!
  • Verschieben einer 16-Bit-Zahl um eine Stelle nach rechts
    Die einfachste Form der Division einer Dualzahl besteht in dem Verschieben aller Bits um eine Stelle nach rechts - entsprechend einer Division durch 2.
    Tragen Sie die zu verschiebende Zahl in die Register A und B ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht in dem Registerpaar auch das Ergebnis.
    Testen Sie insbesondere auch folgende Operanden: 0x0000, 0x0001, 0x0100 und 0xFFFF!
  • Verschieben einer 16-Bit-Zahl um eine Stelle nach links
    Die einfachste Form der Multiplikation einer Dualzahl besteht in dem Verschieben aller Bits um eine Stelle nach links - entsprechend einer Multiplikation mit 2.
    Tragen Sie die zu verschiebende Zahl in die Register A und B ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht in dem Registerpaar auch das Ergebnis.
    Testen Sie insbesondere auch folgende Operanden: 0x0000, 0x0080, 0x8000 und 0xFFFF - achten Sie auch auf den Zustand des Carry Flag am Ende jedes Durchlaufes!
  • Multiplizieren zweier 8-Bit-Zahlen zu einem 16-Bit-breiten Ergebnis
    Die Multiplikation zweier 8-Bit breiter Zahlen ist das erste etwas anspruchsvollere Beispiel in dieser Zusammenstellung.
    Tragen Sie die gewünschten Operanden in die Register A und B ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar CD das Ergebnis der Berechnung.
    Testen Sie insbesondere auch die Multiplikation mit 0 oder 1 sowie Zahlen, deren Produkt noch bzw. nicht mehr in ein einzelnes Byte passt!
  • Division einer 16-Bit-breiten Zahl durch eine 8-Bit-Zahl
    Die 16-Bit-Division ist das anspruchsvollste Beispiel auf dieser Seite.
    Tragen Sie den Dividenden in das Registerpaar AB und den Divisor in das Register C ein, assemblieren Sie und lassen Sie das Programm laufen. Am Ende steht im Registerpaar CD das Ergebnis der (Ganzzahl-)Division und im Registerpaar AB der verbleibende Divisionsrest.
    Testen Sie insbesondere auch die Division durch 0, 1 oder eine 2er-Potenz!