SuperCPU durchleuchtet - Folge 6
Im vorigen Kursteil haben wir neben ein paar interessanten neuen Befehlen
auch einige neue Adressierungsarten kennengelernt. Doch gleich kommt's ganz
dick: Wir richten unser Augenmerk auf die Stack-relative Adressierung! Was
das sein soll und wie man sie benutzt, werdet Ihr nun erfahren...

Kurz und knapp auf den Punkt gebracht: Die Stack-relative und die
Stack-relative indirekt indizierte (was fuer ein Wort) behandeln den Stack
wie ein Feld, das indiziert wird. Wie in Basic bei einem mit DIM erzeugten
Feld wird nun der Stack behandelt! Der Stack-Pointer wird dabei als
Startadresse des Feldes genommen. Der aber zeigt immer auf die naechste FREIE
Adresse auf dem Stack, wodurch ein Index von Null sinnlos waere. Daten und
Adressen, die auf den Stack gepusht worden sind, lassen sich somit ab Index 1
ansprechen. Wie das alles gehen soll, kommt gleich - im Moment reicht es
erstmal, wenn Ihr es Euch wie ein Feld in Basic vorstellt. Die Stack-relative
Adressierung erlaubt nun, auf irgendein Byte, das auf den Stack geschoben
wurde, zuzugreifen. Doch der Hammer ist die Stack-relative indirekte
Adressierung: Damit laesst sich auf eine Adresse zugreifen, die auf dem Stack
liegt! Im Klartext: Man schiebt mit PHA beispielsweise eine Adresse (16 Bit)
auf den Stack, und auf den Inhalt dieser Adresse kann man dann ueber die
Stack-relative indirekte Adressierung zugreifen - mit anderen Worten wie auf
eine Adresse, die als Zeiger in der Zero(Direct)Page steht. Beide neuen
Adressierungsarten erlauben es, Parameter sauber und effizient ueber den
Stack zu uebergeben. Die Entwickler des 65816 dachten dabei sicher auch an
C-Compiler - diese Sprache benutzt wie viele andere den Stack zur
Parameteruebergabe an Subroutines. Doch noch viel wichtiger ist: Mit dieser
Adressierung laesst sich Code schreiben, der ueberall im Speicher laeuft und
auf Daten zugreifen kann, die an beliebiger Stelle im Speicher liegen.


Relativitaetstheorie

Diese neuen Adressierungsarten gehoeren sicherlich zu den
interessantesten des 65816, andererseits muss man sich ihren Einsatz
genau ueberlegen, damit er effektiv ist. Auf alle Faelle kann man durch
diese Adressierungsart nun auf beliebige Daten, die auf den Stack
gepusht wurden, zugreifen - und nicht, wie sonst ueblich, nur auf den
zuletzt dort abgelegten Wert! 

Gehen wir erstmal auf die Stack-relative Adressierung (ohne
indirekt-indiziert) ein. Man kommt damit an jedes der letzten $FF gestackten
Bytes (es koennen beim 65816 ja mehr als 255 Bytes auf dem Stapel landen, da
der Stackpointer 16 Bit hat, wie wir in einem vorigen Kursteil gesehen
haben). Befehle der Stack-relativen Adressierung sind zwei Byte lang. Nach
dem Opcode kommt nur ein Byte, und das ist der Index in den Stack. Wie gesagt
wird der Stack-Pointer als Basis fuer dieses "Feld" benutzt. Der Index wird
zum Stack-Pointer dazugezaehlt, und schon hat die CPU die Adresse innerhalb
des Stacks heraus, auf die zugegriffen wird.
 
Dies kann speziell dann nuetzlich sein, wenn ein Teil eines Programms Daten
an einen anderen Teil senden will. Natuerlich kann man diese Daten irgendwo
speichern, und die andere Routine muss sie sich von dort abholen. Die Methode
ueber den Stack ist aber etwas flexibler. Angenommen, eine
Multiplikationsroutine erwartet zwei 16 Bit-Werte. Irgendein Teil des
Hauptprogramms pusht diese auf den Stack. Spaeter holt sich dann die
Multiplikationsroutine einen Wert vom Stack, laesst dabei aber den anderen
Wert sowie den Stack-Pointer voellig unberuehrt:

LDA 3,S ; ersten Operand holen

oder

LDA 1,S ; zweiten Operand holen

