Primitive oder nicht: Diese Frage stellt sich mit Sicherheit irgendwann jedem Forth-Programmierer. FORTH-Worte können nämlich aus bereits bestehenden FORTH-Worten aufgebaut oder aber direkt in Maschinensprache definiert werden. Letztere werden als Primitive bezeichnet und können im volksFORTH mit Hilfe eines einfachen Inline-Assemblers erstellt werden. Lohnt es sich überhaupt, solche Maschinencoderoutinen einzubauen, oder könnte man nicht gleich ganz in Assembler programmieren? Die Antwort auf diese Fragen sowie die Definition von Primitiven, ihre Anwendung und der Umgang mit dem volksFORTH-Assembler wird in dieser Folge erläutert.
FORTH-Worte werden in der Regel aus bereits vorhandenen FORTH-Worten zusammengebaut. Die Ausführungsgeschwindigkeit eines solchen ohnehin schnellen „Highlevel“-Wortes kann in den meisten Fällen noch weiter gesteigert werden, wenn das betreffende Wort ganz oder teilweise in Maschinencode definiert wird. Solche Worte werden Primitive genannt, wobei lediglich der Umstand beschrieben wird, daß Primitive aus Maschinenbefehlen bestehen. Primitive werden z. B. für elementare Systemfunktionen, Betriebssystemaufrufe oder Fließkommaberechnungen eingesetzt. So besteht der Sprachkern der meisten FORTH-Systeme für den Atari ST bis zu ca. 75 % aus Primitiven. Den Rest machen Highlevel-Worte aus, die sich auf diesen Primitiven aufbauen. Allerdings bringen Primitive nicht nur Vorteile.
So belegen Maschinencodedefintionen mehr Speicherplatz als ein Highlevel-wort, in dem jedes Komponentenwort nur vier Bytes für seine Adresse belegt. Wesentlich schwerer wiegt jedoch der Nachteil, daß durch die Verwendung von Maschinencodedefinitionen die Übertragbarkeit von Quelltext zwischen verschiedenen FORTH-Systemen selbst auf der gleichen Hardware stark eingeschränkt wird.
Die Definition eines Primitiven wird durch das Definitionswort CODE eingeleitet. CODE wird in volksFORTH in der folgenden Form eingesetzt:
CODE <Name>
:
< Assembler Worte >
NEXT
END-CODE
Wer die letzte Folge dieser Einführungsserie gelesen hat, wird sich bereits die Funktion des Definitionswortes CODE denken können. Seine Hauptaufgabe besteht darin, einen Wörterbucheintrag für den nachfolgenden Wortnamen <Name> aufzubauen. Dieser Wörterbucheintrag entspricht dem eines Highlevel-Wortes. Der einzige Unterschied ist der Inhalt des Codefeldes. Während der Zeiger im Codefeld eines Highlevel-Wortes auf eine Maschinenroutine zeigt, die für das jeweilige Definitionswort charakteristisch ist, enthält das Codefeld eines Primitiven einen Zeiger auf das eigene Parameterfeld. Genau da nämlich befindet sich die Maschinenroutine, die beim Aufruf des Primitiven zur Ausführung gebracht wird. Das Parameterfeld eines Primitiven wird durch die auf CODE folgenden Assembler-Worte gefüllt. Bei ihnen handelt es sich hauptsächlich um FORTH-Worte, deren Namen mit den 68000er-Mne-monics identisch sind, aber auch um Kontroll- und Steuerworte. Die Assembler-Worte befinden sich in einem separaten Vokabular mit dem Namen ASSEMBLER. Damit der Textinterpreter die Assembler-Worte bei ihrer Eingabe auch finden und ausführen kann, muß zunächst das Vokabular ASSEMBLER als „Context-Vokabular“ vereinbart werden. Dies erledigt CODE durch Ausführung des Wortes ASSEMBLER. Falls Sie mit der Vokabularstruktur des Wörterbuches noch nicht vertraut sind, lesen Sie den Infokasten, der diesen Sachverhalt ein wenig ausführlicher erläutert.
Beendet wird die Definition eines Primitiven stets durch einen Sprung zu dem Adreßinterpreter NEXT (es gibt auch Ausnahmen, von denen später kurz die Rede sein wird). Dies wird durch das Assembler-Wort NEXT realisiert. NEXT kompiliert entweder einen Sprungbefehl nach NEXT oder, wie beim volksFORTH-Assembler, den Adreßinterpreter selber an das Ende des Parameterfeldes von <Name>. END-CODE beendet schließlich eine Maschinencodedefinition, indem es das ASSEMBLER-Vokabular aus der Liste der vom Textinterpreter zu durchsuchenden Vokabulare entfernt und das vor der Ausführung von CODE aktuelle Vokabular wieder als Context-Vokabular vereinbart.
Der FORTH-Assembler ist ein Werkzeug, um Primitive definieren oder allgemein Maschinencoderoutinen in FORTH schreiben zu können. Mit einem Assembler wie diesem ist es allerdings nicht möglich, eigenständige Maschinenprogramme zu erstellen. Dazu müßte der Assembler um die Möglichkeit, eine lauffähige PRG-Datei erzeugen zu können, erweitert werden. Genausowenig ist es aufgrund der FORTH eigenen Schreibweise möglich, Quelltexte zu assemblieren, die für einen „richtigen“ Assembler geschrieben wurden. In diesem Fall müßte man den FORTH-Assembler so erweitern, daß er auch in der Lage ist, bei Bedarf den Quelltext eines Motorola-Standardassemblers verarbeiten zu können. Das Erweitern des FORTH-Assemblers, der ja im Grunde kein homogenes Programm, sondern eine Aneinanderreihung an sich unabhängiger „Module“ darstellt, ist relativ problemlos zu bewerkstelligen. Ob sich der Aufwand allerdings lohnt, sei einmal dahingestellt, möglich ist es jedenfalls.
Bei der Programmentwicklung wird der Assembler am Anfang nur selten eingesetzt werden. Der Grund dafür ist, daß Highlevel-Worte in der Regel schneller zu erstellen und leichter zu testen sind als Primitive. Erst wenn das Programm oder ein Programmteil fertig gestellt ist, kann man damit beginnen, einzelne Worte in Maschinencode zu recodieren. Da es dem Adreßinterpreter „egal“ ist, welche Art von Wort er auszuführen hat (jedes Wort wird durch den Adressinterpreter auf die gleiche Art und Weise, nämlich über seinen Codefeldzeiger, aufgerufen), kann ein Highlevel-Wort durch ein entsprechendes Primitive .ersetzt werden, ohne den Rest des Programms ändern zu müssen.
Der Quellcode für den volksFORTH-83-Assembler befindet sich auf der zweiten der insgesamt zwei Disketten, die über den PD-Service bezogen werden können. Der Assembler ist sehr gut dokumentiert (die Dokumentation befindet sich in der Datei F_ASSEM.
DOC im Ordner lST_WORD.-DOC), so daß an dieser Stelle nur einige besonders wichtige Dinge besprochen werden sollen. Der Assembler kann z. B. durch die Befehlsfolge
USE ASSEMBLER.SCR <return>
1 LOAD <return>
oder einfach durch
INCLUDE ASSEMBLER.SCR
geladen und kompiliert werden. Wer sich vom Umfang des Assemblers überzeugen möchte, kann es einmal mit WORDS versuchen. Dazu muß aber das ASSEMBLER-Vokabular als Context-Vokabular vereinbart werden, was wie erwähnt durch Ausführung des Wortes ASSEMBLER geschieht. Nun steht dem Anwender der komplette Befehlssatz des 68000er zur Verfügung sowie zahlreiche Zusatzworte, mit denen z. B. Labels definiert oder Kontrollstrukturen (Entscheidungen und Schleifen) gebildet werden können. Definitionsworte für einfache Datenstrukturen wie z. B. ’EQU’ oder ’DW’ gibt es nicht, da man auch innerhalb einer Maschinencodedefinition sowohl auf alle Datenstrukturen des FORTH-Kerns (Variablen und Konstanten) als auch auf alle benutzerdefinierten Datenstrukturen zurückgreifen kann.
Um den Assembler besser kennenzulernen, sollen zwei Beispiele für typische Maschinencodedefinitionen vorgestellt werden. Dazu muß aber noch eine Besonderheit des FORTH-Assemblers vorangestellt werden: Auch innerhalb des Assemblers gilt die Umgekehrt Polnische Notation, d. h. es werden wieder zuerst die Operanden aufgeführt und dann erst der Befehl. So wird aus dem Befehl ’MOVE DO, Dl’ des Standard Assemblers in FORTH der Befehl Dl DO MOVE’. Nicht immer gelingt die Umsetzung so leicht wie in diesem Beispiel, so daß der FORTH-Assembler auch für erfahrenere Assembler-Programmierer recht gewöhnungsbedürftig ist.
Doch zurück zu den angekündigten Beispielen. Zunächst soll ein kleines Wort definiert werden, welches den Inhalt des obersten Stackelements mit zehn multipliziert. An diesem Beispiel sehen Sie, wie Maschinenroutinen über den Stack Parameter übergeben werden. Auch bei Maschinencodedefinitionen stellt der Stack die Schittstelle zur Parameterübergabe dar. Vorausgesetzt, der volksFORTH-Assembler wurde geladen, kann das Wort wie folgt aufgebaut werden:
CODE ZEHNMAL Ok
SP )+ DO MOVE Ok
10 # Dl MOVE Ok
Dl DO MULU Ok
DO SP -) MOVE Ok
NEXT Ok
END-CODE Ok
Zunächst erkennen Sie an dem stets erscheinenden ’Ok’, daß sich FORTH während der Definition eines Primitiven im Ausführungsmodus befindet. Alle Worte des ASSEMBLERS-Vokabular werden direkt ausgeführt und kompilieren, wenn es sich um einen Maschinenbefehl handelt, den entsprechenden Prozessoropcode in das Wörterbuch. Es ist daher auch ohne weiteres möglich, während der Definition eines Primitiven z. B. irgendwelche Berechnungen durchzuführen, den Editor aufzurufen, eine Datei auszudrucken usw. Auch führt eine Fehlersituation nicht zu einem Abbruch der Definition, wie es etwa bei der Definition eines Highlevel-Wortes der Fall ist. Allerdings sollten Sie generell darauf achten, daß während der Definition einer Maschinencoderoutine der Inhalt des Stacks nicht verändert wird, da viele Assembler-Worte bestimmte Kontrollwerte zwischenzeitlich auf dem Stack ablegen, um die Richtigkeit von Adressierungsarten oder Kontrollstrukturen zu überprüfen.
Das Primitive ZEHNMAL wird durch das Definitionswort CODE eingeleitet, welches für den nachfolgenden Namen einen Wörterbucheintrag anlegt. Nun folgen die Assembler-Worte, die (da sich FORTH ja noch im Ausführungsmodus befindet) sofort ausgeführt werden. Da wäre zunächst der Befehl ’ SP )+ DO MOVE’, welcher das oberste Element auf dem Stack in das Register DO transportiert. Da dieses Element gleichzeitig auch vom Stack verschwinden soll, wird die indirekte Adressierung (SP steht für Stack Pointer bzw. Adreßregister A6, welches den Datenstackzeiger enthält) mit pre-inkrement verwendet. Der Multiplikator wird in Register Dl geladen. Es folgt eine Multiplikation des Inhalts von Dl mit dem Inhalt von DO durch den Befehl ’Dl DO MULU’. Anschließend wird der Inhalt von DO wieder, diesmal allerdings durch die Adressierungsart 'Indirekt mit Postdekrement’ um eine Zahl in den Stack aufzunehmen, in das oberste Stackelement zurückübertragen. Das Assemblerwort NEXT kompiliert den Adreß-interpreter an das Ende des Wortes, damit nach der Ausführung von ZEHNMAL die Kontrolle wieder an das rufende Wort zurückgegeben werden kann. Die Definition des Primitiven ZEHNMAL, das sicher um einiges einfacher hätte definiert werden können, endet mit END-CODE, welches lediglich das ASSEMBLER-Vokabular „unsichtbar“ macht und das vor dem Aufruf von ASSEMBLER als Context vereinbarte Vokabular wieder zum Context-Vokabular macht.
Wie bereits erwähnt, kann während der Definition eines Primitiven auch auf bereits existierende Datenstrukturen wie z. B. Variablen zurückgegriffen werden. Auch dazu ein kleines Beispiel:
VARIABLE ZAHL OK
O ZAHL!
CODE ZAHL=5 OK
5 # ZAHL R#) MOVE OK
ZAHL R#) Dl MOVE OK
NEXT
END-CODE
Die Ausführung von ZAHL = 5 lädt schlicht und einfach die Variable ZAHL mit der Zahl 5. Mit ein wenig mehr Aufwand lassen sich so auch komplexere Datenstrukturen wie Listen oder Rekords bearbeiten.
Als zweites Beispiel soll gezeigt werden, wie sich z. B. GEMDOS-Aufrufe durchführen lassen. Zwar haben die meisten Entwickler von FORTH-Systemen diese Arbeit dem Anwender bereits abgenommen, indem sie fertige Worte zur Verfügung stellen, dennoch ist es recht lehrreich, eine solche Definition noch einmal nachzuvollziehen.
CODE MEDIACH? OK
.W (DRV R#) A 7 -) MOVE OK
9 # A7 -) MOVE OK
$0D TRAP OK
4 A7 ADDQ OK
DO SP -) MOVE OK
NEXT
END-CODE
Wieviel man von einem Programm letztlich in Maschinencode definiert, bleibt dem Programmierer überlassen. Bestimmte Dinge, z. B. Betriebssystemaufrufe, lassen sich, sofern solche Worte nicht von vorneherein im System enthalten sind, nur in Maschinencode schreiben. Das gleiche gilt auch für Fließkommaroutinen, die ansonsten unerträglich langsam wären. Andererseits lohnt es sich kaum, Worte, die z. B. auf Tastatureingaben warten, in Maschinencode zu definieren. Als objektiver Maßstab läßt sich eigentlich nur ein Vergleich der Ausführungsgeschwindigkeiten heranziehen. Ein gutes Beispiel für einen solchen Vergleich bietet das Wort FILL, das einen bestimmten Speicherbereich mit einer Konstanten füllt. Abb. 1 zeigt FILL einmal in Maschinencode definiert und einmal in der entsprechenden Highlevel-Version. An den gemessenen Zeiten (ca. 2.8 Sec für die Maschinencoderoutine und ca. 1.05 Min für das Highlevel-Wort!) wird deutlich, wie groß der Geschwindigkeitsvorteil durch eine Maschinencodedefinition werden kann. Allerdings ist in dieser Gegenüberstellung der jeweilige Entwicklungsaufwand nicht berücksichtigt.
Nicht immer muß einer Maschinenroutine ein Wortkopf bestehend aus Namens-, Verbindungs- und Codefeld vorangehen. In manchen Fällen, reicht es, wenn sich eine Maschinencoderoutine irgendwo im Speicher befindet. Eine solche Maschinenroutine kann dann nicht über die Tastatur, sondern nur von anderen Primitiven über einen Sprungbefehl aufgerufen werden. Diese Maschinenroutine muß dann auch nicht mit einem Sprung zu NEXT enden, sondern kann z. B. mit einem ’RTS’-Befehl abgeschlossen werden. Um eine sog. „headerlose“ Routine zu erstellen, kann man wieder auf den eingebauten Assembler zurückgreifen. Da diesmal auf das Definitionswort CODE verzichtet wird, muß zunächst das ASSEMBLER-Vokabular durch Ausführen von ASSEMBLER „per Hand“ als CONTEXT-Vokabular vereinbart werden. Nun kann die Maschinencoderoutine mit Hilfe der Assembler-Worte auf die gleiche Art und Weise wie innerhalb einer CODE Definition erstellt werden. So lassen sich z. B. bestimmte Teile des Betriebssystems des Atari ST patchen, sofern sich das Betriebssystem im RAM befindet. In diesem Fall muß der Wörterbuchzeiger, der ja maßgeblich dafür verantwortlich ist, an welche Stelle ein Maschinenbefehl gespeichert wird, kurzzeitig auf die Adresse der zu ändernden Routine gesetzt werden.
Schließlich soll die Möglichkeit erwähnt werden, Maschinencoderoutinen mit Highlevel-Worten zu kombinieren. Durch das Wort ;C: wird das FORTH-System innerhalb einer Pri-mitive-Definition in den Compilemo-dus geschaltet. Dies kann z. B. notwendig sein, wenn innerhalb einer Maschinenroutine ein Text ausgegeben werden soll:
CODE TEST
---
0< IF ;C: .” Fehler in der Eingabe”
ASSEMBLER THEN
;C: beendet eine Primitive-Definition und schaltet durch das Wort ’J’ den Compilemodus ein (so einfach geht das in FORTH, probieren Sie es einmal aus). Da anschließend die Kontrolle wieder an den Adreßinterpreter übergeben wird, kann die Primitive-Definition nicht fortgeführt werden. Dies wäre nur möglich, wenn das aufgerufene Highlevel-Wort mit einem RTS-Befehl enden würde, wie dies z. B. bei FORTH-Systemen der Fall ist, deren Worte direkt verknüpft sind und daher keinen Adreßinterpreter benötigen.
Wie bereits erwähnt, ist es mit der Übertragbarkeit von Assemblerroutinen zwischen verschiedenen FORTH-Systemen auf dem Atari ST nicht sehr weit her. So verfügt jede FORTH-Version für den ST, inzwischen sind es fast ein Dutzend, über ihren eigenen Assembler-Befehlssatz. Abhilfe schafft hier nur ein Blick ins Handbuch bzw. in den meist reichhaltigen Quellcode, der jedes FORTH-System begleitet.
Assemblierte Wortdefinitionen lassen sich mit Hilfe eines Disassembleres wieder in ihre ursprüngliche Form zurückübersetzen. Auch volksFORTH verfügt über einen Dissambier, der z. B. durch
INCLUDE DISASS.SCR
geladen werden kann. Der volksFORTH Disassembler kann zum einen zum Disassemblieren von Primitiven verwendet werden:
DISW < Name >
wobei <Name> der Name des Primitiven ist. Genausogut lassen sich aber auch allgemeine Maschinenroutinen bzw. Routinen des Betriebssystems disassemblieren. Der Disassembler muß dann in der Form
DIS <adr>
bzw.
LDIS <adr>
aufgerufen werden, wobei LDIS für Routinen ausserhalb von FORTH angewendet wird. Probieren Sie den Disassembler ruhig öfters aus. Sie lernen dadurch mehr über den Aufbau eines FORTH-Systems, als es im Rahmen einer Artikelserie oder auch eines Lehrbuches möglich ist. Überhaupt sollten sie einen Blick in den Quellcode von volksFORTH werfen bzw. diesen einmal ausführlicher studieren. Sie finden dort auch viele gute und anschauliche Beispiele für Maschinenspracheprogrammierung in FORTH.
Das FORTH-Wörterbuch ist in mehrere Untereinheiten aufgeteilt, die Vokabulare genannt werden. Die Unterteilung des Wörterbuche in Vokabulare bringt in erster Linie zwei Vorteile. Zum einen wird die Suche nach einem bestimmten Wort beschleunigt, wenn nicht das gesamte Wörterbuch, sondern nur ein kleiner Teil davon durchsucht werden muß. Zum anderen können funktionsverwandte Wörter in einem Vokabular zusammengelaßt werden. So werden beispielsweise alle Assembler-Worte in einem Vokabular mil dem Namen ASSEMBLER, alle Edit«, Worte in einem Vokabular mit dem Namen EDITOR oder alle Fließkomma-Worte in einem Vokabular mit dem Namen FLOAT zusammengefaßt.
Vokabulare werden durch das Definitionswort VOCABULARY Entert. So kann z. B. ein neues Vokabular mit dem Namen NEU wie folgt definiert werden:
VOCABULARY NEU
VOCABULARY erzeugl für NEU einen Wörterbucheintrag, der aber ein wenig komplizierter aufgebaut ist als der Wörterbucheintrag einer normalen Wortdefin tion Damit das neue Vokabular auch vom Textinterpreter durchsucht wird, muß es als Context-Vokabular vereinbart werden D es geschieht einfach durch Eingabe des Vokabulamamens:
NEU ok
Die Eingabe eines Vokabularnamens erklärt das betreffende Vokabular zum Context-Vokabular, was b w kt daß dieses Vokabular bei einem Suchlauf mit FIND als erstes durchsucht wird. Nun wir das Vokabular zwar in einem Suchlauf berücksichtigt; damit aber auch Wörter in das neue Vokabular eingetragen werden können, muß das NEU auch als Current-Vokabular vereinbart werden. Dies geschieht durch :
NEU DEFINITIONS
Nach der Definition eines Wortes :
: TEST . ” AHA " :
wird dieses Wort nicht in das standardmäßig vereinbarte Vokabular FORTH, sondern in das Vokabular NEU eingetragen, das damit sein erstes Wort enthält.
In FORTH existiert eine ganz bestimmte Ordnung, nach der die einzelnen Vokabulare des Wörterbuches durchsucht werden. Diese Suchordnung kann mit ORDER ausgegeben werden:
ORDER
NEU FORTH ONLY NEU
Sie erkennen, daß das erste zu durchsuchende Vokabular NEU heißt. Dieses erste Vokabular in der Suchordnung wird als „Transienf-Vokabu-lar bezeichnet. Transient bedeutet soviel wie flüchtig und beschreibt den Sachverhalt ganz gut. da dieses Vokabular nur solange in die Suchreihenlolge aulgenommen wird, wie kein anderes Vokabular als Context-Vokabular vereinbart wird.
Wird etwa durch
FORTH
das Vokabular FORTH erneut als Context Vokabula vereinbart, so will FORTH nicht mehr von einem Wort mit dem Namen TEST wissen.
da das Vokabular, in dem sich TEST befindet, nicht in die Suchreihenfolge aufgenommen ist:
TEST TEST haeh?
Allerdings werden alle neuen Wortdefinitionen nach wie vor in das Vokabular NEU aufgenommen, da dieses Vokabular immer noch als Current-Vokabular vereinbart Ist. Erst durch
FORTH DEFINITIONS
wird der alte Zustand wiederhergestellt. Das gleiche kann auch durch das Wort ONLYFORTH erreicht werden, das generell den „Original"-Zustand wieder herstellt.
Das Transient-Vokabular kann durch das Wort ALSO permanent in die Suchreihenfolge aufgenommen werden. So wird durch
NEU ALSO
das Vokabular NEU wieder in die Suchreihenlolge mit aufgenommen und die Eingabe von
TEST AHA Ok
führt zu dem gewünschten Resultat. ALSO bewirkt lediglich, daß das momentane Transient-Vokabular auf dem Vokabular-Stack verdoppelt wird. Davon kann mit sich durch ORDER leicht überzeugen.
Ein anderer Aspekt, der sich durch die Vokabularstruktur des Wörterbuches ergibt, soll an dieser Stelle erwähnt werden. Durch das Aufteilen des Wörterbuches in einzelne Vokabulare kann ein Wortname zwei vollkommen unterschiedliche Bedeutungen haben, wenn er in zwei verschiedenen Vokabularen eingetragen ist. Durch ein einfaches Umschaiten des Con-text-Vokabulars kann einer Gruppe von Worten eine vollkommen neue Bedeutung zugeordnet werden.
Abbildung 1
CODE FILL- ( adr anzahl byte----)
SP )+ DO MOVE
SP )+ Dl MOVE
SP )+ D6 MOVE
D6 REG) AO LEA
D1 TST 0< > IF
1 D1 SUBQ
D1 D0
.B DO AO )+ MOVE
LOOP THEN
NEXT
END-CODE
: FILL1 ( adr anzahl byte--)
SWAP 0
DO
OVER 1 + >R
DUP R> C!
LOOP
2 DROP
;
CREATE TESTFELD 10000 ALLOT
: BENCH 1
100 0 DO
TESTFELD 10000 65 FILL
LOOP
;
: BENCH 2
100 0 DO
TESTFELD 10000 66 FILL
LOOP
;