Frank's Akku-Stackmaschine

Das beiliegende Programm ist Assembler und Simulator für eine recht einfache Akku-Stackmaschine. Ein Assemblerlisting in einer relativ komfortablen Form (mit symbolischen Sprunglabels) eingelesen und anschließend ausgeführt. Dabei ist es möglich, dem Programm bei der Simulation zuzusehen.

Trotz der Einfachheit der Maschine sollte es möglich sein, fortgeschrittene Programmiersprachen auf ihr zu übersetzen, da wichtige Mechanismen wie indirekte Adressierung unterstützt werden. Auch Eingabe/Ausgabefunktionen sind vorhanden, damit ein Programm auch Eingaben lesen und Ausgaben drucken kann.

Kompilieren

Zum Kompilieren sind eventuell die Definitionen am Anfang des 'Makefile' zu editieren. Insbesondere sind die Befehlszeilen für LEX (lex oder flex) und YACC (yacc oder bison) zu setzen. Bei letzterem muß _unbedingt_ die Option '-d' mit angegeben werden, damit die Definitionsdatei korrekt erzeugt wird. Bei der Definition von LIBS müssen die verwendeten Bibliotheken angegeben werden, insbesondere '-lfl' bzw '-ll' für flex bzw. lex.

Anschließend muß nur noch 'make' aufgerufen werden, der Rest funktioniert automatisch. Abschließend sollte sich das ausführbare Programm 'sm' im Verzeichnis befinden.

Ausführen

Gestartet wird der Simulator mit der Befehlszeile
	sm  asmfile
wobei 'asmfile' eine Datei mit einem gültigen Assemblerprogramm ist. Das Programm wird geladen und sofort anschließend ausgeführt. Zusätzlich versteht das Programm noch drei Parameter:
-d
Schaltet das Debugging des Parsers ein, so das man bei dessen Arbeit zusehen kann.
-l
Druckt ein Assemblerlisting während des Ladens der Datei. Zu diesem Zeitpunkt sind sämtliche Sprunglabels noch nicht aufgelöst.
-r
Druckt ein Assemblerlisting während der Ausführung des Programms. Jeder Befehl wird vor seiner Ausführung auf gedruckt. Zusätzlich sieht man auch den Inhalt des Akkus und den Stackpointer.
Alle Parameter können simultan angegeben werden.

Die Maschine

Die simulierte Maschine ist eine Stackmaschine, die zusätzlich noch einen Akku besitzt, in dem die meisten Operationen durchgeführt werden. Durch das Vorhandensein des Akkus ist zwar das Verhalten von manchen Befehlen, die man von einer puren Stackmaschine kennt, etwas ungewohnt, sollte aber kompaktere Programme erlauben.

Die Maschine ist ein 32-Bit-Rechner mit 64kB Speicher. Integerarithmetik wird ebenso unterstützt wie Fließkommaarithmetik (letztere etwas unkomfortabel). Ein Programm teilt sich den Speicher mit dem Stack, wobei die Aufteilung, wo das Programm und wo der Stack im Speicher liegt, frei gewählt werden kann. Genau genommen sind sogar zwei Stacks vorhanden: neben dem 'Arithmetikstack', der mit den Stackbefehlen angesprochen werden kann auch noch der 'Call-Stack', auf dem Sprungadressen für Unterprogrammaufrufe abgelegt werden. Diese Differenzierung hat den Vorteil, daß der Stack auch über Unterprogramme hinweg verwendet werden kann, insbesondere ist eine Parameterübergabe über den Stack möglich. Da der Speicher aus 8-Bit großen Elementen besteht, ist darauf zu achten, daß eine Integerzahl 4 Bytes benötigt, und eine Fließkommazahl 8 Bytes. Da ein Stackelement und der Akku nur 4 Bytes groß sind, können Fließkommazahlen nicht direkt bearbeitet werden, sondern nur indirekt. Alle Fließkommabefehle arbeiten mit Adressen der eigentlichen Operanden.

Assemblerbefehle

