Speichern

Forth kennt von sich aus keine Datentypen, allerdings ist es in Forth möglich jeden Datentyp zu erzeugen. Manch einer sieht das als Nachteil andere sehen darin einen der größten Vorteile von Forth. Der Hintergrund dafür, dass das möglich ist, liegt darin, dass man in Forth direkten Zugriff auf den Speicher des Computers hat; man kann also mit ihm machen 'was man will'.

Es gibt zwar einige Worte, mit denen man Variablen anlegen kann, aber der eigentliche 'Witz' bei der Sache liegt darin, dass man in Forth den Speicher des Computers selber auslesen und beschreiben kann, wie man will; aber der Reihe nach:

Zwei Worte kennen sie schon: VARIABLE und ALLOT. Trotzdem soll hier noch einmal an ihre Arbeitsweise erinnert werden.

VARIABLE schafft eine benannte Variable. Genauer gesagt, wird dem Forth-Vokabular ein neuer Name hinzugefügt, und dieser Name dient dazu eine einfache Zahl zu speichern und wieder lesen zu können.

  VARIABLE fritz  ok

Schfft zum Beispiel eine Variable mit dem Namen 'fritz'. Wie kann man in dieser Variablen nun Werte speichern, von ihr lesen oder sie anzeigen lassen. Dazu gibt es drei Worte, die ihnen zum Teil auch schon bekannt sein dürften. Hier soll aber auch erklärt werden, wie sie funktionieren. Das erste, was dabei erklärt werden muss ist die, wie fritz denn nun arbeitet? Eigentlich ist es ganz einfach, fritz legt einfach eine Adresse auf den Stack und zwar nicht irgendeine Adresse, sondern die Adresse, bei der Zahlen gespeichert werden können.

  fritz  ok
  . 134545136  ok

Bei ihnen wird die Zahl mit ziemlicher Sicherheit eine andere sein, aber hier bedeutet es, dass an der Adresse 134545136 Platz für eine Zahl geschaffen wurde und dieser Platz mit dem Namen 'fritz' verknüpft ist. Wie speichert man nun eine Zahl an dieser Adresse? Dazu gibt es ein weiteres Forth-Wort: ! (genannt: "Store"). Dieses Wort bekommt einen Wert und eine Adresse übergeben und speichert diesen Wert an der Adresse ab. Da nun fritz eine Adresse auf dem Stack ablegt, ist das Speichern eines Wertes in fritz sehr einfach:

  5 fritz !
   ok

Das nächste Wort, dass nun besprochen werden soll, ist das Wort @ (genannt: "Fetch"). Dieses Wort bekommt eine Adresse übergeben und liest den Wert, der an dieser Adresse gespeichert ist, aus und legt ihn auf den Stack. Da hier wieder das Gleiche gilt, wie beim Speichern, ist das Auslesen einer Variablen genau so einfach:

  fritz @ .
  5  ok

Mitunter will man den Inhalt einer Variablen einfach nur ausgeben und nicht weiter damit arbeiten. In diesem Fall ist es einfacher einen weiteren Befehl zu nutzen: ?. Er stellt eine Kombination aus @ und . dar.

  fritz ?
  5  ok

Was ist nun, wenn man mehr als eine Zahl speichern will? Es ist ja zum Beispiel denkbar, dass man eine doppelte Zahl speichern will, die natürlich bei dem von VARIABLE bereitgestellten Platz nicht hinpasst. Es gibt in manchen Forth-Systemen das Wort 2VARIABLE, aber eigentlich ist es unnötig, denn es gibt das Wort ALLOT, dass sie auch schon kennen. ALLOT verschiebt das Ende des Forth-Wortschatzes um soviele Byte nach hinten, wie ihm als Argument übergeben werden. Da nun eine neue Variable immer am Ende dieses Wortschatzes angehängt wird, entsteht dadurch mehr Platz.

