Endlich! Der Stack ... ("Programmieren" lernen)

  • Dieses Thema hätte man auch gleich am Anfang angehen können; man hätte können.

    Vielleicht schadet es aber auch nicht, daß nach den bisherigen Teilen evtl. schon ein wenig Verständnis für das Thema Zahlen, Register, Adressen und v.a. Hoch- und Runterzählen vorhanden ist. Es ist nämlich ziemlich theoretisch und erfordert ein bißchen Vorstellungskraft.


    Die Überschrift ist übrigens doppeldeutig gemeint. Ein Stack hat eine herausragende Eigenschaft - er ist endlich !


    Das Häßliche dabei ist nur, daß man oft nicht genau weiß, wie endlich, weil man es i.a. einfach drauf ankommen läßt und davon ausgeht, daß noch genug Platz darin vorhanden ist. Und wenn doch nicht - crasht die Maschine.



    Stack - Stapelspeicher - Kellerspeicher , das meint alles das Gleiche.

    Es ist letztlich einfach ein reserviertes Stück Speicher. In dieses kann man hineinschreiben oder daraus lesen, wie bei jeder normalen RAM Adresse sonst auch. Aber beim Stack gibt es eine Besonderheit: er wird verwaltet !

    Das bedeutet, daß eine besondere Struktur exisitert, die genau weiß, an welcher Stelle der Lese-/Schreibzugriff erfolgen darf. Das ist der Stackpointer.


    Man kann sich den Stackpointer wie eine Grenze oder Schwelle vorstellen, auf einer Seite davon befinden sich bereits gespeicherte Daten, auf der anderen Seite davon liegt freies RAM.


    Möchte man etwas in den Stack schreiben, dann teilt man das mit einem Befehl mit. Nun wird der neue Inhalt in die freie RAM Stelle geschrieben, die durch den Stackpointer angezeigt wird. Sofort anschließend wird der Stackpointer so verändert, daß er wieder auf eine freie Position zeigt. Er wird also um eine Position in den Bereich des freien RAMs weitergerückt. Die "Grenze" ändert ihre Position und das freie RAM ist um einen Speicherplatz kleiner geworden.


    Das schöne dabei - das passiert automatisch. Man muß also nur mitteilen: Bitte Speichern!, an welcher Stelle das genau geschieht, darum kümmert sich der Stack quasi selbst.


    Beim Laden ist der Ablauf ähnlich - nur halt genau andersherum. Der Stackpointer wird auf die direkt neben ihm liegende Stelle gesetzt, von der bekannt ist, daß in dieser Richtung die bereits gespeicherten Daten liegen. Dann wird von dort der Inhalt gelesen. Anschließend steht der Inhalt zwar meist noch dort im RAM drin (es sei denn der Wert wird noch zusätzlich gelöscht), aber weil der Stackpointer ja nun auf diese Adresse zeigt, ist sie nun als "frei" bzw. "wieder benutzbar" markiert.


    Wenn man etwas in den Stack hineinschreibt, verschiebt sich die Grenze in die eine Richtung, beim Lesen in die andere.



    Problematisch sind nun die äußeren Begrenzungen des Bereiches, den der Stackpointer verwaltet. Wenn der Stapelspeicher bereits komplett gefüllt ist, müßte der Stackpointer ja eigentlich am Platz verharren und einen Fehler mitteilen - nämlich, daß alles voll ist. Zumindest im Homecomputerbereich passiert das aber meist nicht. Stattdessen wird der Stackpointer auf die nächstmögliche Stelle gesetzt und das ist dann der Wert, wo er eigentlich ansagt, daß der gesamte Stack frei ist.

    Das ist genauso wie wenn ein Register die $00 nach unten überschreitet und dann im nächsten Schritt (dekrementieren) bei $FF landet.


    In die andere Richtung an der oberen Grenze zeigt der Stackpointer z.B. auf die $FF und wird bei der nächsten Erhöhung auf $00 gesetzt, womit auch wieder die komplette Verwaltung in sich zusammenbricht, weil nun der eigentlich freie Speicher zum bereits gefüllten erklärt worden ist.


    (Das ließe sich natürlich auch vorher abfragen und ausschließen, aber das wird i.a. nicht gemacht. Wenn es passiert gibt das allerschönsten Datenmüll, insbesondere wenn man z.B. zweimal über den kompletten Bereich Daten hineinschreibt. Man findet in dem Fall nämlich nur noch die zweite Hälfte wieder.)


    Trotzdem ist es ein schöner angenehmer Zwischenspeicher, gerade weil man sich beim Speichern und Laden wenig Gedanken machen muß.



    Eine Sache muß man trotzdem beachten: Es wird ja immer der Wert genau an die Stelle des Stackpointers geschrieben, und der rückt danach eins weiter. Beim Lesen geht das andersherum - dies bedeutet aber, daß dieser letzte Wert als erster wiedergefunden wird.

    Das ist bei Einzelwerten völlig unproblematisch. Die legt man ab und holt sie wieder.


    Aber (!) : Wenn man mehrere Werte hintereinander ablegt, die logisch irgendwie zusammengehören, dann wird der zuletzt abgelegte als erster wieder herausgeholt. Anschließend der, der direkt vor ihm gespeichert wurde und erst ganz zum Schluß der erste Wert aus dieser Reihe.

    Dafür hat sich jemand den schönen Namen LIFO ausgedacht - Last IN First OUT !

    (auf deutsch: Die Ersten werden die Letzten sein. (na ja, zumindest so ähnlich) )

    Von dieser Eigenschaft hat er auch seinen Namen - Stapel.

    Wie bei einem Stapel Hölzer oder Papier oder Bleiplatten, kann man ein weiteres Objekt oben auflegen, aber man kann die Einzelobjekte ohne weiteren Aufwand (wie Umdrehen des gesamten Stapels) nur in umgekehrter Reihenfolge dort einzeln wieder herunternehmen, um ihn abzubauen.



    Es ist übrigens gar nicht gesagt, daß ein Stapel immer nach oben wachsen muß. Im RAM ist das nämlich sogar eigentlich ziemlich egal - es geht ja um Information und nicht um Bleiplatten. Oft wird daher sogar bei einer hohen Adresse begonnen und dann der Stackpointer zu kleineren Adressen bewegt, wenn etwas gespeichert wird. Dementsprechend wird dabei der Stackpointer selbst kleiner. Er wird in so einem Fall einfach zur untersten zulässigen Adresse addiert und diese Ergebnisadresse ist halt die erste freie (und oberhalb des Stackpointers liegen dann die bereits gespeicherten Daten).


    Beispiel:

    $1000 + $56 (Stackpointer) bei 8Bit ($FF) bedeutet

    oberhalb von $1056 (d.h. ab $1057) bis zur maximalen Obergrenze bei $10FF liegen bereits Daten

    speichert man nun etwas ab, kommt das nach $1056 und der Stackpointer wird um -1 auf $1055 verringert

    es ist somit die Grenze nach unten gewandert und nun weniger Platz da

    der Stapel "wächst" quasi (wie ein Stalaktit) von oben ($10FF) nach unten ($1000)

    und so eine Art von Stapelspeicher nennt man dann auch einen Kellerspeicher



    Es ist aber eben auch nicht gesagt, daß der Stapel im Adreßbereich nach unten wachsen muß.

    Es ist ebenfalls nicht gesagt, ob der Stackpointer nun auf das erste freie Element zu zeigen hat, oder auf des letzte bereits gespeicherte.


    Wie das genau geregelt ist, kann man nur in der Anleitung zum "jeweiligen Stack" erfahren, d.h. i.a. im Handbuch zur Maschine.



    Übrigens: Wenn der Stackpointer auf das letzte gespeicherte Element zeigt, wäre es verständlicherweise überaus sinnvoll, ihn zu verändern BEVOR eine Schreiboperation geschieht (sonst würde man das Element ja überschreiben). Beim Lesen dagegen würde man einen solchen Stackpointer dagegen erst NACH dem Lesen verändert haben wollen.

    Dieses Verhalten wäre also genau andersherum als bei einem Stackpointer, der auf die nächste freie Stelle zeigt.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Die Befehle zum Stack waren schonmal "angesagt" worden

    PHA - speichert den Akku im Stack (push accu)

    PLA - lädt den Akku mit dem obersten zugänglichen Stackwert (pull accu)


    Daneben kann man auch noch das Prozessorstatusregister - also die Stelle, wo die Flags sind - auf den Stack bringen

    PHP - speichert die gerade aktuellen Flags im Stack

    PLP - lädt die Flags zurück


    Der Stackpointer war schon öfters zu sehen - immer dann, wenn die Registerzeile angezeigt wurde.

    Dort steht in der letzten Spalte: SP - Stackpointer und darunter der aktuelle Wert.


    Das Programm macht nun zwei Sachen

    G5000 - lädt jedes Zeichen der ersten Bildschirmzeile und sichert es im Stack

    G500C - holt die Zeichen aus dem Stack und schreibt sie bei Spalte 1 beginned in die erste Zeile




    Mit R zeigt man die Register VOR dem Programmlauf an. SP ist bei $F8.

    Mit G5000 wird Bildschirmzeile eins Zeichen für Zeichen gelesen und jedes in den Stack geschrieben (PHA).

    Nach dem BREAK sieht man: Der SP steht jetzt plötzlich bei $D0 - das sind exakt die $28 Werte weniger, die die erste Zeile umfaßt



    Mit G500C werden die Zeichen zurückgeholt. Und nun kann man schön den Effekt vom LIFO sehen - oberste Zeile.

    Außerdem ist SP wieder auf dem Ausgangswert von $F8 angekommen, d.h. es sind alle Daten wieder ausgelesen worden, die vorher hineinkamen.



    Start man jetzt 2x G5000 wird zuerst wieder das gleiche passieren, wie beim allerersten Aufruf. SP sinkt auf $D0. Und dann nochmal -$28 herunter auf $A8.


    Frage: Was passiert in der ersten Zeile, wenn man jetzt mit G500C zweimal die Zeichen zurückliest ? Wie wird der Text dargestellt ? Beim ersten Mal ? und wie beim zweiten Mal ?




    Noch eine weitere Veranschaulichung.

    Die Zeile 1 wurde in den Stack gelesen.



    Da hier der SP am Anfang bei $F9 lag, landet man nach dem Speichern bei SP $D1.

    Nun liegt beim C16 - und wohl allen Rechnern mit 6502 - der Stack selbst bei $0100 bis $01FF.

    Mit

    M 01D0 01FF

    zeigt man sich also hier den obersten Stackbereich direkt an, inkl. dem gespeicherten Text.

    Dazu gibt es zum Vergleich die Zeile 1 (oben als Text) und ihren Bildschirmspeicher beginnend bei $0C00.


    Man versuche in dieser Bytewüste die einzelnen Zeichen wiederzuerkennen - insbesondere aber ihre Abfolge im Speicher nachzuvollziehen - die Zahlen und das "!" in der Randleiste beiten dafür gute Anhaltspunkte. Wichtig sind die Bytewerte, die man zwischen Bildschirmspeicher und Stack-Dump zur Übung mal zuordnen, gleiche finden, sollte.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Wie zuvor schon angedeutet, ist eine der häufigsten Stackanwendungen das kurzfristige Sichern von Registern, oft im Rahmen von Subroutinen.


    Eine hilfreiche kleine Subroutine wird hier gezeigt. Man kann sie bei Bedarf einbinden, wenn man Programme hat, die sonst nur per Reset unterbrechbar wären.

    Abgefragt wird die Adresse $C6, in der die Codes der gerade gedrückten Taste angezeigt werden, und diese auf das ESC Key abgefragt. Ist ESC gedrückt springt die Routine auf ein BRK und verläßt damit sofort das Programm ($C6 ist evtl. auf anderen Geräten eine andere Adresse (PET,VC20), ich bin mir aber ziemlich sicher, daß es so eine Speicherstelle auch da gibt; bitte in Doku nachschlagen).



    Man sieht, daß der Akku auf dem Stack zwischengespeichert wird, vor dem Test auf ESC, anschließend wird er von dort wieder hergestellt.

    S "SUBEXITKEY",8,6040,604A

    zum Abspeichern für eine Wiederverwendung

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries

  • =6502=


    Das Gleiche kann nun auch bei der Subroutine SUBWAIT gemacht werden.

    Damit wird das Speichern auf fixe Adressen ($4000,$4001) nun abgelöst durch die flexiblere Lösung auf dem Stack.


    Der Vorteil ist sicher direkt einsichtig: Man muß die beiden Adressen nicht mehr reserviert halten und nicht "Buch führen" über Speicherstellen.


    (Allerdings: Mit einem "echten" Assembler wird das Problem einfach dadurch aufgelöst, daß man Datenplätzen Namen zuweisen kann und sich so das Problem "entschärft". Oft wird dann sogar wieder die Adresse bemüht, da das Programm sogar (im Quellcode(!)) lesbarer wird.)




    S "SUBWAIT2",8,6000,60019

    zum Abspeichern


    Den Akku mit in den Stack zu legen, wäre momentan hier gar nicht nötig, da er in der Subroutine nicht benutzt wird.



    Es erscheint zudem die vielleicht "interessanteste" Befehlskonstruktion des 6502,

    der Befehl für No Operation: NOP


    Hier auch gleich in einer üblichen Nutzung als "Füllmittel" für Stellen, wo man Code verändert hat und noch Speicherstellen übrigblieben, weil der vorherige Code länger war als der ihn ersetzende. In der vorigen Version dieser Subroutine waren die Bytes $6000 bis $6005, d.h. 6 Speicherstellen, mit 2 Speicherbefehlen belegt. Die neue Version benötigt nur noch 5 Bytes und das sechste wird einfach mit NOP aufgefüllt.

    (Es wäre hier auch völlig problemlos möglich, die beiden verschachtelten Schleifen komplett um eine Speicherstelle nach vorn zu verschieben. Das geht aber nicht immer, etwa wenn JMPs mit im Spiel sind.)


    NOP hat auch noch eine wichtige Anwendung, wenn man eine sehr kurze Wartezeit benötigt, d.h. kürzeste Zeitabschnitte warten will, etwa um bestimmte Dinge synchron ablaufen zu lassen. So lassen sich bei Nutzung von Rasterzeileninterrupts Tabellen mit NOPs anlegen, die man benutzt um die Routine mit dem Ergebnis am Bildschirm abzustimmen.


    Dieses Kommando existiert auf anderen Plattformen teils überhaupt nicht.

    Dort kann man sich dann mit einem "sinnlosen" Ladebefehl o.ä. behelfen, am ARM würde man z.B. schreiben MOV R0,R0 also das Register R0 nach R0 speichern.

    -- 1982 gab es keinen Raspberry Pi , aber Pi und Raspberries