In den folgenden Tabellen sind die Befehle nach Themengebiet tabellarisch aufgelistet. Dabei bedeutet '*' in der Spalte 'A', daß der Inhalt des Akkus verändert wird, die Zahl in der Spalte 'ST' bezeichnet die Änderung des Stackpointers (-1 = 1 Element wird vom Stack entfernt). Alle Assemblerbefehle müssen in Kleinschrift eingegeben werden.

1. Speicherzugriff

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
load # zahl       | * |  0 | Lädt den Akku mit der gegebenen Zahl
load addr         | * |  0 | Lädt den Akku mit der 4-Byte-Zahl, die an der
                  |   |    | gegebenen Adresse steht
load              | * | -1 | Lädt den Akku mit der 4-Byte-Zahl an der Adresse,
                  |   |    | auf die das oberste Stackelement zeigt
store addr        |   |  0 | Speichert den Akku als 4-Byte-Zahl an die Adresse
store             |   | -1 | Speichert den Akku als 4-Byte-Zahl an die Adresse,
                  |   |    | auf die das oberste Stackelement zeigt
                  |   |    |
loadw addr        | * |  0 | Lädt den Akku mit der 2-Byte-Zahl, die an der
                  |   |    | gegebenen Adresse steht
loadw             | * | -1 | Lädt den Akku mit der 2-Byte-Zahl an der Adresse,
                  |   |    | auf die das oberste Stackelement zeigt
loadb addr        | * |  0 | Lädt den Akku mit der 1-Byte-Zahl, die an der
                  |   |    | gegebenen Adresse steht
loadb             | * | -1 | Lädt den Akku mit der 1-Byte-Zahl an der Adresse,
                  |   |    | auf die das oberste Stackelement zeigt
                  |   |    |
storew addr       |   |  0 | Speichert den Akkuinhalt als 2-Byte-Zahl an die
                  |   |    | Adresse. Der Akku wird dabei auch dann nicht ver-
                  |   |    | ändert, wenn er grösser als 2 Bytes ist
storew            |   | -1 | Speichert den Akkuinhalt als 2-Byte-Zahl an die
                  |   |    | Adresse, auf die das oberste Stackelement zeigt
storeb addr       |   |  0 | Speichert den Akkuinhalt als 1-Byte-Zahl an die
                  |   |    | Adresse. Der Akku wird dabei auch dann nicht ver-
                  |   |    | ändert, wenn er grösser als 1 Byte ist
storeb            |   | -1 | Speichert den Akkuinhalt als 1-Byte-Zahl an die
                  |   |    | Adresse, auf die das oberste Stackelement zeigt
Bemerkung: Die Befehle 'loadw # zahl' und 'loadb # zahl' gibt es nicht, weil sie mit 'load # zahl' äquivalent sind.

2. Stackmanipulation

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
push              |   | +1 | Speichert Akku auf den Stack
pop               | * | -1 | Liest Wert vom Stack in den Akku
dup               |   | +1 | Verdoppelt oberstes Stackelement
swap              |   |  0 | Vertauscht die oberen beiden Stackelemente
exch              | * |  0 | Vertauscht Akku und oberstes Stackelement

3. Integerarithmetik

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
mult # zahl       | * |  0 | Multipliziert Akku mit Zahl (Akku = Akku * zahl)
mult addr         | * |  0 | Multipliziert Akku mit der Zahl, die an der
                  |   |    | gegebenen Speicheradresse steht
mult              | * | -1 | Multipliziert Akku mit oberstem Stackelement
                  |   |    |
add # zahl        | * |  0 | Addiert Zahl zum Akku (Akku = Akku + zahl)
add addr          | * |  0 | Addiert den Inhalt der Speicheradresse zum Akku
add               | * | -1 | Addiert oberstes Stackelement zum Akku
                  |   |    |
sub # zahl        | * |  0 | Subtrahiert Zahl vom Akku (Akku = Akku - zahl)
sub addr          | * |  0 | Subtrahiert Inhalt der Speicheradresse vom Akku
sub               | * | -1 | Subtrahiert oberstes Stackelement vom Akku
                  |   |    |
div # zahl        | * |  0 | Dividiert Akku durch Zahl (Akku = Akku / zahl)
                  |   |    | Das Ergebnis ist die untere Gaussklammer dieser
                  |   |    | Division
div addr          | * |  0 | Dividiert Akku durch Inhalt der Speicheradresse
div               | * | -1 | Dividiert Akku durch oberstes Stackelement