Bauen wir uns doch eine Variable, die Platz für eine doppelte Zahl schafft. Dazu muss man allerdings wissen, wieviele Bits eine einfache Zahl in seinem System einnimmt. In lina sind das 32 Bits, also 4 Byte. Dann legen wir mal los:

  VARIABLE cremer 4 ALLOT

VARIABLE schafft wieder ein neues Wort, hier 'cremer', und sorgt dafür, dass Platz für eine einfache Zahl dort angelegt wird. Mit dem 4 ALLOT wird dafür gesorgt, dass noch vier weitere Bytes Platz geschaffen wird, also insgesamt genug für eine doppelte Zahl. Auch dass sollte man mal ausprobieren.

  2.0 cremer 2!
    ok

Das dabei verwendete Wort 2! sollte selbsterklärend sein. Nun können wir mit cremer wieder genau so arbeiten, wie wir es gerade mit fritz gelernt haben. Man achte nur darauf, dass es sich nun um eine doppelte Zahl handelt.

  cremer 2@
    ok
  D.
  20  ok

Da nun klar ist, wie man zum Beispiel Platz für eine doppelte Zahl schafft, sollte auch klar sein, wie man noch mehr Platz schafft. Trotzdem auch hier noch ein Beispiel: Stellen sie sich vor, sie brauchen Platz für 5 (einfache) Zahlen. Das ist nun einfach:

  VARIABLE 5ZAHL 16 ALLOT
    ok

Stellvertretend für andere Positionen soll hier nun verdeutlicht werden, wie man auf die vierte Zahl zugreifen kann. Die Sache ist sehr einfach, wenn man sich erinnert, dass eine Variable beim Aufruf die Adresse auf den Stack legt, an der eine Zahl gespeichert werden kann und wenn man außerdem wieß wie lang eine Zahl beim eigenen Forth-System ist (zur Erinnerung: Bei mir sind es 32 Bit = 4 Byte).

Ruft man 5ZAHL auf, dann erhält man die Adresse der ersten Zahl. Addiert man zu dieser Adresse 4 hinzu, dann erhält man die Adresse der zweiten Zahl. Abermals vier addiert gibt dann die Adresse der dritten Zahl. Es ist klar, dass man 12 addieren muss, um an die Stelle der vierten Zahl zu kommen. Wenn man dort etwas speichern will, ist das nun ganz einfach:

  5ZAHL 12 + !
    ok

Und genau so einfach kann man es auch wieder auslesen:

  5ZAHL 12 + @
    ok

Ich denke, dass das Prinzip verstanden ist. Es geht aber noch besser! Nun kommt ein neues Wort ins Spiel, dass in Forth eine wichtige Bedeutung hat: CREATE

Forth kennt 'von Haus aus' eine Menge Worte und sie wissen auch, wie man dieser Liste von Worten noch weitere hinzufügt. Ohne hier in die Details zu gehen, kann ich ihnen schon mal verraten, dass all diese Worte in einer Liste abgelegt sind. Mit CREATE erzeugen sie ein neues Wort und hängen es an die Liste an. Wenn sie also eingeben:

  CREATE NEUESWORT

Dann wird ein neues Wort in die Liste eingetragen und kann von dem Moment an genau so verwendet werden, wie alle anderen Worte. Genau wie eine Variable legt es beim Aufruf seine Adresse auf den Stack. Der Unterschied zu einer Variablen liegt darin, dass dort kein Platz reserviert ist -- versuchen sie bitte nicht dort etwas zu speichern.

Aber man kann etwas besseres, wenn man das Wort , hinzu nimmt. Das Komma bekommt einen Wert übergeben und speichert ihn am Ende der Wortliste, dem Dictionary ab. Außerdem wird das Ende der Liste entsprechend erhöht, so dass der Eintrag eines weiteren Wortes diese Werte nicht mehr überschreibt.

Hätte man statt des Obigen, das folgende eingegeben:

  CREATE NEUESWORT 10 , 11 , 21 , 22 , 50 ,