Wie man hier deutlich sieht, wird auf den zuletzt gepushten Wert mit dem
Index 1 zugegriffen, nicht etwa 0. Der Stack-Pointer zeigt ja, wie erwaehnt,
immer auf das naechste freie Byte auf dem Stack, dessen Adresse genau eine
unter dem zuletzt auf den Stack geschobenen Byte liegt. Ein Index von 0 waere
also deshalb sinnlos, es sei denn man will auf das letzte Byte zugreifen, das
mit einem Pull-Befehl bereits geholt wurde! Das waere allerdings nicht gerade
besonders stabil, da ja zwischendurch ein Interrupt aufgetreten sein kann,
der laengst das Byte mit eigenen Werten ueberschrieben hat.

Indirekter Zugriff

Waehrend die Stack-relative Adressierung dazu da ist, direkt auf Daten auf
dem Stack zuzugreifen, kann man mit der Stack-relativen indirekt-indizierten
Adressierung auf Daten zugreifen, die sich an ADRESSEN befinden, welche auf
dem Stack liegen! (Diesen Satz bitte nochmals lesen und gruendlich verdauen!)
Nehmen wir unser Beispiel von eben und aendern es etwas ab: Nun nehmen wir
an, dass nicht die zu multiplizierenden Zahlen, sondern deren Adressen (also
wo sie im Speicher stehen) auf den Stack gepusht wurden! Wie folgende Routine
zeigt, koennen beide Werte ueber die auf dem Stack befindliche Adresse
indirekt geladen werden:

LDY #0
LDA (1,S),Y ; ersten 16-Bit Multiplikationsoperand holen 
TAX         ; und in X merken
LDY #2
LDA (1,S),Y ; zweiten 16-Bit Multiplikationsoperand holen

Die 1,S ergibt die Adresse des zuletzt gepushten Wertes, also des
Pointers auf den zweiten Multiplikations-Wert. (Genauer sogar: 1,S zeigt auf
das Lowbyte der Adresse, das Highbyte befindet sich bei 2,S, also auf der
naechst hoeheren Stack-Adresse.) Zu dieser indirekten Adresse wird nun das
Y-Register addiert, die indirekte Adresse + 0 enthaelt den ersten, die
indirekte Adresse + 2 enthaelt den zweiten Wert. Und mit dem Inhalt dieser
indirekt-indizierten Adresse wird der Akku geladen. Tja, das ist es - und
hoert sich wie immer komplizierter an als es ist.

Bekannter Verwandter

Um den eventuellen Nebel etwas zu lichten: Diese Art der Adressierung ist,
wie schon kurz angesprochen, verwandt mit der uns allen bekannten indirekten
Adressierung ueber die Zeropage. Dort steht z.B. eine Adresse in $FE/$FF, auf
die man dann noch mit ,Y zugreifen kann. Nichts anderes passiert hier, nur
dass sich der Ort des Zeigers nicht immer an der selben Stelle (feste Adresse
in der Zeropage) befindet, sondern eben auf dem Stack, der als Feld gesehen
wird, dass an der Adresse des Stackpointers beginnt. Bei beiden
Adressierungsarten wird ein Index zu einem Pointer addiert, und dieser muss
bei beiden im Y-Register stehen.

Das ist nicht alles

Um diese neuen Adressierungsarten auch gut nutzen zu koennen, implementierten
die Entwickler des Prozessors in der SuperCPU einige Befehle, um speziell
Adressen auf den Stack zu schieben. Es sind dies die Befehle PEA, PEI und
PER, die wir in dem Kursteil, in dem es um verschiedene Stack-Befehle ging,
schon kurz angesprochen haben - vielleicht hat sich manch einer gefragt, was
diese Befehle ueberhaupt sollen. Nun wird es Zeit, Licht in die Sache zu
bringen. Nebenbei sind die einzigen Befehle, die Daten direkt von der einen
zu einer anderen Speicherstelle bringen koennen, ohne dabei irgendein
Register zu benutzen!

Der PEA ist der einfachste von den drei neuen Push-Befehlen. Der Operand ist
ein 16 Bit-Wert, wobei es sich um eine Adresse oder um einen Daten-Wert
handeln kann. Das ist dem Befehl ganz egal, er bringt diese 16 Bit auf jeden
Fall auf den Stack. Ein Beispiel:

PEA $2134; pusht $2134 auf den Stack

Dieser Befehl schiebt $2134 auf den Stack, dabei kann es sich um die Adresse
$2134 handeln, aber es kann auch der Wert $2134 sein, es ist dem Coder
ueberlassen, wie er das interpretiert. Will man mit Werten arbeiten, ist aber
zu beachten, dass hier kein "#"-Zeichen, wie sonst bei Konstanten, verwendet
wird! Der Wert, der auf den Stack gebracht wird, ist uebrigens immer 16 Bit,
egal ob das M- oder X-Flag nun gerade auf 8 oder 16 Bit steht.