4. Bitarithmetik

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
inot              | * |  0 | Logisches not des Akku. Eine 0 wird zu -1, und
                  |   |    | jede beliebige Zahl ungleich 0 wird zu 0
not               | * |  0 | Bitweises not des Akku. Jedes Bit wird umgedreht
                  |   |    |
and # zahl        | * |  0 | Bitweises und des Akku mit der Zahl
and addr          | * |  0 | Bitweises und des Akku mit Inhalt des Speichers
and               | * | -1 | Bitweises und des Akku mit oberstem Stackelement
                  |   |    |
or # zahl         | * |  0 | Bitweises or des Akku mit der Zahl
or addr           | * |  0 | Bitweises or des Akku mit Inhalt des Speichers
or                | * | -1 | Bitweises or des Akku mit oberstem Stackelement
                  |   |    |
shl # zahl        | * |  0 | Shift Left. Der Akku wird um zahl Bits nach links
                  |   |    | verschoben. Rechts werden Null-Bits eingefügt
shl addr          | * |  0 | Verschiebt Akku um soviele Bits nach links wie in
                  |   |    | der Speicherzelle angegeben
shl               | * | -1 | Verschiebt Akku um soviele Bits nach links wie im
                  |   |    | obersten Stackelement angegeben
                  |   |    |
shr # zahl        | * |  0 | Shift Right. Der Akku wird um zahl Bits nach
                  |   |    | rechts verschoben. Links werden Null-Bits
                  |   |    | eingefügt.
shr addr          | * |  0 | Verschiebt Akku um soviele Bits nach rechts wie
                  |   |    | in der Speicherzelle angegeben
shr               | * | -1 | Verschiebt Akku um soviele Bits nach rechts wie
                  |   |    | im obersten Stackelement angegeben

5. Sprünge und Verzweigungen

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
jump addr         |   |  0 | Springt an die Speicheradresse
jump              |   | -1 | Springt an die Adresse, die durch das oberste
                  |   |    | Stackelement gegeben ist
call addr         |   |  0 | Unterprogrammaufruf. Die Programmausführung wird
                  |   |    | an addr fortgesetzt. Führt das Unterprogramm ei-
                  |   |    | nen 'ret'-Befehl aus, wird das Programm direkt
                  |   |    | hinter dem 'call' fortgesetzt
call              |   | -1 | Wie eben, doch wird hier an die Adresse verzweigt,
                  |   |    | die im obersten Stackelement steht
ret               |   |  0 | Rücksprung aus einem Unterprogramm genau hinter
                  |   |    | den 'call'-Befehl, mit dem das Unterprogramm auf-
                  |   |    | gerufen wurde
bzero addr        |   |  0 | Wenn Akku  = 0, so wird nach addr gesprungen
                  |   |    | Wenn Akku != 0, so wird die Ausführung direkt
                  |   |    | hinter dem Befehl fortgesetzt
bzero             |   | -1 | Wie eben, doch wird, wenn der Akku=0 ist, an die
                  |   |    | Adresse gesprungen, die im obersten Stackelement
                  |   |    | steht
bnzro addr        |   |  0 | Wenn Akku != 0, so wird nach addr gesprungen
                  |   |    | Wenn Akku  = 0, so wird die Ausführung direkt
                  |   |    | hinter dem Befehl fortgesetzt
bnzro             |   | -1 | Wie eben, doch wird, wenn der Akku!=0 ist, an die
                  |   |    | Adresse gesprungen, die im obersten Stackelement
                  |   |    | steht
bpos addr         |   |  0 | Wenn Akku  > 0, so wird nach addr gesprungen
                  |   |    | Wenn Akku <= 0, so wird die Ausführung direkt
                  |   |    | hinter dem Befehl fortgesetzt
bpos              |   | -1 | Wie eben, doch wird, wenn der Akku>0 ist, an die
                  |   |    | Adresse gesprungen, die im obersten Stackelement
                  |   |    | steht
bneg addr         |   |  0 | Wenn Akku  < 0, so wird nach addr gesprungen
                  |   |    | Wenn Akku >= 0, so wird die Ausführung direkt
                  |   |    | hinter dem Befehl fortgesetzt