Dann hätte man nicht nur ein neues Wort geschaffen, das wie eine Variable funktioniert, sondern man hätte auch Platz für 5 Zahlwerte geschaffen und diese sogar schon mit Werten belegt. Probieren sie es aus:

  NEUESWORT 12 + @ .
  22  ok

Sie sehen, es funktioniert. Auf diese Art kann man also nicht nur Variablen erzeugen, die mehr als eine Zahl aufnehmen können, sondern die sogar schon mit Werten belegen. In vielen Situationen ist das sinnvoll.

Neben den Variablen für einfache und doppelte Zahlen, kann man in Forth allerdings auch auf kleinere Einheiten zugreifen, auf Bytes. Das ist sinnvoll, weil zum Beispiel Zeichenketten oft aus Bytes aufgebaut sind. Die zugehörigen Wörter heißen dann auch C! und C@, die genau wie ! und @ arbeiten, allerdings immer nur ein Byte lesen oder Schreiben. Der Buchstabe 'C' deutete dabei schon darauf hin, wie sie einzusetzen sind, denn 'C' ist eine Abkürzung für 'Character', also (Buchstaben)Zeichen.

Eine kleine Ergänzung gibt es noch, wenn man in Forth von Speicher und Variablen spricht: Die Konstanten.

Es ist in Forth (und nicht nur da) guter Programmierstil Werte, die sich ändern können, in Konstanten zu verpacken. Ein sehr gutes Beispiel ist die Größe in Byte, die bei einer aktuellen Forth-Variante eine einfache Zahl einnimmt. Verwendet man hierfür eine Konstante, dann wird Programmkode viel portabler und der Grund dafür wird sofort klar, aber zunächst soll einmal die Frage geklärt werden, wie in Forth Konstanten definiert werden. Wenn man sich an die Forth-Sprechweise gewöhnt hat, eigentlich ganz einfach:

  5 CONSTANT foo

Mit der obigen Zeile wird eine Konstante 'foo' erzeugt und dieser Konstanten wird der Wert '5' zugewiesen; so einfach ist das.

Was ist nun der Unterschied zwischen einer Konstanten und einer Variablen? Der erste Unterschied liegt darin, dass man einer Konstanten keinen neuen Wert zuweisen kann (oder nur mit Schwierigkeiten). Außerdem 'arbeitet' eine Konstante auch anders als eine Variable. Bei einer Variablen erhielt man beim Aufruf eine Adresse zurück von der dann der Wert ausgelesen werden konnte, oder eben ein neuer Wert eingetragen werden konnte. Das ist bei einer Konstanten nicht so. Hier erhält man sofort den Wert der Konstanten.

  foo .
  5  ok

Worin liegt der Vorteil von Konstanten. Nun, nehmen wir das Beispiel von vorhin. Wir hatten geschrieben:

  NEUESWORT 12 + @

Was passiert, wenn man diese Wortdefinition auf einem 'alten' Forth aufruft, bei dem eine einfache Zahl nicht 32 sondern nur 16 Bit lang ist? Ganz einfach: Bei einem derartigen Forth beanspruchen die 5 Werte, die NEUESWORT enthält nur 5 mal 2, also 10 Byte. Mit 12 + erreichen wir also einen Speicherbereich, der nicht nur nicht mehr im Berech von NEUESWORT liegt, sondern darüber hinaus an einer Stelle, von der wir gar nicht wissen, was dort liegt. Solange wir da nur Werte auslesen wird unser Programm nicht mehr laufen. Noch kritischer wird es, wen wir versuchen dort einen neuen Wert abzulegen. Ein Systemabsturz kann die Folge sein.

Was wir eigentlich vorhatten, war ja, auf das vierte Element von NEUESWORT zuzugreifen. Wäre nun eine Konstante definiert, die angibt, wieviele Byte eine einfache Zahl beansprucht, sagen wir mal: CELL, dann hätten wir besser geschrieben:

  NEUESWORT CELL 3 * + @