Interessanter wird es beim PEI - das steht fuer Push effective indirect
address. Der Operand ist eine Adresse in der Direct(Zero)Page. Der 16
Bit-Wert, der an dieser Adresse steht, landet auf dem Stack. Damit kann man
entweder einen Zeiger auf den Stack schieben, oder einen 16 Bit-Wert, der in
der Zeropage liegt. Haben wir beispielsweise den Wert oder die Adresse $5678
ab $FE/$FF abgelegt, dann wuerde ein

PEI ($FE); pusht zwei Bytes die ab $FE in der Direct Page liegen

die $5678 aus der Zeropage holen und auf dem Stack ablegen. Genau wie PEA
pusht auch PEI immer einen 16 Bit-Wert, egal ob der Akku oder die
Index-Register gerade 8 oder 16 Bit gestellt sind.

Noch relativer

Der PER-Befehl (Push effective relative) schiebt eine zum Program-Counter
relative Adresse auf den Stack. Dies ist enorm hilfreich beim Schreiben von
relokatierbarem Code. Der Operand, dem man dem Assembler mitteilt, ist
irgendeine Stelle im Programm, zum Beispiel ein Daten-Bereich. Der Operand,
den der Assembler generiert ist ein 16 Bit-relativer Wert: die Differenz
zwischen der Adresse des naechsten Befehls und der Adresse, die als Operand
angegeben wurde. Wird der Befehl nun ausgefuehrt, wird dieser Wert zu der
Adresse des naechsten Befehls hinzuaddiert - dies geschieht zur Laufzeit!
Dadurch ergibt sich auf jeden Fall die Adresse, an der die Daten stehen,
egal, wohin das Programm gerade geladen wurde bzw. wo es gerade liegt. Und
genau diese Adresse wird auf den Stack geschoben, wo man dann mit der
Stack-relativen indirekt-indizierten Adressierung darauf zugreifen kann.
Liegt die Adresse vor dem PER-Befehl, generiert der Assembler eine sehr
grosse 16 Bit-Zahl, die beim Addieren zum Program-Counter zu einem Ueberlauf
ueber $FFFF fuehrt und somit dann automatisch wieder zur richtigen Adresse
fuehrt. Der PER-Befehl arbeitet aehnlich dem BRL (Branch long), die Adresse,
die man dem mitgibt, ist auch eine Adresse irgendwo im Programm, der
Assembler bildet auch hier einen 16 Bit-Wert, der dem Abstand zu dieser
Adresse entspricht, und wenn der Befehl ausgefuehrt wird, wird dieser Wert
zum Program-Counter hinzuaddiert, wodurch sich die Adresse ergibt, wo das
Programm weiterlaeuft.

Um zu verstehen, wie man den PER-Befehl zusammen mit relativen Spruengen
benutzt, schauen wir uns eine kleine Routine (Listing 6.1) an, die an JEDER
beliebigen Adresse laufen kann. Angenommen, wir assemblieren sie nach $C000.
Irgendwo dort liegt dann auch die Adresse des Wertes DATA0. Das Programm will
nun auf diesen Wert zugreifen. Klar, ein simpler LDA DATA0 wuerde
funktionieren - aber nur dann, wenn das Programm auch wirklich bei $C000
liegt. Wenn die Routine aber relokatierbar sein soll, muss man sie
beispielsweise auch nach $8000 laden koennen. Somit wuerde der LDA DATA0 eine
voellig falsche Information in den Akku holen. Stattdessen verwenden wir nun
einen PER DATA0. Der Assembler rechnet den Abstand zwischen dem Befehl und
der Adresse von DATA0 aus. Wird das Programm spaeter abgearbeitet, zaehlt der
65816, wenn er auf den PER-Befehl trifft, den Abstand wieder dazu - und kommt
somit auf die Adresse von DATA0, egal, wohin das Programm geladen wurde!
Diese Adresse wird auf den Stack gebracht. Nun kann man mit der Stack-relativ
indirekt-indizierten Adressierung auf den Wert in DATA0 zugreifen. Und von da
aus geht's dann auch weiter: Ist die Adresse von DATA0 naemlich erstmal auf
dem Stack, kann man durch hochzaehlen des Y-Registers dann auch auf DATA1,
DATA2 und DATA3 zugreifen! In Listing 6.2 seht Ihr ein weiteres Beispiel, wie
man mit PER relocateable Code schreiben kann, hier wird sogar nur die
Stack-relative Adressierung gebraucht. Beide Routinen koennen nach dem
Assemblieren ueberall im Speicher ablaufen -viel Spass noch beim
Experimentieren!


(w) Malte Mundt
©1999 Go64 Redax! & Count Zero/SCS*TRC for all HTML Stuff

[ Zum 5. Teil ][ Zum Index ][ Zum 7. Teil ]