bneg              |   | -1 | Wie eben, doch wird, wenn der Akku<0 ist, an die
                  |   |    | Adresse gesprungen, die im obersten Stackelement
                  |   |    | steht

6. Fliesskommaarithmetik

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
fmult             |   | -1 | Liest Fliesskommazahl aus der Adresse, auf die
                  |   |    | das oberste Stackelement zeigt, multipliziert
                  |   |    | diese mit der Fliesskommazahl, auf die der Akku
                  |   |    | zeigt, und speichert das Ergebnis an die Adresse,
                  |   |    | auf die der Akku zeigt, zurück
fdiv              |   | -1 | Liest Fliesskommazahl aus der Adresse, auf die
                  |   |    | der Akku zeigt, und teilt durch die Fliesskomma-
                  |   |    | zahl, auf die das oberste Stackelement zeigt
fadd              |   | -1 | Addiert die Fliesskommazahl, auf die das oberste
                  |   |    | Stackelement zeigt, zu der Fliesskommazahl hinzu,
                  |   |    | auf die der Akku zeigt
fsub              |   | -1 | Subtrahiert die Fliesskommazahl, auf die das
                  |   |    | oberste Stackelement zeigt, zu der Fliesskomma-
                  |   |    | zahl hinzu, auf die der Akku zeigt
itof addr         |   |    | Wandelt die 4-Byte-Integer-Zahl, die im Akku
                  |   |    | steht, nach Fliesskomma um, und speichert diese
                  |   |    | Fliesskommazahl an der gegebenen Adresse
itof              |   | -1 | Wandelt die 4-Byte-Integer-Zahl, die im Akku
                  |   |    | steht, nach Fliesskomma um, und speichert diese
                  |   |    | Fliesskommazahl and der Adresse, auf die das
                  |   |    | oberste Stackelement zeigt
ftoi addr         |   |    | Liest die Fliesskommazahl, die an der gegebenen
                  |   |    | Adresse steht, wandelt sie in eine 4-Byte-Integer
                  |   |    | Zahl (untere Gaussklammer) und speichert diese
                  |   |    | Integerzahl im Akku
ftoi              |   | -1 | Liest die Fliesskommazahl, auf die das oberste
                  |   |    | Stackelement zeigt, wandelt sie per unterer
                  |   |    | Gaussklammer in eine Integerzahl um und speichert
                  |   |    | diese Integerzahl im Akku

7. Eingabe / Ausgabe

Befehl            | A | ST | Beschreibung
------------------+---+----+--------------------------------------------------
print             |   |  0 | Druckt die Zahl im Akku als 4-Byte-Zahl aus
cprint            |   |  0 | Druckt den Wert im Akku als ASCII-Zeichen aus
                  |   |    | Dabei muss der Akkuinhalt 0<=akku>256 sein
fprint            |   |  0 | Druckt den Speicherinhalt, auf den der Akku
                  |   |    | zeigt, als Fliesskomma aus.
sprint            |   |  0 | Druckt den Speicherinhalt, auf den der Akku
                  |   |    | zeigt, als Null-terminierten String aus.
read              | * |  0 | Liest eine 4-Byte-Zahl von der Konsole in den
                  |   |    | Akku. Wird keine Zahl eingegeben, ist das Er-
                  |   |    | gebnis 0.
cread             | * |  0 | Liest ein ASCII-Zeichen von der Konsole in den
                  |   |    | Akku
fread             |   |  0 | Liest eine Fliesskommazahl von der Konsole und
                  |   |    | speichert diese an der Adresse, auf die der Akku
                  |   |    | zeigt. Wird keine Zahl eingegeben, ist das Er-
                  |   |    | gebnis 0.
sread             |   |  0 | Liest einen Null-terminierten String von der
                  |   |    | Konsole und speichert diesen beginnend mit der
                  |   |    | Adresse, auf die der Akku zeigt, ab

Das Assemblerlisting

Im Assemblerlisting können noch zusätzliche Befehle auftauchen, die nur beim Laden des Programms von Interesse sind, aber keine direkten Assemblerbefehle sind. Sie vereinfachen die Erstellung eines Programms erheblich.