Überträgt man das auf ein anderes Forth-System, dann können zwei Dinge passieren. Entweder es gibt das Wort CELL dort auch, dann brauchen wir uns nicht darum zu kömmern, wie lang eine Zelle in diesem System ist, die Konstante regelt das für uns. Oder aber, das Wort CELL existiert nicht. Nun, in dem Fall meckert Forth die obige Eingabe an und wir wissen auch, was wir zu tun haben.

Man sollte also immer dann mit Konstanten statt direkten Zahlen arbeiten, wenn man sicher stellen will, dass Programme möglichst portablel sein sollen. Außerdem haben Konstanten den Vorteil, dass sie Programme auch lesbarer machen.

Eine kleine Hilfe

Der Umgang mit Speicher ist in Forth sicherlich etwas ungewöhnlich, insbesondere dann, wenn man von einer 'modernen' Programmiersprache her kommt. Gerade Programmiersprachen wie C# verbergen gerade den Aspekt der Speicherverwaltung vor dem Programmierer. Meist existiert ein Befehl wie 'new', mit dem eine neue Instanz einer Klasse ins Leben gerufen wird und die Hintergründe, wie die dann entstandenen Objekte miteinander verbunden werden und wo im Speicher sie überhaupt liegen, werden für den Programmierer nicht transparent.

Es gibt sehr gute Gründe dafür, dass sich die modernen Programmiersprachen gerade in dieser Richtung entwickelt haben. Man kann ziemlich viel Unsinn anstellen, wenn man auf den Speicher direkten Zugriff hat. Versuchen sie mal ein:

  5 0 !

Wenn sie ein 'gutes' Forth haben, erhalten sie eine Fehlermeldung. Ein 'normales' Forth wird einfach sang- und klanglos abstürzen.

Man sollte also vermeiden, 'einfach so' im Speicher herumzuschreiben oder von dort zu lesen. Trotzdem wäre es ja ab und an mal ganz interessant zu sehen, was im Speicher so passiert. Manche Forth-Varianten bieten dazu das Wort DUMP, es ist aber nicht in allen Forth-Varianten verfügbar und daher soll es hier kurz vorgestellt werden und dann auch einmal programmiert werden. Es ist sehr nützlich.

Was ist dieses Wort DUMP und was macht es? Nun, wie der Name schon sagt, soll es 'dumpen'. Das ist ein ziemlich alter Begriff und er besagt, dass man sich den Speicher und seine Inhalte direkt ansehen will. Dazu hat man sich schon sehr früh auf ein bestimmtes Format geeinigt (Was mit früh gemeint ist und wie man mit Hexdumps umgeht, liest man am besten hier.)

Wie sieht ein solcher Hexdump aus, und wie wird er gelesen? Nun von Interesse ist natürlich die Adresse des Speichers, die man gerade sieht, dann der Inhalt. Dieser sollte einmal in 'Zahlform' und einmal in 'Zeichenform' vorliegen. Der Grund dafür ist, dass manchmal eben eher die Zeichen interessieren und manchmal eher die Zahlen.

Wie ich oben schon dargelegt habe ist es oft einfacher sich die Bytes in hexadezimaler Darstellung anzusehen, da so ein Byte immer in zwei Zeichen passt; darum heißen die Dumps auch 'Hex'dump. Aber uach für die Zeichen gibt es ein Überlegung. Der ASCII-Zeichensatz besteht aus druckbaren und nicht druckbaren Zeichen und alle Zeichen des ASCII-Zeichensatzes sind in 7 Bit kodiert, also den Werte nvon 0 bis 127. Ein Byte kann aber auch größere Werte bis 255 darstellen. Was macht man mit denen?

Hier soll nun Schritt für Schritt ein DUMP-Wort vorgestellt werden. Das ist vor allem für diejenigen gedacht, die kein solches Wort haben, aber auch als Übung für die Progrmmierung in Forth.

Der erste Schritt besteht darin, dass man nachdenkt. Unser Wort DUMP soll eine Adresse übergeben bekommen und dann eine Zahl, die angibt, wieviele Zeilen a 16 Byte von der Adresse an ausgegeben werden sollen. Es ist daher sinnvoll sich zunächst mal Gedanken über eine Dumpzeile zu machen.

