Zugegeben, das Thema des Funktionsaufrufes ist nicht gerade einfach zu erklären, trotzdem ist es im Grunde sehr simpel aber auch sehr verwirrend. Am besten hilft wohl, sich verschiedenfarbige Papierchen bereitzulegen, die die verschiedenen Register darstellen sollen und ein anderes grosses Blatt Häuschenpapier, welches den Stack darstellen soll. Und dann beginnt man, diese Papierchen zu beschriften und dann herumzuschieben. Kleiner Tipp: Post-It-Zettelchen an Fenster (die müssten sowieso wiedermal geputzt werden!). Im folgenden werden die Worte Funktion, Routine und Prozedur gebraucht, sie bedeuten hier jedoch stets dasselbe. Caller steht für die aufrufende Funktion und Callee für die aufgerufene Funktion.
Um zwischen Funktionen wechseln zu können, gibt es sogenannte Stacks, auf denen hierarchisch alle Informationen aller höhergestellten Funktionen abgespeichert werden. Stacks werden von oben nach unten gefüllt, also von hohen zu tiefen Adressen. Normalerweise redet man nur von einem einzigen Stack, der bei einer sehr hohen Adresse beginnt, um die tief unten liegenden Programme nicht zu überlappen. Der Stackpointer %sp ist gespeichert in %o6 und zeigt stets auf die letzte und somit tiefste Stelle, an der der Stack einen gültigen Eintrag hat (Und nicht etwa die nächste unbenutzte Stelle). Der Framepointer %fp ist gespeichert in %i6 und zeigt auf die unterste Speicherstelle, die noch dem Caller gehört (Und nicht etwa die höchste des Callee).
Jede Funktion hat das Recht, Speicherplatz im Stack anzufordern. Zusätzlichen Speicherplatz für den Stack erhält man mit folgendem Befehl:
add %sp, -(Anzahl Bytes), %sp
Der Stackpointer ist immer 8-aligniert (für Doublewords). Um sicherzugehen, dass dies auch immer so ist, gibt man anstelle der obigen, einfach die folgende Zeile ein:
add %sp, -(Anzahl Bytes) & -8, %sp
Grundsätzlich muss man nach jeder solchen add-Operation später mittels einer genau gleichen sub-Operation den Speicher wieder freigeben, ansonsten würde der Stack nicht mehr korrekt ausgelesen werden können. Diese add-Anweisung wird jedoch für Funktionsaufrufe grundsätzlich nicht gebraucht, sie dient nur der kurzzeitigen Anforderung von zusätzlichem Speicherplatz für lokale Variablen. Eine Anweisung, die man für die Funktionsaufrufe benötigt, ist die save-Anweisung.
Eine kleine Anmerkung im Voraus zum save-Befehl und Registerspeichern, welche im Skript für Verwirrung sorgen kann: Ein save-Befehl selbst speichert NICHT die Register %i und %l auf dem Stack. Dies erledigt der Window-Overflow-Trap (siehe unten). Wenn im weiteren Text davon geredet wird, dass die %i- und %l-Register *gesichert* werden, so bedeutet dies nur, dass mittels der Overflow-Routine (auf momentan noch mystische Weise) gewährleistet wird, dass bei einer späteren Zurückkehr mittels dem restore-Befehl wieder die ursprünglichen Register vor dem Aufruf vorliegen. Zur Vereinfachung kann man vorerst annehmen, dass die %i- und %l-Register in die reservierten 64 Bytes auf dem Stack gesichert werden. Dazu gleich mehr:
Die Anweisung save dient dazu, um in einer aufgerufenen Prozedur die Register der aufrufenden Prozedur nicht zu zerstören. Um in einer aufgerufenen Routine mittels save die alten Registerinhalte abzuspeichern, müssen stets 64 Bytes für die Register %l0 bis %l7 und %i0 bis %i7 reserviert werden (nur reserviert!). Zusätzlich kann man noch weiteren Platz für die Funktionsvariablen anfordern:
save %sp, (-64 -(Zusätzlicher lokaler Speicher) ) & -8, %sp
Eine save-Anweisung bewirkt folgendes: Als erstes wird überprüft, ob noch genügend Register-Windows vorhanden sind, und, falls nicht, werden dementsprechende Verfahren eingeleitet (siehe unten: Window-Overflow). Es werden also die %l- und %i-Register *gespeichert*. Was die save-Anweisung danach macht, ist das Kopieren der Register %o0 bis %o7 in die Register %i0 bis %i7 (Nach! der obigen Prozedur)
Zum Schluss bewirkt save noch eine Anpassung des %sp (%o6), was einer simplen Addition entspricht. Der Stack muss ja von oben nach unten wachsen, weshalb also zum bisherigen %sp einfach ( (-64 -(Zusätzlicher lokaler Speicher) ) & -8) addiert wird (Deshalb sind hier auch alle Zahlen negativ). Dabei ist zu beachten, dass bei dieser Addition die beiden Quell-Operanden von der alten Register-Konstellation genommen werden, der Ziel-Operand sich jedoch bereits auf die neuen Register bezieht.
Der zusätzlich angeforderte Speicherplatz ist ab dem Framepointer zu erreichen. Das erste angeforderte Byte befindet sich (nicht, wie man vielleicht meinen könnte, an %fp, sondern...) an der Adresse %fp-1, das zweite an %fp-2, ... Dieser Speicherplatz kann für die lokalen Variablen und spätere Übergabevariablen benutzt werden.
Die *gesicherten* Register zusammen mit dem angeforderten Speicherplatz nennt man ein Stackframe. Jede Adresse unterhalb des Stackpointers (also ausserhalb des Stackframes) ist für ein User-Programm tabu. Der Framepointer %fp (Unter 80x86 heisst dieser bp=Basepointer) steht stets im Register %i6. Er enthält die Adresse des Stackpointers der aufrufenden Routine, was somit gleichzeitig die Basis des aktuellen Stackframes ist. Damit ist gemeint, dass alle Adressen unterhalb des %fp bis und mit %sp zum aktuellen Stackframe gehören.
Die gegenteilige Anweisung von save ist restore. Sie kopiert zuerst die i-Register zurück in die o-Register und setzt danach die ursprünglichen l- und i-Register wieder auf die Situation vor dem Aufruf (Siehe unten, Window-Underflow). Man kann hier annehmen, dass die durch die save-Anweisung auf dem Stack *gesicherten* Register wieder herausgelesen werden. Zusätzlich könnte man hinter diesem Befehl gleich noch eine Addition anhängen. Dies wird jedoch meistens nicht gebraucht.
Eine Subroutine kann mit den folgenden Befehlen aufgerufen werden:
Function call: call Label Verzweigt an die Adresse "Label" und kopiert den %pc in %o7
oder
Jump and Link: jmpl reg1, reg2 Verzweigt an die Adresse, die in reg1 steht und kopiert den
%pc in reg2 (welches normalerweise %o7 ist)
Aus einer Routine kann mittels folgender Befehle zurückgekehrt werden (Beide Zeilen sind äquivalent)
Return: ret oder Jump and Link: jmpl %i7 + 8, %g0
Dies aus folgendem Grund: Innerhalb der Subroutine ist der alte %pc im Register %i7 gespeichert, welcher auf den ursprünglichen Aufrufe-Befehl zeigt (4 Bytes). Darauf folgt ein Delay-Slot (4 Bytes) und deshalb muss zum %i7 noch 8 hinzugezählt werden, um die nächste ausführbare Anweisung zu adressieren. Das %g0 bedeutet hier, dass der soeben gültige %pc verworfen wird (es wird ja aus der Routine zurückgekehrt, und nicht eine neue aufgesetzt).
Hier einmal der Ablauf eines normalen Funktionsaufrufen mit anschliessender Zurückkehr:
Im aufrufenden Programm (Caller) stehen folgende 2 Zeilen:
call Die_Subroutine "Die_Subroutine" wird in den %pc geladen
nop Delayslot, Vorsicht, Benutzung sehr gefährlich nach call
In der Subroutine (Callee) stehen folgende Zeilen:
Die_Subroutine: Dies ist das Label, welches die Adresse für den obigen Aufruf liefert.
save %sp, (-64 -(5*4)) & 8, %sp Die i- und l-Register werden (bei Window-Overflow) gesichert, die
o-Register in die i-Register kopiert und der Stackpointer für 5 int-Variablen angepasst.
...
ret Die aurfufende Funktion wird gleichfals aufgerufen mit der Adresse %i7 + 8
restore Delay-Slot: Kopiert die i-Register in die o-Register und lädt die i- und l-Register wieder
Folgendermassen werden Parameter von einer aufrufenden Funktion an die aufgerufene Funktion übergeben:
insgesamt 6 Argumente (genauer: 6 * 4 Bytes) können über die Register übergeben werden. Dazu werden von der Aufrufenden Funktion die Werte in die Register o0 bis o5 geschireben, welche dann von der aufgerufenen Funktion (wegen dem save-Befehl) aus den Registern i0 bis i5 gelesen werden können.
Werden mehr als 6 * 4 Bytes übergeben, so muss auf den Stack zurückgegriffen werden. Da es nicht erlaubt ist, auf Adressen tiefer als der %sp zuzugreifen, wird kurzerhand neuer Speicherplatz angefordert. Für 2 Integerwerte, die über den Stack übergeben werden müssen, benötigt man also folgenden Befehl:
add %sp, (-2 * 4) & -8, %sp
Dadurch hat man nun die nötigen 8 Bytes, und man kann die zu übergebenden Parameter auf dem eigenen Stack abspeichern. In der aufgerufenen Prozedur können die Daten mittels dem %fp wiederum herausgelesen werden. (Genauer: siehe unten)
Konventionen
Das Ganze tönt, wie es beim Assembler üblich ist, nach deftig viel Chaos: Jeder kann soviel Speicher herstellen, wie er will und somit jeder anderen Prozedur das Auffinden der richtigen Variablen verunmöglichen. Damit dies jedoch nicht so ist und die Kompatibilität erhalten bleibt, hat man sich folgende Konvention ausgedacht: Das Stackframe muss bei jedem Funktionsaufruf folgende Form haben:
64 Bytes: Register speichern 4 Bytes: struct-Angabe (Darauf kann mittels %sp + 64 direkt zugegriffen werden.) 24 Bytes: Die gleichen 6 * 4 Bytes, die über die Register übergeben werden (4 Bytes:) Damit alle Daten bis hier 8-aligniert sind. n Bytes: Die lokalen Variablen und Übergabevariablen der aufgerufenen Funktion (wiederum 8-aligniert!) Somit sieht die save-Anweisung normalerweise folgendermassen aus: save %sp, -(64 + 4 + 24 + n) & -8, %sp Dies entspricht: save %sp, (-92 - n) & -8, %sp
Die Register, die Struct-Angabe und die Übergabewerte werden alle vom zukünftigen %sp aus erreicht. Die n Bytes lokale Variablen jedoch werden normalerweise vom %fp aus angepeilt. Aus diesem Grund sind diese Werte auch absteigend angeordnet. Insgesamt kann man sagen: ganze Stackframes und die lokalen Variablen sind absteigend angeordnet. Die Daten innerhalb der Blöcke jedoch sind wiederum aufsteigend (relativ positiv zum Beginn des Datenblockes) angeordnet. Zusammenfassend sieht ein Stack-Frame also so aus:
|- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -|
| Unbenutzter Stack-Speicherplatz |
==========+===============================================================|
%sp + 0 | |
. | gespeicherte |
. | Register |
. | (64 Bytes) |
%sp + 60 | |
----------+---------------------------------------------------------------|
%sp + 64 | Structure-Pointer (4 Bytes) |
----------+---------------------------------------------------------------+
%sp + 68 | |
. | erste 6 übergebene |
. | Werte (gleich wie die Register) |
. | (24 Bytes) |
%sp + 88 | |
----------+---------------------------------------------------------------+
%fp - n | ^ |
| Lokale ^ |
| Variablen ^ |
%fp - 4 | ^ |
==========+===============================================================+
%fp + 0 | |
| benutzter Stack-Speicherplatz |
| von Caller |
Übergabe von mehr als 6 Parameter
Angenommen, man möchte 11 Integer (6 haben Platz und 5 sind überschüssig) als Parameter übergeben. Das Stack-Frame des Callers sieht zu Beginn so aus:
==========+===============================================================|
%sp + 0 | Die ersten 68 Bytes |
%sp + 64 | des Stackframes |
----------+---------------------------------------------------------------+
%sp + 68 | |
. | Platz für erste 6 Parameter |
%sp + 88 | |
----------+---------------------------------------------------------------+
%fp - n | Lokale ^ |
%fp - 4 | Variablen ^ |
==========+===============================================================+
Des weiteren kann man annehmen, dass mit Ausnahme der lokalen Variablen das Stackframe unbenutzt ist. Dies deshalb weil die ersten 64 Bytes erst bei einem Window-Overflow benutzt werden und bei einem Window-Overflow wieder unnütz werden und weil der Struct-Pointer an der Stelle %sp+64 erst bei der Rückgabe benötigt wird und weil der Platz für die ersten 6 Parameter noch nicht gefüllt ist.
Nun muss man zuerst die 5 überschüssigen Integer als Parameter übergeben. Man fordert also 5*4 Bytes Speicherplatz an:
add %sp, (-5 * 4) & -8, %sp
Da auf die lokalen Variablen über den %fp und auf den Rest des Stackframes per %sp zugegriffen wird, kann man sagen, dass dieser neue Speicherplatz zwischen den lokalen Variablen und dem Platz für die ersten 6 Parameter liegt:
==========+===============================================================|
%sp + 0 | Die ersten 68 Bytes |
%sp + 64 | des Stackframes |
----------+---------------------------------------------------------------+
%sp + 68 | |
. | Platz für erste 6 Parameter |
%sp + 88 | |
----------+---------------------------------------------------------------+
%sp+92 + 0| Platz für Parameter 7 |
%sp+92 + 4| Platz für Parameter 8 |
%sp+92 + 8| Platz für Parameter 9 |
%sp+92 +12| Platz für Parameter 10 |
%sp+92 +16| Platz für Parameter 11 |
----------+---------------------------------------------------------------+
%fp - n | Lokale ^ |
%fp - 4 | Variablen ^ |
==========+===============================================================+
Jetzt müssen die Parameter 7 bis 11 nur noch an die reservierte Stelle geschrieben werden. Die ersten 6 Parameter werden wie gewohnt per %o-Register übergeben.
Um Werte aus einer Subroutine an die aufrufende Prozedur zurückzugeben, werden die o-Register benutzt. Normalerweise sucht die aufrufende Prozedur das Ergebnis im Register o0. Dies wird oft durch die Addition, die in restore inbegriffen ist, erreicht (denn normalerweise werden die o-Register bei restore ja wieder auf den ehemaligen Stand zurückgesetzt). Die Addition hinter restore funktioniert dabei wie bei save nach folgendem Prinzip: Nimm die beiden Quellregister von den momentan noch aktiven Registern, addiere sie, wechsle dann die Register gemäss restore aus und schreibe dann ins Zielregister.
Zusammenfassend können in einer normalen Funktion oder Prozedur die Register folgendermassen verwendet werden:
globale Register 0-7: Wie gesagt, verfügbar, aber mit Vorsicht zu geniessen.
lokale Register 0-7: Vollumfänglich verfügbar.
Output-Register 0-5: frei verfügbar und zu verwenden bei einem Funktionsaufruf für Parameterübergabe
%o6: Stackpointer, bloss nicht damit herumspielen
%o7: Zukünftiger %pc, wird bei Funktionsaufruf überschrieben!
Input-Register 0-5: grundsätzlich verfügbar, zu bedenken jedoch, dass Änderungen auch in der aufrufenden Prozedur wirksam werden.
%i6: Frame-Pointer, auf keinen Fall öndern!!!
%i7: alter %pc, jegliche Änderung wird mit 99.999%-iger Wahrscheinlichkeit zum Absturz des Programms oder gar des Systems führen!!!
Nun gibt es jedoch auch noch Situationen, bei denen diese Angaben etwas verschärft werden. Es handelt sich hierbei um Subroutinen, die keine weiteren Funktionsaufrufe mehr beinhalben. Man nennt diese Subroutinen Leaf-Subroutinen (Blatt-Routinen). Der Vorteil von solchen Leaf-Routinen ist, dass sich nicht zwingendermassen verpflichtet sind, die Register zu speichern und somit sehr schnell sind, andersherum haben sie jedoch auch weniger Register zur freien Verfügung, da sie sich den Registern der aufrufenden Funktion bedienen müssen. Folgende Änderungen müssen nun berücksichtigt werden:
Zum fehlerfreien Ablauf des gesamten Programmes können nur noch die Register %o0 bis %o5 und %g0 und %g1 benutzt werden. Die angegebenen o-Register wären auch bei normalen Funktionen frei zur Verfügung, dort würde man sie jedoch brauchen, um wiederum Funktionen aufzurufen und Parameter zu übergeben. Die Register %g0 und %g1 sind allgemeine Register. Alle anderen Register werden eventuell von der aufrufenden Funktion benutzt, weshalb sie besser nicht angerührt werden sollten (Es sei denn, man erwartet in bestimmten Registern klar definierte Werte).
Der Aufruf einer Leaf-Routine funktioniert genau gleich, mit dem kleinen aber wichtigen Detail, dass der alte %pc nicht an %i7, sondern an %o7 steht. Zurückgesprungen muss also mit folgendem Befehl:
jmpl %o7 + 8, %g0 !oder als eigener Befehl: retl Auf keinen Fall ret benutzen, denn dieser ist bekanntliche ein Synonym für jmpl %i7 + 8, %g0
Register-Window
Wie oben erklärt müssen für jeden Funktions-Call verschiedenste Register auf dem Stack *gesichert* werden. Würde man dies bei jedem call machen, so kostet dies sehr viel Zeit, weshalb man folgende Lösung ersinnte: Man baut einfach einen Prozessor, der das ganze Prozedere der Registerkopiererei in einem Schnurz macht, indem er "einfach" ein paar weitere Register hinzugelötet bekommt. Diese zusätzlichen Register sind um einiges schneller zu erreichen, als der Stack. Konkret heisst das, dass ein Prozessor 2 bis 32 Mal die i-, o- und l-Register besitzt, wobei jeweils ein Set von diesen Registern ein Register-Window genannt wird. Der Sparc v8 besitzt 8 solche Windows, TKISEM simuliert 4 davon, COBASS simuliert 8. Zusätzlich besitzt der Prozessor nach wie vor nur 8 globale Register (die werden bei Funktions-Calls ja nicht beeinträchtigt).
Angenommen, die momentanen Register befinden sich im Window 4. Wenn nun eine Funktion aufgerufen wird, die die Registerinhalte abspeichern will, so werden die o-Register des Windows 4 in die i-Register des Windows 3 kopiert und sodann das gesamte Window 3 aktiviert (Der Prozessor zählt also rückwärts!). Und damit hätte sich das Geschiebe bereits erledigt. Der Prozessor läuft nun im aktivierten Window 3. Sobald ein restore auftritt, wechselt er wieder ins Window 4, nachdem er die i-Register des Windows 3 wiederum in die o-Register des Windows 4 kopiert hat.
Von diesen Registern wird ein Window stets freigehalten, damit Traps dort drin laufen können. Genaugenommen ist dies stets das Window, welches gleich nach (%cwp--) dem aktuellen Window drankommt.
Um diese Windows korrekt zu aktivieren, benötigt der Prozessor noch zwei zusätzliche Angaben: CWP und WIN. WCP beinhaltet stets die momentan aktive Window-Nummer. Dieser Wert wird bei einem save stets um 1 verringert (mit Wrap-Around, also nach 0 kommt wieder 7) und wird bei einem restore um 1 erhöht. WIN (Window invalid Mask) dagegen ist eine Maske, welche stets anzeigt, welches Window zuletzt noch benutzt werden darf.
Mittels dieser beiden Grössen kann der Prozessor erkennen, wenn er bei einem save oder restore kein weiteres freies Window mehr zur Verfügung hat. Er prüft jeweils, ob WIM[CWP]=1. Ist dies der Fall, so liegt ein Window Over- oder Underflow vor.
Genauer:
Wird bei einem save (Beispielsweise im Window 3) erkannt, dass das zu aktivierende Window (Nummer 2) das invalid-Mask-Bit (%wim=0010b) gesetzt hat, so bedeutet dies, dass alle Windows gefüllt sind und das zu aktivierende (Nummer 2) gerade das vorrätige Window für die Traps war (Overflow). Damit bei der Aktivierung dieses Windows trotzdem wieder ein Trap-Window übrig ist, muss das wiederum nächste (Nummer 1), welches bis jetzt noch die Register einer älteren Funktion speicherte, auf den Stack geschrieben werden. Dazu wird in dieses Window (Nummer 1) gewechselt, und die Register werden an den dort vorgegebenen %sp geschrieben (erst jetzt werden die reservierten 64 Bytes benutzt!). Sodann kann das Window (Nummer 1) gefahrlos für Traps benutzt werden. Die %wim wird nun nach rechts rotiert (%wim=0001b) und das ursprüngliche Window (Nummer 2) wird wieder aktiviert, und aus dem Trap wird wieder zurückgesprungen zum eigentlichen Programm. Dort wird nun der save-Befehl erneut ausgeführt, worauf kein Trap mehr entstehen wird.
Wird bei einem restore (Beispielsweise im Window 1) erkannt, dass das zu aktivierende Window (Nummer 2) das invalid-Mask-Bit (%wim=0100b) gesetzt hat, so bedeutet dies, dass alle Windows ausser dem aktiven leer sind (Underflow). Da jedoch ein restore auftritt, muss dies bedeuten, dass sich in dem zu aktivierenden Window (Nummer 2) theoretisch Register befinden müssten, die nun aber auf dem Stack gesichert sind (aufgrund eines früheren save mit Overflow). Diese Register sind gespeichert an dem momentanen %fp, welcher der Gleiche ist, wie der %sp des zu aktivierenden Windows. Sodann wird dieses Window aktiviert (%fp -> %sp), und die Register werden von dem Stack wieder herausgelesen. Die %wim wird nun nach links rotiert (%wim=1000b) und das ursprüngliche Window (Nummer 1) wird wieder aktiviert, und aus dem Trap wird wieder zurückgesprungen zum eigentlichen Programm. Dort wird nun der restore-Befehl erneut ausgeführt, worauf kein Trap mehr entstehen wird.
Nun, soweit die Theorie. Bis hier mag es noch einigermassen einleuchtend sein. Wenn man jedoch beispielsweise den Traphandler von TKISEM ankuckt, so kommt man spätestens beim Underflow wieder ins stocken. Hier deshalb der Original-Code (aus dem Vorlesungsskript) mit meinen Erklärungen dazu (der Code wurde von mir zur Vereinfachung rechts nach bestem Wissen komplettiert):
Overflow
1 wover_vect: | wover_vect: 2 set wover, %l3 | set wover, %l3 3 jmpl %l3, %r0 | jmpl %l3, %r0 4 nop | nop 5 | 6 wover: | wover: 7 save | save 8 | 9 stda %l0, [%sp]10 | stda %l0, [%sp]10 10 add %sp, 8, %l0 | add %sp, 8, %l0 11 stda %l2, [%l0]10 | stda %l2, [%l0]10 12 inc 8, %l0 | inc 8, %l0 13 stda %l4, [%l0]10 | stda %l4, [%l0]10 14 | inc 8, %l0 15 ... | stda %l6, [%l0]10 16 | 17 | inc 8, %l0 18 stda %i0, [%l0]10 | stda %i0, [%l0]10 19 inc 8, %l0 | inc 8, %l0 20 stda %i2, [%l0]10 | stda %i2, [%l0]10 21 | inc 8, %l0 22 ... | stda %i4, [%l0]10 23 | inc 8, %l0 24 stda %i6, [%l0]10 | stda %i6, [%l0]10 25 | 26 mov %psr, %l2 | mov %psr, %l2 27 and %l2, 0x1f, %l2 | and %l2, 0x1f, %l2 28 set 1, %l3 | set 1, %l3 29 sll %l3, %l2, %l3 | sll %l3, %l2, %l3 30 mov %l3, %wim | mov %l3, %wim 31 nop | nop 32 nop | nop 33 nop | nop 34 | 35 restore | restore 36 jmpl %r17, %g0 | jmpl %r17, %g0 37 rett %r18 | rett %r18
Zeilen 1 bis 4: Trap-Vektor
Dies ist die Implementierung des Trap-Vectors, welcher vom System in die Trap-Vector-Tabelle eingetragen wird. Tritt ein Trap auf, so wird an die Adresse gesprungen, an der diese Zeilen im RAM stehen, und dann abgearbeitet. Ein solcher Vektor beinhaltet stets 4 Befehle (mehr hat nicht Platz). In Zeile 2 schreibt der Computer die eigentliche Adresse des Traphandlers (wover, Zeile 6) ins Register l3. Da ein Label stets eine grosse Konstante ist, handelt es sich bei diesem set-Befehl um eine synthetische Operation, welche 2 Befehle benötigt. In der Zeile 3 wird dann zum Trap gesprungen und der %pc verworfen (es soll nicht in die Vektortabelle zurückgesprungen werden). Nun bleibt noch der Delayslot: nop.
Zeile 7: Wechseln ins betroffene Register
Wenn ein Programm beispielsweise im Window 3 läuft (wie oben beschrieben), so läuft der Traphandler automatisch im Trap-Window 2, welches extra für Traps stets freigehalten wird. Bei einem Overflow kann man sich zudem noch sicher sein, dass dieses Trap-Window 2 als invalid gekennzeichnet ist (dies war ja auch gerade eben der Grund, wieso der Trap aufgerufen wurde). Das zu speichernde Window ist also das wiederum nächst tiefere (Nummer 1). Durch dieses Wissen kann man in dieses zu speichernde Window nun ganz einfach mittels eines save-Befehls wechseln (diesmal tritt kein Trap auf, da das invalide Window ja garantiert das ist, in dem wir uns bereits befinden, und nicht da, in das wir wechseln wollen). Mittels dieses einen save-Befehls kann also gefahrlos in das zu speichernde Window gewechselt werden, worauf der Traphandler dann auf die betroffenen Register zugreifen kann.
Zeilen 9 bis 24: Das Sichern der Register
Was in diesen Zeilen passiert, ist eigentlich Routine-Arbeit. Grundsätzlich liest der Computer einfach alle Register aus und speichert sie an den vorgesehenen Platz. Jetzt erst werden die Register tatsächlich GESICHERT.
Allerdings müssen folgende Dinge beachtet werden: Zuerst mal läuft der Traphandler im Supervisor-Bereich, was bedeutet, dass er nicht einfach so auf den Stack des zu sichernden Programms zugreifen kann. Er benutzt deshalb den alternativ-Store-Befehl stda (welcher nur im Supervisormode verfügbar ist). Die Zahl 10 zuhinterst an der Zeile bedeutet jeweils, dass er auf den Speicherbereich 10 = 0x0a = USER_DATA zugreift. Und zwar greift er auf die Adresse des Stackpointers %sp zu, welches genau die Stelle ist, an welcher schon früher (siehe oben) der Platz für die 16 zu speichernden Register geschaffen wurde. Der Befehl, den er benutzt, ist stda, was bedeutet, dass er immer gleich zwei Register (8 Byte) auf einmal liest und diese dann an die vorgesehene Adresse abspeichert.
Zu Beginn dieser Zeilen (nach dem save) darf der Traphandler keine Register überschreiben, da ansonsten deren Inhalte verloren wären. Deshalb muss in der Zeile 9 die Adresse direkt per %sp angesprochen werden. Ab der Zeile 10 sind jedoch die Register l0 und l1 gespeichert, worauf diese gefahrlos benutzt werden können. Tatsächlich wird nun das Register %l0 als Laufvariable benutzt, welche nach jeder Zeile wieder auf die nächsten zu füllenden 8 Bytes zeigt.
Zeilen 26 bis 33: Anpassung der WIM (Window-Invalid-Mask)
Die Daten sind nun gesichert und das Window kann somit als das neue Trap-Window festgelegt werden. Grundsätzlich spricht man immer davon, dass man die WIM einfach nur rotieren muss, es gibt auf dem Sparc allerdings keinen Rotate-Befehl. Dafür sind nun diese paar Zeilen da.
In Zeile 26 wird das Prozessor-State-Register %psr in %l2 geladen und in Zeile 27 mit 0x1f (binär 00011111) verknüpft. Dadurch erhält man die untersten 5 Bits des %psr, was dem Current-Window-Pointer CWP entspricht. Sodann speichert man in Zeile 28 in das Register %l3 eine 1 (binär 00000001) und schiebt diese 1 in Zeile 29 um genausoviele Stellen nach links, wie der CWP angibt. Befinden wir uns beispielsweise im Window 1, so ist das Bit Nummer 1 nach dieser Operation maskiert (00000010). Nun kann man diesen Wert direkt ins %wim zurückschreiben (Eine Operation, die 4 Taktzyklen braucht, deshalb die nops).
Zeilen 35 bis 37: Prozedur beenden
Da nun alle Arbeiten erledigt sind, kann wieder zum ursprünglichen Programm zurückgekehrt werden. Zum einen wird zuerst wieder ins korrekte Trap-Window mittels restore zurückgekehrt (dies erzeugt auch garantiert keinen weiteren Trap, da die Invalid-Mask mittlerweile auf das aktuelle Window abgeändert wurde). Dann wird mittels eines jump-and-link-Befehls zurückgesprungen an die Speicherstelle, an der der Fehler auftrat (Die beim Auftreten des Traps in %r17=%l1 gespeichert wurde), damit sie erneut aufgerufen werden kann ohne einen Trap zu erzeugen. Der Befehl rett schlussendlich setzt den %npc auf den ursprünglichen Wert (%r18=%l2) und wechselt wieder in den Usermode.
Underflow
Irgendwann vorher: | .data
| nreg_sets: .word 4
... |
1 wunder_vect: | wunder_vect:
2 set wunder, %l3 | set wunder, %l3
3 jmpl %l3, %r3 | jmpl %l3, %r3
4 nop | nop
5 |
6 wunder: | wunder:
7 set nreg_sets, %l0 | set nreg_sets, %l0
8 ld [%l0], %l0 | ld [%l0], %l0
9 mov %psr, %l6 | mov %psr, %l6
10 and %l6, 0x1f, %l5 | and %l6, 0x1f, %l5
11 add %l5, 3, %l5 | add %l5, 3, %l5
12 cmp %l0, %l5 | cmp %l0, %l5
13 bgt 1f | bgt 1f
14 nop | nop
15 sub %l5, %l0, %l5 | sub %l5, %l0, %l5
16 |
17 1: | 1:
18 set 1, %l7 | set 1, %l7
19 sll %l7, %l5, %l7 | sll %l7, %l5, %l7
20 mov %l7, %wim | mov %l7, %wim
21 nop | nop
22 nop | nop
23 nop | nop
24 |
25 restore | restore
26 restore | restore
27 |
28 add %sp, 8, %l0 | add %sp, 8, %l0
29 ldda [%l0]10, %l2 | ldda [%l0]10, %l2
30 inc 8, %l0 | inc 8, %l0
31 | ldda [%l0]10, %l4
32 | inc 8, %l0
33 | ldda [%l0]10, %l6
34 | inc 8, %l0
35 | ldda [%l0]10, %i0
36 ... | inc 8, %l0
37 | ldda [%l0]10, %i2
38 | inc 8, %l0
39 | ldda [%l0]10, %i4
40 | inc 8, %l0
41 | ldda [%l0]10, %i6
42 ldda [%sp]10, %l0 | ldda [%sp]10, %l0
43 |
44 save | save
45 save | save
46 jmpl %r17, %g0 | jmpl %r17, %g0
47 rett %r18 | rett %r18
Zeilen 1 bis 4: Trap-Vektor
Hier passiert das Gleiche wie beim Overflow. die drei Zeilen Code ergeben 4 Befehle (set ist synthetisch) und werden in die Trapvektor-Tabelle eingetragen.
Weitere Überlegung
Bevor nun die weiteren Zeilen betrachtet werden, muss man sich einer Sache klar werden: Wenn beispielsweise im Window 1 (wie oben) ein Underflow-Trap auftritt, so bedeutet dies, dass das Window 2 invalid ist (es enthält keine gültigen Werte). Der Trap, der dann aufgerufen wird, läuft jedoch standardgemäss (immer, ohne Ausnahme, das einzig Sichere am Assemblerprogrammieren unter Sparc) im dem Window vor! dem aktuellen Window, also dem mit der Nummer 0. Und das ist nun eine etwas dumme Sache. Beim Overflowtrap hatte man den Vorteil, dass man sich garantiert in genau dem Window befand, auf welches auch die invalid-Mask zeigt, worauf jeglicher save- oder restore-Befehl garantiert keinen weiteren Trap erzeugte.
Beim Underflow ist dem nicht mehr so (Um genau zu sein ist es garantiert! nicht so, es sei denn, der Prozessor besitzt nur genau 2 Register-Windows). Dies bedeutet, dass also bevor die Daten wiederhergestellt werden können, zuerst die WIM angepasst werden muss, damit dann überhaupt in das zu restaurierende Window gewechselt werden kann.
Zeilen 7 bis 15: Modulo-Berechnung für den CWP
Das Kernstück dieser Zeilen sind die Zeilen 9 und 10, welche wie beim Overflow-Trap den CWP ermitteln. Man halte hier jedoch fest, dass dieser CWP sich auf das Trap-Window bezieht, und nicht auf das zu restaurierende Window.
Was in den anderen Zeilen passiert ist folgendes: In den Zeilen 7 und 8 wird zuerst einmal ermittelt, wieviele Register der Prozessor überhaupt besitzt (Da dies hier eine TKISEM-Implementierung ist, handelt es sich um die Zahl 4). Diese Zahl befindet sich irgendwo im Kern des Systems in einer Variable (Die Zeilen, die mit "Irgendwann vorher" beschriftet sind).
Nun möchte man ja gerne das Window mit der Nummer 3 als invalid markieren (Dasjenige, welches nach dem zu restaurierenden Window kommt). Was also getan werden muss, ist, dem aktuellen CWP die Zahl 3 zu addieren (Zeile 11). Dies deshalb (Rekapitulation): Momentan befindet man sich im Window 0 (Trap-Window). Das Window, welches den Trap erzeugte, hatte die Nummer 1. Das zu restaurierende Window, welches momentan noch als invalid markiert ist und deshalb auch den Trap bewirkte, war Nummer 2. Und dasjenige, das nun neu als das invalide Window gelten soll, hat die Nummer 3. Somit kann man sehen, dass in jedweger Kombination und Konfiguration (selbst bei Prozessoren mit nur 2 Registerwindows). Stets 3 hinzugezählt werden muss.
Dies ist aber noch nicht ganz alles: Würde der Trap beispielsweise in einem Window mit der Nummer 3 arbeiten, so hätte das zu maskierende Window ja die Nummer 6, und das gibt es (beispielsweise in TKISEM) nicht immer. Was nun also in den Zeilen 12 bis 15 folgt, ist die Modulo-Berechnung des CWP: In Zeile 12 wird der (mit 3 addierte) CWP mit der Anzahl Register des Prozessort verglichen (Befindet sich jetzt im Register %l0, Zeilen 7 und 8). Ist die Anzahl Register des Prozessors grösser als der CWP, so handelt es sich um eine gültige Zahl und es wird direkt weitergesprungen zu Zeile 17. Falls jedoch nicht, so muss dem CWP noch die Anzahl Register abgezählt werden (Zeile 15), damit %l5 nun ebenfalls eine gültige Nummer enthält.
Beispiel: Anzahl Windows von TKISEM = 4, CWP = 0:
0 + 3 = 3 Anzahl Windows ist grösser als 3, deshalb gleich weiter zu Zeile 17
Beispiel: Anzahl Windows von TKISEM = 4, CWP = 3:
3 + 3 = 6 Anzahl Windows ist nicht grösser als 3, deshalb...
%l5 = 6 - 4 = 2 Entspricht nun einer gültigen Nummer
Beispiel: Anzahl Windows von COBASS = 8, CWP = 3:
3 + 3 = 6 Anzahl Windows ist grösser als 6, deshalb gleich weiter zu Zeile 17
Das folgende Beispiel zeigt, dass dieser Handler nicht für den allgemeinen Fall funktioniert. Da es sich hier jedoch um die explizite implementierung von TKISEM handelt, tritt dieser Fall gar nicht auf. Anzahl Windows = 2, CWP = 1:
1 + 3 = 4 Anzahl Windows ist nicht grösser als 4, deshalb...
%l5 = 4 - 2 = 2 Entspricht KEINER gültigen Nummer!
Zeilen 17 bis 23:
Dies entspricht nun wieder den Zeilen des Overflow-Traps. Hier wird eine 1 kreiert (binär 00000001) und um die gerade eben errechnete Zahl des zu invalidierenden Windows verschoben.
Zeilen 25 und 28: Wechseln in das zu restaurierende Window
Nun endlich kann man gefahrlos in das zu restaurierende Window wechseln, da nun die WIM so gesetzt ist, dass sie garantiert nicht mehr im Weg steht. Zum einem muss jedoch beachtet werden, dass hier anstelle des save- der restore-Befehl benutzt werden muss, da das Window sich ja nach! dem aktuellen Trapwindow befindet. Zum anderen braucht es deren 2, da wie oben beschrieben das zu restaurierende Window ja erst das übernächste ist. Zur Anmerkung noch: Dies funktioniert bei einem Prozessor mit nur 2 Windows widerum nicht!
Zeilen 28 bis 42: Wiederherstellen der Register
In diesen Zeilen werden nun ähnlich wie im Overflow-Trap alle Register wieder herausgelesen. Eine kleine Besonderheit ist hierbei, dass mit den beiden Registern %l0 und %l1 bis zum Schluss gewartet wird, sodann %l0 wieder als Laufvariable dienen kann.
Zeilen 42 bis 47: Beenden des Traps
Was nun noch folgt, ist das Zurückspringen in das Trap-Window (2 Mal save) und dann das Zurückspringen zum Programm, welches den Trap verursachte.