1. Kommentare

Ein Kommentar im Assemblerlisting kann mit dem Semikolon ';' begonnen werden. Alles bis zum Zeilenende wird dann ignoriert.

2. Ausdrücke

Überall dort, wo ein expliziter Wert erwartet wird, kann auch ein arithmetischer Ausdruck angegeben werden. Die elementaren Rechenarten '+', '*', '-', '/' stehen zur Verfügung. Integerzahlen können sowohl dezimal als auch mit abschliessendem 'h' oder führendem Dollarzeichen (1000h bzw. $1000) hexadezimal angegeben werden.

3. Deklaration und Benutzung von Labels

Im Assemblerlisting kann ein Label als
	Label:
definiert werden. Dabei ist 'Label' ein beliebiger Text, der sich aus Grossbuchstaben, Kleinbuchstaben, Ziffern und dem Unterstrich zusammensetzen kann. Das erste Zeichen eines Labels muss ein Grossbuchstabe oder ein Unterstrich sein. Die Definition eines Labels muss eindeutig sein, und sie gilt auch rückwirkend (ein Label kann bereits vor seiner Definition verwendet werden). Label können unter ihrem Namen dann überall verwendet werden, insbesondere dort, wo ein Adresswert erwartet wird. Beispiel:
	call Unterprogramm
...
Unterprogramm:
	ret
Auch beim Laden eines expliziten Wertes kann ein Label verwendet werden:
	load # Unterprogramm
lädt die Adresse des Unterprogramms in den Akku.

Zusätzlich kann bei jedem Label noch ein Offset angegeben werden, also ein positiver oder negativer Ausdruck.

	load # Unterprogramm + 4
lädt die Adresse des Unterprogramms + 4 in den Akku.

4. Festlegen der Ladeadresse

An beliebiger Stelle des Assemblerlistings kann der folgende Ausdruck auftaruchen:
	:addr
Wobei addr eine Adresse ist. Von dort an wird das Assemblerlisting dann an diese Speicheradresse aufwärts geladen. Beispiel:
:100h
	add		; Steht an Speicheradresse 100h
:1000h
	add		; Steht an Speicheradresse 1000h
Dieses Beispiel ist natürlich unsinnig.

5. Daten

Mit dem Kommando 'data' können auch explizite Werte im Speicher abgelegt werden. Der Befehl versteht sowohl Integer (4-Byte-Zahlen), Words (2-Byte-Zahlen), Bytes (1-Byte-Zahlen), Labels (2-Byte-Adressen), Fliesskommazahlen (belegen 8 Bytes) und Strings (Null-Terminiert, variable Länge). Mit einem 'data'-Kommando können mehrere und verschiedene Daten, durch Komma getrennt, hintereinander abgelegt werden.

Es ist zu bemerken, dass das 'data'-Kommando die einzige Möglichkeit ist, Fliesskommakonstanten, die ja nicht per 'load' geladen werden können, zu definieren. Ein Programm, welches mit Fliesskommazahlen arbeitet, kommt nicht drum herum, per 'data'-Befehl eine Tabelle mit verwendeten Zahlen anzulegen. In diesem Zusammenhang erkennen wir auch den Vorteil, bei einem Label ein Offset angeben zu können. Dies erspart uns, jedem Wert ein eigenes Label zuzuordnen. Hier ein Beispiel für die gemeinsame Verwendung dieser Features:

Tabelle:
	data 42.0, 6.2, "Dies ist ein Test"

Programm:
	load # Tabelle		; Lade Adresse von 42.0
	fprint			; Drucke "42"
	load # Tabelle + 8	; Lade Adresse von 6.2, da jede Fliesskomma-
				; zahl 8 Bytes belegt
	fprint			; Drucke "6.2"
	load # Tabelle + 16	; Lade Adresse des Textes
	sprint			; Drucke Text
Es ist zu beachten: Jede Fliesskommazahl muss einen Dezimalpunkt enthalten, ansonsten wird sie als Integerzahl interpretiert. Würde oben die "42.0" durch "42" ersetzt, würde nichts mehr funktionieren, da sich dann auch die Adressen der folgenden Elemente verschieben. Words (also 2-Byte-Zahlen) werden von Integern durch ein führendes 'w' unterschieden. Ebenso erhalten Bytes (also 1-Byte-Zahlen) ein führendes 'x'.
Beispiel für alle möglichen Daten:
	data 100, w$100, x10h, Programm + 18, 42.0*24.0