Eine Dumpzeile soll zu Beginn die Adresse ausgeben, dann den Speicherinhalt ab da in Zahlenform und schließlich in Zeichenform. Da danach eventuell noch eine Zeile ausgegeben werden soll, sollte der Befehl, der eine Zeile ausgibt eine Adresse bekommen und auch eine Adresse ausgeben und zwar die, mit der dann sofort die nächste Zeile ausgegeben werden kann.

Der erste Teil der Zeile soll die Adresse ausgeben. Am besten wäre es, wenn dieses Wort die Anfangsadresse auch wieder zurück gibt, da dann damit sofort auch die Zahlenform ausgegeben werden kann. Ein solches Wort zu schreiben ist einfach:

  : DUMP-ADR ( addr -- addr )
    HEX
    DUP
    0
    <# # # # # # # # # #> TYPE ."  | "
    DECIMAL ;

Ich denke große Teile des Wortes sind selbsterklärend. Nachdem auf die hexadezimale Zahlenbasis umgeschaltet wurde, wird die Adresse verdoppelt, damit man sie nach Ablauf des Wortes noch zur Verfügung hat und dann eine 0 dazu geschrieben, da die Wörter <#, # und #> eine doppelte Zahl erwarten und Adressen einfache Zahlen sind.

Dann werden 8 Stellen der Adresse ausgegeben. Das liegt daran, dass mein Forth ein 32 Bit Forth ist. 32 Bit sind 4 Byte, von denen jedes zwei Stellen benötigt. Bei anderen Forthsystemen sollte man das anpassen. Hinter der Adressausgabe kommt noch ein Trennzeichen, um die Adresse von der Zahldarstellung zu trennen.

Nun gehts an die Zahldarstellung. Aus Gründen, die in einem späteren Kapitel noch ausführlicher besprochen werden, ist es gute Forth-Praxis möglichst kleine Worte zu schreiben. Daher sollte es erst mal ein Wort geben, das ein Byte ausgibt. Gefolgt werden soll es von einem Leerzeichen und die Adresse soll, um Eins erhöht, weiter gegeben werden, damit sofort das nächste Byte ausgegeben werden kann. Auch das ist kein Problem:

  : 1DUMPBYTE ( addr -- addr )
    HEX
    DUP C@
    0 <# # # #> TYPE SPACE
    1+
    DECIMAL ;

Hier sieht man auch, wie das Wort C@ mal im Einsatz gebraucht wird.

Nun sollen vier Bytes hintereinander ausgegeben werden und danach ein zusätzliches Leerzeichen, um die ganze Sache ein bisschenstrukturierter zu machen.

  : 4DUMPBYTE ( addr -- addr )
    1DUMPBYTE 1DUMPBYTE 1DUMPBYTE 1DUMPBYTE
    SPACE ;

Nun können alle 16 Bytes ausgegeben werden. Danach sollte wieder die Anfangsadresse liegen.

  : 16DUMPBYTE
    DUP
    4DUMPBYTE 4DUMPBYTE 4DUMPBYTE 4DUMPBYTE
    DROP ;

Das letzte DROP dient dazu die erhöhte Adresse zu 'vergessen'. Auf dem Stack liegt die Anfangsadresse für die Zeichenausgabe.

Ich will die Sache einfach halten. Mit einem 127 AND kann man die unteren sieben Bit 'maskieren', das oberste Bit wird dadurch einfach gelöscht. Damit werden zwar Bytes, die gar kein Zeichen darstellen sollen auch wieder zu ASCII-Zeichen, aber das stört nicht. Kritischer ist ein anderes Problem. ASCII-Zeichen, die kleiner als 32 sind und das Zeichen mit der Nummer 127 sind nicht darstellbar. Für diese Zeichen soll ein Punkt ausgegeben werden und ansonsten das Zeichen selber. Die Zeichenausgabe soll wieder eine Adresse übergeben bekommen und die um eins erhöhte Adresse zurück geben.

  : DUMPZEICHEN (addr -- addr )
    DUP
    C@ 127 AND
    DUP 32 < SWAP DUP 127 = ROT OR
    IF 46 EMIT DROP ELSE EMIT THEN
    1+ ;

Damit können nun 16 Zeichen und dann ein Zeilenvorschub ausgegeben werden.

  : 16DUMPZEICHEN (addr -- addr )
    16 0 DO DUMPZEICHEN LOOP CR ;

Nun haben wir alles zusammen, was eine Dumpzeile ausmacht und brauchen es nur noch zu einem Wort zusammen zu setzen:

  : DUMPZEILE ( addr -- addr )
    DUMP-ADR 16DUMPBYTE 16DUMPZEICHEN ;

Und schon ist das DUMP-Wort fertig:

  : DUMP ( addr n -- )
    CR
    0 DO DUMPZEILE LOOP DROP ;

Spielen wir mal mit unserem neuen Wort ein bisschen rum. Sie erinnern sich, dass wir eine Konstante 'foo' definiert hatten. Mal sehen, ob wir sie wieder finden. Das Wort ', das dazu verwendet wird, wird auch später noch erklärt. Hier verwenden wir es einfach mal:

  ' foo 5 DUMP
  B71CAAE0 | A1 BB 04 08  00 00 00 00  05 00 00 00  FF 8B FF 82 !;..............
  B71CAAF0 | FC 00 3F 0B  BF F8 BF 2F  C2 FC 2F F8  2F FF FC 2F |.?.?x?/B|/x/.|/
  B71CAB00 | C2 FC 2F FF  FF FF FF CB  F7 8B F7 8B  FF CB FE 2B B|/....Kw.w..K~+
  B71CAB10 | F2 FE 2F F8  BF 82 FF 01  FC 2F F0 BF  C3 0B FF F7 r~/x?...|/p?C..w
  B71CAB20 | 0B FF 0B BC  2E F0 BB BC  2F F8 BF FF  C2 F0 BF F0 ...<.p;<.x?.Bp?p
   ok

Tja, das sieht schon ziemlich interessant aus, auch wenn die Adressen an der linken Seite bei ihnen sicherlich andere sein dürften. Aber wo ist unser 'foo'? Nun wir sind dahinter gelandet. Wenn man den Aufruf etwas ändert, dann sieht das Bild folgendermaßen aus:

  ' foo 16 - 5 DUMP 
  B71CAAD0 | 05 00 00 00  B8 AA 1C B7  03 00 00 80  66 6F 6F 20 ....8*.7....foo 
  B71CAAE0 | A1 BB 04 08  00 00 00 00  05 00 00 00  FF 8B FF 82 !;..............
  B71CAAF0 | FC 00 3F 0B  BF F8 BF 2F  C2 FC 2F F8  2F FF FC 2F |.?.?x?/B|/x/.|/
  B71CAB00 | C2 FC 2F FF  FF FF FF CB  F7 8B F7 8B  FF CB FE 2B B|/....Kw.w..K~+
  B71CAB10 | F2 FE 2F F8  BF 82 FF 01  FC 2F F0 BF  C3 0B FF F7 r~/x?...|/p?C..w
   ok

Nun sieht die Sache anders aus. Das 'foo' taucht auf und zwar in der ersten Zeile ganz rechts. Allerdings steht noch ein Leerzeichen dahinter (hexadezimal = 20). Dann folgt die Zeile, die wir schon kennen. Und da taucht auch in der Mitte, beim 9ten Byte die '5' auf, für die unsere Konstante steht.

Auch wenn sie hier sicher noch nicht verstehen können, was die ganzen anderen Bytes bedeuten, vertrauen sie mir, das kommt noch und ich denke, dass sie schon mal einen kleinen Überblick bekommen haben, wie man mit dem Speicher in Forth umgeht.