Dies speichert Dezimal 100 als 4-Byte-Integer, hex 100 (=256) als 2-Byte-Zahl, eine hexadezimale 10 (=16) als Byte, die Adresse auf das Label 'Programm' mit dem Offset 18 wird als Adresse (2 Bytes) gespeichert, der Ausdruck wird aufgelöst (1008.0) und als Fliesskommazahl abgespeichert.

6. Includes

Mit
#include dateiname
wird eine weitere Datei genau an diese Stelle eingelesen. Zwischen dem Schlüsselwort 'include' und dem Dateinamen darf nur genau ein Leerzeichen sein. Die Datei darf ebenfalls weitere Dateien laden. Das ganze funktioniert nur, wenn das Programm mit Hilfe von flex übersetzt wurde.

Speicherorganisation

Damit ein Programm ablaufen kann, muss es der Maschine sagen, an welcher Speicherstelle das Programm gestartet werden soll. Weiterhin muss auch der Speicher aufgeteilt werden, insbesondere müssen der Maschine zwei Speicherbereiche für den Stack und den Call-Stack. Die entsprechenden Werte befinden sich in den ersten 6 Speicherstellen.
  Speicher    +--------------+
            0 | PROGRAM CTR  |
            1 | PROGRAM CTR  |
            2 | STACKPTR     |
            3 | STACKPTR     |
            4 | CALLSTACKPTR |
            5 | CALLSTACKPTR |
        6-255 | RESERVED     |
          256 |              |
            - | UNUSED       |
        65535 |              |
              +--------------+
Die Werte für PC, STACKPTR und CALLSTACKPTR, jeweils eine 2-Byte-Adresse, können im Assemblerlisting einfach mit dem data-Kommando gesetzt werden. Dabei wird der PC normalerweise auf die Adresse des Hauptprogramms gesetzt, und die anderen beiden Pointer auf relativ beliebige Adressen, so dass sie weder das Programm noch sich selber stören. Eventüll ist es ganz nützlich zu wissen, dass beide Stacks nach oben wachsen. Bei falscher Programmierung können beide Stacks die ursprünglichen Adressen auch unterschreiten.

Üblicherweise beginnt ein Listing deshalb wie folgt:

:0
	data MAIN, we000h, wf000h
Dadurch wird der PC auf das Label MAIN gesetzt (was natürlich zu definieren ist), der STACKPTR auf die Adresse e000, und der CALLSTACKPTR auf f000. Da die Adressen bis 256 reserviert sind, sollte an dieser Stelle
:256
folgen, damit keine Daten im reservierten Speicherbereich abgelegt werden.

Programmstart und Programmende

Wie und wo das Programm gestartet wird, wissen wir nun, nämlich einfach durch Schreiben des PC. Beendet wird ein Programm einfach durch einen Sprung an die Speicheradresse 0. Das kann sowohl über einen 'jump' als auch einen beliebigen Verzweigungsbefehl geschehen. An dieser Stelle können wir endlich unser 'Hello World' schreiben:
:0
	data MAIN, we000h, wf000h

:1000h
	data "Hello World\n"

MAIN:
	load # 1000h
	sprint
	jump 0
Das Program lädt die Adresse des Strings in den Akku und gibt dann den String aus.

Weitere Beispiele

Im Unterverzeichnis 'asm' befinden sich zwei weitere kleine Assemblerprogramme und eine Unterfunktion:
fib.asm
Berechnung von Fibonaccizahlen
pow.asm
Berechnung von Exponenten (a ^ b)
memcpy.inc
Die 'memcpy'-Funktion.
Während die ersten beiden Listings direkt geladen und ausgeführt werden können, ist das letztere nur eine Funktion, die nur per #include verwendet werden kann. In pow.asm wird gleichzeitig die Handhabung von Fliesskommazahlen demonstriert.


Frank Pilhofer <fp -AT- fpx.de> Back to the Homepage
Last modified: Fri Jun 8 13:02:13 2001