Wie versprochen sprechen wir weiterhin über die Entwicklung
von Elbrus-Prozessoren . Dieser Artikel ist technisch. Die Informationen in diesem Artikel sind keine offiziellen Unterlagen, da sie während des Studiums von Elbrus ähnlich wie eine Black Box erhalten wurden. Aber es wird sicherlich interessant für ein besseres Verständnis der Elbrus-Architektur sein, denn obwohl wir eine offizielle Dokumentation hatten, wurden viele Details erst nach langwierigen Experimenten klar, als
Embox funktionierte.
Denken Sie daran, dass wir im
vorherigen Artikel über den grundlegenden Systemstart und den Treiber für die serielle Schnittstelle gesprochen haben. Embox wurde gestartet, aber für die weitere Weiterentwicklung brauchten wir Interrupts, einen System-Timer und natürlich möchte ich einige Unit-Tests einbinden, und dafür brauchen wir setjmp. Dieser Artikel konzentriert sich auf Register, Stapel und andere technische Details, die zur Implementierung all dieser Dinge erforderlich sind.
Beginnen wir mit einer kurzen Einführung in die Architektur. Dies sind die Mindestinformationen, die erforderlich sind, um zu verstehen, was später erläutert wird. In Zukunft werden wir auf die Informationen aus diesem Abschnitt verweisen.
Kurze Einführung: Stapel
In Elbrus gibt es drei Stapel:
- Prozedurstapel (PS)
- Prozedur Chain Stack (PCS)
- User Stack (US)
Lassen Sie uns sie genauer analysieren. Die Adressen in der Abbildung sind bedingt und zeigen, in welche Richtung die Bewegungen gerichtet sind - von einer größeren Adresse zu einer kleineren oder umgekehrt.

Der Prozedurstapel (PS) ist für Daten vorgesehen, die "Betriebsregistern" zugeordnet sind.
Beispielsweise kann es sich um Funktionsargumente handeln. In „normalen“ Architekturen ist dieses Konzept den Allzweckregistern am nächsten. Im Gegensatz zu "normalen" Prozessorarchitekturen werden in E2K in Registern verwendete Register auf einem separaten Stapel gestapelt.
Der Stapel von Bindungsinformationen (PCS) dient zum Platzieren von Informationen über die vorherige (aufrufende) Prozedur und wird bei der Rückgabe verwendet. Die Daten auf der Absenderadresse sowie bei den Registern werden an einem separaten Ort abgelegt. Daher ist die Stapelheraufstufung (z. B. das Beenden ausnahmsweise in C ++) zeitaufwändiger als in „normalen“ Architekturen. Auf der anderen Seite werden dadurch Stapelüberlaufprobleme beseitigt.
Beide Stapel (PS und PCS) sind durch eine Basisadresse, Größe und einen Stromversatz gekennzeichnet. Diese Parameter werden in den PSP- und PCSP-Registern eingestellt, sie sind 128-Bit und im Assembler müssen Sie auf bestimmte Felder verweisen (z. B. hoch oder niedrig). Darüber hinaus hängt die Funktionsweise der Stapel eng mit dem Konzept einer Registerdatei zusammen, mehr dazu weiter unten. Die Interaktion mit der Datei erfolgt über den Mechanismus des Pumpens / Austauschs von Registern. Eine aktive Rolle in diesem Mechanismus spielt der sogenannte "Hardware-Zeiger auf die Oberseite des Stapels" der Prozedur bzw. des Stapels von Bindungsinformationen. Darüber auch weiter unten. Es ist wichtig, dass sich die Daten dieser Stapel zu jedem Zeitpunkt entweder im RAM oder in einer Registerdatei befinden.
Es ist auch erwähnenswert, dass diese Stapel (der prozedurale Stapel und der Stapel von Bindungsinformationen) erwachsen
werden . Wir sind darauf gestoßen, als wir context_switch implementiert haben.
Der Benutzerstapel erhält auch die Basisadresse und -größe. Der aktuelle Zeiger befindet sich im Register USD.lo. Im Kern ist es ein klassischer Stapel, der nach unten wächst. Nur passen im Gegensatz zu „normalen“ Architekturen Informationen von anderen Stapeln (Registern und Rücksprungadressen) nicht dorthin.
Eine meiner Meinung nach nicht standardmäßige Anforderung für die Grenzen und Größen der Stapel ist die 4K-Ausrichtung, und sowohl die Basisadresse des Stapels als auch seine Größe müssen auf 4K ausgerichtet sein. In anderen Architekturen bin ich einer solchen Einschränkung nicht begegnet. Dieses Detail ist uns erneut begegnet, als wir context_switch implementiert haben.
Kurze Einführung: Register. Dateien registrieren. Fenster registrieren
Nachdem wir die Stapel ein wenig herausgefunden haben, müssen wir verstehen, wie die Informationen in ihnen dargestellt werden. Dazu müssen wir einige weitere Konzepte einführen.
Eine Registerdatei (RF) ist ein Satz aller Register. Es gibt zwei Registerdateien, die wir benötigen: Eine Datei mit Verbindungsinformationen (Chain-Datei - CF), die andere wird als Registerdatei (RF) bezeichnet und speichert "Betriebsregister", die auf dem prozeduralen Stapel gespeichert sind.
Das Registerfenster ist der Bereich (Registersatz) der Registerdatei, der derzeit verfügbar ist.
Ich werde es genauer erklären. Was ist eine Reihe von Registern, denke ich, muss niemand erklären.
Es ist bekannt, dass einer der Engpässe in der x86-Architektur genau eine kleine Anzahl von Registern ist. In RISC-Architekturen mit Registern ist es einfacher, normalerweise etwa 16 Register, von denen mehrere (2-3) für offizielle Zwecke belegt sind. Warum nicht einfach 128 Register erstellen, denn dies scheint die Systemleistung zu steigern? Die Antwort ist ganz einfach: Ein Prozessorbefehl benötigt einen Platz zum Speichern der Registeradresse, und wenn es viele davon gibt, werden auch viele Bits dafür benötigt. Deshalb gehen sie zu allen möglichen Tricks, erstellen Schattenregister, registrieren Banken, Fenster und so weiter. Mit Schattenregistern meine ich das Prinzip der Registerorganisation in ARM. Wenn eine Unterbrechung oder eine andere Situation auftritt, ist ein anderer Satz von Registern mit denselben Namen (Nummern) verfügbar, während die im ursprünglichen Satz gespeicherten Informationen dort verbleiben. Registerbanken sind in der Tat Schattenregistern sehr ähnlich, es gibt einfach keine Hardware-Umschaltung von Registersätzen, und der Programmierer wählt aus, welche Bank (Registersatz) jetzt kontaktiert werden soll.
Registerfenster dienen dazu, die Arbeit mit dem Stapel zu optimieren. Wie Sie wahrscheinlich verstehen, geben Sie in einer „normalen“ Architektur eine Prozedur ein, speichern Register im Stapel (oder das Speichern der aufrufenden Prozedur hängt von der Vereinbarung ab) und Sie können Register verwenden, da die Informationen bereits auf dem Stapel gespeichert sind. Der Speicherzugriff ist jedoch langsam und sollte daher vermieden werden. Wenn Sie die Prozedur eingeben, stellen Sie einfach einen neuen Registersatz zur Verfügung. Die Daten des alten werden gespeichert, sodass Sie sie nicht in den Speicher kopieren müssen. Wenn Sie zur aufrufenden Prozedur zurückkehren, wird außerdem das vorherige Registerfenster zurückgegeben, sodass alle Daten in den Registern relevant sind. Dies ist das Konzept eines Registerfensters.

Es ist klar, dass Sie die Register noch auf dem Stapel (im Speicher) speichern müssen, dies kann jedoch erfolgen, wenn die freien Registerfenster beendet sind.
Und was tun mit den Eingabe- und Ausgaberegistern (Argumente bei der Eingabe der Funktion und dem zurückgegebenen Ergebnis)? Lassen Sie das Fenster einen Teil der Register enthalten, die vom vorherigen Fenster aus sichtbar sind, genauer gesagt, ein Teil der Register ist für beide Fenster verfügbar. Wenn Sie dann die Funktion aufrufen, müssen Sie im Allgemeinen nicht auf den Speicher zugreifen. Angenommen, unsere Register sehen so aus

Das heißt, r0 im ersten Fenster ist das gleiche Register wie r2 in Null und r1 aus dem ersten Fenster im gleichen Register wie r3. Das heißt, wenn wir vor dem Aufruf der Prozedur in r2 schreiben (die Fensternummer ändern), erhalten wir den Wert in r0 in der aufgerufenen Prozedur. Dieses Prinzip wird als Mechanismus zum Drehen von Fenstern bezeichnet.
Lassen Sie uns etwas mehr optimieren, denn die Macher von Elbrus haben genau das getan. Lassen Sie die Fenster, die wir haben, keine feste Größe haben, sondern variabel, die Fenstergröße kann zum Zeitpunkt des Eintritts in die Prozedur eingestellt werden. Wir werden das gleiche mit der Anzahl der gedrehten Register tun. Dies führt natürlich zu einigen Problemen, da in den klassischen drehbaren Fenstern ein Fensterindex vorhanden ist, über den bestimmt wird, dass Sie Daten aus der Registerdatei auf dem Stapel speichern oder laden müssen. Wenn Sie jedoch nicht den Fensterindex eingeben, sondern den Registerindex, von dem aus unser aktuelles Fenster startet, tritt dieses Problem nicht auf. In Elbrus sind diese Indizes in den Registern PSHTP (für den PS-Prozedurstapel) und PCSHTP (für den PCS-Prozedurinformationsstapel) enthalten. Die Dokumentation bezieht sich auf „Hardware-Zeiger auf die Oberseite des Stapels“. Jetzt können Sie erneut versuchen, über die Stapel zu lesen. Ich denke, es wird klarer.
Wie Sie verstehen, impliziert ein solcher Mechanismus, dass Sie steuern können, was sich im Speicher befindet. Synchronisieren Sie also die Registerdatei und den Stapel. Ich meine einen Systemprogrammierer. Wenn Sie ein Anwendungsprogrammierer sind, bietet das Gerät einen transparenten Ein- und Ausstieg aus dem Verfahren. Das heißt, wenn beim Versuch, ein neues Fenster auszuwählen, nicht genügend Register vorhanden sind, wird das Registerfenster automatisch „abgepumpt“. In diesem Fall werden alle Daten aus der Registerdatei auf dem entsprechenden Stapel (im Speicher) gespeichert und der „Zeiger auf die Hardware-Oberseite des Stapels“ (Offset-Index) wird auf Null zurückgesetzt. Ebenso erfolgt das Austauschen einer Registerdatei vom Stapel automatisch. Wenn Sie beispielsweise eine Kontextumschaltung entwickeln, die genau das ist, was wir getan haben, benötigen Sie einen Mechanismus für die Arbeit mit dem verborgenen Teil der Registerdatei. In Elbrus werden hierfür die Befehle FLUSHR und FLUSHC verwendet. FLUSHR - Wenn die Registerdatei gelöscht wird, werden alle Fenster außer dem aktuellen in den prozeduralen Stapel geleert. Der PSHTP-Index wird dementsprechend auf Null zurückgesetzt. FLUSHC - Bereinigt die Bindungsinformationsdatei. Alles außer dem aktuellen Fenster wird auf den Bindungsinformationsstapel geschrieben. Der PCSHTP-Index wird ebenfalls auf Null zurückgesetzt.
Kurze Einführung: Implementierung in Elbrus
Nachdem wir die nicht offensichtliche Arbeit mit Registern und Stapeln besprochen haben, werden wir genauer auf verschiedene Situationen in Elbrus eingehen.
Wenn wir die nächste Funktion aufrufen, erstellt der Prozessor zwei Fenster: ein Fenster auf dem PS-Stapel und ein Fenster auf dem PCS-Stapel.
Ein Fenster im PCS-Stapel enthält die Informationen, die für die Rückkehr von einer Funktion erforderlich sind: z. B. IP (Instruction Pointer) der Anweisung, bei der Sie von der Funktion zurückkehren müssen. Damit ist alles mehr oder weniger klar.
Das Fenster auf dem PS-Stack ist etwas kniffliger. Das Konzept der Register des aktuellen Fensters wird vorgestellt. In diesem Fenster haben Sie Zugriff auf die Register des aktuellen Fensters -% dr0,% dr1, ...,% dr15, ... Das heißt, für uns als Benutzer sind sie immer von 0 nummeriert, dies ist jedoch eine Nummerierung relativ zur Basisadresse des aktuellen Fensters. Über diese Register werden die Argumente übergeben, wenn die Funktion aufgerufen wird, und der Wert wird zurückgegeben, und die Funktion wird als Allzweckregister innerhalb der Funktion verwendet. Tatsächlich wurde dies unter Berücksichtigung des Mechanismus des Drehens von Registerfenstern erklärt.
Die Größe des Registerfensters in Elbrus kann gesteuert werden. Dies ist, wie gesagt, zur Optimierung notwendig. In einer Funktion benötigen wir beispielsweise nur 4 Register zum Übergeben von Argumenten und einige Berechnungen. In diesem Fall entscheidet der Programmierer (oder Compiler), wie viele Register der Funktion zugewiesen werden sollen, und legt basierend darauf die Fenstergröße fest. Die Fenstergröße wird durch die Operation setwd festgelegt:
setwd wsz=0x10
Gibt die Fenstergröße in Form von Quad-Registern (128-Bit-Registern) an.

Angenommen, Sie möchten eine Funktion von einer Funktion aus aufrufen. Hierzu wird das bereits beschriebene Konzept eines gedrehten Registerfensters angewendet. Das Bild oben zeigt ein Fragment einer Registerdatei, in der eine Funktion mit Fenster 1 (grün) eine Funktion mit Fenster 2 (orange) aufruft. In jeder dieser beiden Funktionen haben Sie Zugriff auf% dr0,% dr1, ... Die Argumente werden jedoch über die sogenannten Rotationsregister weitergeleitet. Mit anderen Worten, ein Teil der Register von Fenster 1 wird zu den Registern von Fenster 2 (beachten Sie, dass sich diese beiden Fenster schneiden). Diese Register werden ebenfalls vom Fenster eingestellt (siehe Rotationsregister im Bild) und haben die Adresse% db [0],% db [1], ... Somit ist das% dr0-Register in Fenster 2 nichts anderes als das% db [0] -Register in Fenster 1.
Das Rotationsregisterfenster wird durch die Operation setbn festgelegt:
setbn rbs = 0x3, rsz = 0x8
rbs legt die Größe des gedrehten Fensters fest und rsz legt die Basisadresse fest, jedoch relativ zum aktuellen Registerfenster. Das heißt, Hier haben wir ab dem 8. 3 Register zugeordnet.
Basierend auf dem Vorstehenden zeigen wir, wie der Funktionsaufruf aussieht. Der Einfachheit halber nehmen wir an, dass die Funktion ein Argument akzeptiert:
void my_func(uint64_t a) { }
Um diese Funktion aufzurufen, müssen Sie ein Fenster mit Rotationsregistern vorbereiten (dies haben wir bereits über setbn getan). Als nächstes geben wir im% db0-Register den Wert ein, der an my_func übergeben wird. Danach müssen Sie die CALL-Anweisung aufrufen und nicht vergessen, ihr mitzuteilen, wo das Fenster der gedrehten Register beginnt. Wir überspringen jetzt die Vorbereitung für den Anruf (den Befehl disp), da nicht zwischen Groß- und Kleinschreibung unterschieden wird. In Assembler sollte ein Aufruf dieser Funktion daher folgendermaßen aussehen:
addd 0, %dr9, %db[0] disp %ctpr1, my_func call %ctpr1, wbs = 0x8
Also, mit Registern ein wenig herausgefunden. Schauen wir uns nun den Stapel verbindlicher Informationen an. Es speichert die sogenannten CR-Register. In der Tat zwei - CR0, CR1. Und sie enthalten bereits die Informationen, die für die Rückkehr von der Funktion erforderlich sind.

Die Register CR0 und CR1 des Fensters der Funktion, die die Funktion mit den orange markierten Registern aufgerufen hat, sind grün. Die CR0-Register enthalten den Anweisungszeiger der aufrufenden Funktion und eine bestimmte Prädikatdatei (PF-Prädikatdatei). Eine Geschichte darüber geht definitiv über den Rahmen dieses Artikels hinaus.
Die CR1-Register enthalten Daten wie PSR (Textverarbeitungsstatus), Fensternummer, Fenstergrößen usw. In Elbrus ist alles so flexibel, dass jede Prozedur Informationen in CR1 speichert, selbst darüber, ob Gleitkommaoperationen in der Prozedur enthalten sind, und ein Register, das Informationen zu Software-Ausnahmen enthält. Dafür müssen Sie natürlich für das Speichern zusätzlicher Informationen bezahlen.
Es ist sehr wichtig, nicht zu vergessen, dass die Registerdatei und die Bindungsinformationsdatei aus dem RAM herausgepumpt und ausgetauscht werden können und umgekehrt (von den oben beschriebenen PS- und PCS-Stapeln). Dieser Punkt ist wichtig bei der Implementierung von setjmp, das später beschrieben wird.
SETJMP / LONGJMP
Und schließlich, wenn Sie zumindest irgendwie verstehen, wie die Stapel und Register in Elbrus angeordnet sind, können Sie damit beginnen, etwas Nützliches zu tun, dh Embox neue Funktionen hinzuzufügen.
In Embox benötigt das Unit-Testing-System setjmp / longjmp, daher mussten wir diese Funktionen implementieren.
Für die Implementierung müssen die Register CR0, CR1, PSP, PCSP, USD gespeichert / wiederhergestellt werden - die uns bereits aus einer kurzen Einführung bekannt sind. Tatsächlich ist das Speichern / Wiederherstellen in unserer Stirn implementiert, aber es gibt eine signifikante Nuance, die in der Beschreibung von Stapeln und Registern häufig angedeutet wurde, nämlich: Stapel sollten synchronisiert werden, da sie sich nicht nur im Speicher, sondern auch in der Registerdatei befinden. Diese Nuance bedeutet, dass Sie sich um mehrere Funktionen kümmern müssen, ohne die nichts funktioniert.
Die erste Funktion besteht darin, Interrupts während des Speicherns und Wiederherstellens zu deaktivieren. Wenn Sie einen Interrupt wiederherstellen, müssen Sie ihn unbedingt verbieten. Andernfalls kann es vorkommen, dass wir den Interrupt-Handler mit halbgeschalteten Stapeln aufrufen (siehe Auspumpen des in der „Kurzbeschreibung“ beschriebenen Austauschs der Registerdateien). Und beim Speichern besteht das Problem darin, dass der Prozessor nach dem Eintreten und Verlassen des Interrupts wieder einen Teil der Registerdatei aus dem RAM austauschen kann (und dies ruiniert die unveränderlichen Bedingungen PSHTP = 0 und PSCHTP = 0, etwas mehr über sie). Aus diesem Grund müssen Interrupts sowohl in setjmp als auch in longjmp deaktiviert werden. Hierbei ist auch zu beachten, dass Spezialisten des MCST uns empfohlen haben, atomare Klammern zu verwenden, anstatt Interrupts zu deaktivieren. Derzeit verwenden wir jedoch die einfachste (für uns verständliche) Implementierung.
Die zweite Funktion bezieht sich auf das Abpumpen / Auspumpen einer Registerdatei aus dem Speicher. Es ist wie folgt. Die Registerdatei hat eine begrenzte Größe und wird daher häufig in den Speicher gepumpt und umgekehrt. Wenn wir also einfach die Werte der PSP- und PSHTP-Register speichern, legen wir den Wert des aktuellen Zeigers im Speicher und in der Registerdatei fest. Da sich die Registerdatei jedoch ändert, werden zum Zeitpunkt der Kontextwiederherstellung bereits falsche (nicht die von uns "gespeicherten") Daten angezeigt. Um dies zu vermeiden, müssen Sie die gesamte Registerdatei in den Speicher leeren. Wenn wir also in setjmp speichern, haben wir PSP.ind-Register im Speicher und PSHTP.ind-Register im Registerfenster. Es stellt sich heraus, dass Sie die gesamten Register PCSP.ind + PCSHTP.ind speichern müssen. Die folgende Funktion führt diese Operation aus:
.type update_pcsp_ind,@function $update_pcsp_ind: setwd wsz = 0x4, nfx = 0x0 shld %dr1, (64 - 10), %dr1 shrd %dr1, (64 - 10), %dr1 addd %dr1, %dr0, %dr0 E2K_ASM_RETURN
Es ist auch notwendig, einen kleinen Punkt in diesem im Kommentar beschriebenen Code zu verdeutlichen, nämlich das Zeichen im Index PCSHTP.ind programmgesteuert zu erweitern, da der Index negativ sein und in zusätzlichem Code gespeichert werden kann. Dazu wechseln wir zuerst zu (64-10) nach links (64-Bit-Register), zu einem 10-Bit-Feld und dann zurück.
Gleiches gilt für die PSP (Procedure Stack)
.type update_psp_ind,@function $update_psp_ind: setwd wsz = 0x4, nfx = 0x0 shld %dr1, (64 - 12), %dr1 shrd %dr1, (64 - 12), %dr1 muld %dr1, 2, %dr1 addd %dr1, %dr0, %dr0 E2K_ASM_RETURN
Mit einem kleinen Unterschied (das Feld ist 12 Bit, und die Register werden dort in 128-Bit-Begriffen gezählt, dh der Wert muss mit 2 multipliziert werden).
Setjmp Code selbst
C_ENTRY(setjmp): setwd wsz = 0x14, nfx = 0x0 setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr9 rrd %cr0.hi, %dr1 rrd %cr1.lo, %dr2 rrd %cr1.hi, %dr3 rrd %usd.lo, %dr4 rrd %usd.hi, %dr5 rrd %psp.hi, %dr6 rrd %pshtp, %dr7 addd 0, %dr6, %db[0] addd 0, %dr7, %db[1] disp %ctpr1, update_psp_ind ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr6 rrd %pcsp.hi, %dr7 rrd %pcshtp, %dr8 addd 0, %dr7, %db[0] addd 0, %dr8, %db[1] disp %ctpr1, update_pcsp_ind ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr7 std %dr1, [%dr0 + E2K_JMBBUFF_CR0_HI] std %dr2, [%dr0 + E2K_JMBBUFF_CR1_LO] std %dr3, [%dr0 + E2K_JMBBUFF_CR1_HI] std %dr4, [%dr0 + E2K_JMBBUFF_USD_LO] std %dr5, [%dr0 + E2K_JMBBUFF_USD_HI] std %dr6, [%dr0 + E2K_JMBBUFF_PSP_HI] std %dr7, [%dr0 + E2K_JMBBUFF_PCSP_HI] addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 adds 0, 0, %r0 E2K_ASM_RETURN
Bei der Implementierung von longjmp ist es wichtig, die Synchronisation beider Registerdateien nicht zu vergessen. Daher müssen Sie nicht nur das Registerfenster (flushr), sondern auch die Binderdatei (flushc) leeren. Beschreiben wir das Makro:
#define E2K_ASM_FLUSH_CPU \ flushr; \ nop 2; \ flushc; \ nop 3;
Jetzt, da alle Informationen im Speicher sind, können wir die Wiederherstellung in longjmp sicher registrieren.
C_ENTRY(longjmp): setwd wsz = 0x14, nfx = 0x0 setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr9 E2K_ASM_FLUSH_CPU ldd [%dr0 + E2K_JMBBUFF_CR0_HI], %dr2 ldd [%dr0 + E2K_JMBBUFF_CR1_LO], %dr3 ldd [%dr0 + E2K_JMBBUFF_CR1_HI], %dr4 ldd [%dr0 + E2K_JMBBUFF_USD_LO], %dr5 ldd [%dr0 + E2K_JMBBUFF_USD_HI], %dr6 ldd [%dr0 + E2K_JMBBUFF_PSP_HI], %dr7 ldd [%dr0 + E2K_JMBBUFF_PCSP_HI], %dr8 rwd %dr2, %cr0.hi rwd %dr3, %cr1.lo rwd %dr4, %cr1.hi rwd %dr5, %usd.lo rwd %dr6, %usd.hi rwd %dr7, %psp.hi rwd %dr8, %pcsp.hi addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 adds 0, %r1, %r0 E2K_ASM_RETURN
Kontextwechsel
Nachdem wir setjmp / longjmp herausgefunden hatten, schien uns die grundlegende Implementierung von context_switch klar genug zu sein. In der Tat müssen wir wie im ersten Fall die Register der Verbindungsinformationen und Stapel speichern / wiederherstellen und das Prozessorstatusregister (UPSR) korrekt wiederherstellen.
Ich werde es erklären. Wie im Fall von setjmp müssen Sie beim Speichern von Registern zuerst die Registerdatei und die Bindungsinformationsdatei in den Speicher zurücksetzen (flushr + flushc). Danach müssen wir die aktuellen Werte der Register CR0 und CR1 speichern, damit wir bei unserer Rückkehr genau dorthin springen, von wo der aktuelle Stream umgeschaltet wurde. Als Nächstes speichern wir die Deskriptoren der PS-, PCS- und US-Stapel. Und schließlich müssen Sie für die korrekte Wiederherstellung des Interrupt-Modus sorgen - für diese Zwecke speichern wir auch das UPSR-Register.
Assembler-Code context_switch:
C_ENTRY(context_switch): setwd wsz = 0x10, nfx = 0x0 rrd %upsr, %dr2 std %dr2, [%dr0 + E2K_CTX_UPSR] rrd %upsr, %dr2 andnd %dr2, (UPSR_IE | UPSR_NMIE), %dr2 rwd %dr2, %upsr E2K_ASM_FLUSH_CPU rrd %cr0.lo, %dr2 rrd %cr0.hi, %dr3 rrd %cr1.lo, %dr4 rrd %cr1.hi, %dr5 std %dr2, [%dr0 + E2K_CTX_CR0_LO] std %dr3, [%dr0 + E2K_CTX_CR0_HI] std %dr4, [%dr0 + E2K_CTX_CR1_LO] std %dr5, [%dr0 + E2K_CTX_CR1_HI] rrd %usd.lo, %dr3 rrd %usd.hi, %dr4 rrd %psp.lo, %dr5 rrd %psp.hi, %dr6 rrd %pcsp.lo, %dr7 rrd %pcsp.hi, %dr8 std %dr3, [%dr0 + E2K_CTX_USD_LO] std %dr4, [%dr0 + E2K_CTX_USD_HI] std %dr5, [%dr0 + E2K_CTX_PSP_LO] std %dr6, [%dr0 + E2K_CTX_PSP_HI] std %dr7, [%dr0 + E2K_CTX_PCSP_LO] std %dr8, [%dr0 + E2K_CTX_PCSP_HI] ldd [%dr1 + E2K_CTX_CR0_LO], %dr2 ldd [%dr1 + E2K_CTX_CR0_HI], %dr3 ldd [%dr1 + E2K_CTX_CR1_LO], %dr4 ldd [%dr1 + E2K_CTX_CR1_HI], %dr5 rwd %dr2, %cr0.lo rwd %dr3, %cr0.hi rwd %dr4, %cr1.lo rwd %dr5, %cr1.hi ldd [%dr1 + E2K_CTX_USD_LO], %dr3 ldd [%dr1 + E2K_CTX_USD_HI], %dr4 ldd [%dr1 + E2K_CTX_PSP_LO], %dr5 ldd [%dr1 + E2K_CTX_PSP_HI], %dr6 ldd [%dr1 + E2K_CTX_PCSP_LO], %dr7 ldd [%dr1 + E2K_CTX_PCSP_HI], %dr8 rwd %dr3, %usd.lo rwd %dr4, %usd.hi rwd %dr5, %psp.lo rwd %dr6, %psp.hi rwd %dr7, %pcsp.lo rwd %dr8, %pcsp.hi ldd [%dr1 + E2K_CTX_UPSR], %dr2 rwd %dr2, %upsr E2K_ASM_RETURN
Ein weiterer wichtiger Punkt ist die Initialisierung des Betriebssystem-Threads. In Embox hat jeder Thread eine bestimmte primäre Prozedur
void _NORETURN thread_trampoline(void);
in dem alle weiteren Arbeiten des Streams ausgeführt werden. Daher müssen wir die Stapel irgendwie für den Aufruf dieser Funktion vorbereiten. Hier sehen wir uns mit der Tatsache konfrontiert, dass es drei Stapel gibt, die nicht in die gleiche Richtung wachsen. Durch die Architektur erstellen wir einen Stream mit einem einzelnen Stapel, oder besser gesagt, er hat eine einzelne Stelle unter dem Stapel. Oben haben wir eine Struktur, die den Stream selbst beschreibt. Hier mussten wir uns um verschiedene Stapel kümmern, um nicht zu vergessen, dass sie ausgerichtet werden sollten 4 kB, vergessen Sie nicht alle Arten von Zugriffsrechten und so weiter.
Aus diesem Grund haben wir im Moment beschlossen, den Platz unter dem Stapel in drei Teile zu unterteilen, ein Viertel unter dem Stapel verbindlicher Informationen, ein Viertel unter dem prozeduralen Stapel und die Hälfte unter dem Benutzerstapel.
Ich bringe den Code mit, damit Sie beurteilen können, wie groß er ist. Sie müssen berücksichtigen, dass dies eine minimale Initialisierung ist. #define E2K_STACK_ALIGN (1UL << 12) #define round_down(x, bound) ((x) & ~((bound) - 1)) /* Reserve 1/4 for PSP stack, 1/4 for PCSP stack, and 1/2 for USD stack */ #define PSP_CALC_STACK_BASE(sp, size) binalign_bound(sp - size, E2K_STACK_ALIGN) #define PSP_CALC_STACK_SIZE(sp, size) binalign_bound((size) / 4, E2K_STACK_ALIGN) #define PCSP_CALC_STACK_BASE(sp, size) \ (PSP_CALC_STACK_BASE(sp, size) + PSP_CALC_STACK_SIZE(sp, size)) #define PCSP_CALC_STACK_SIZE(sp, size) binalign_bound((size) / 4, E2K_STACK_ALIGN) #define USD_CALC_STACK_BASE(sp, size) round_down(sp, E2K_STACK_ALIGN) #define USD_CALC_STACK_SIZE(sp, size) \ round_down(USD_CALC_STACK_BASE(sp, size) - PCSP_CALC_STACK_BASE(sp, size),\ E2K_STACK_ALIGN) static void e2k_calculate_stacks(struct context *ctx, uint64_t sp, uint64_t size) { uint64_t psp_size, pcsp_size, usd_size; log_debug("Stacks:\n"); ctx->psp_lo |= PSP_CALC_STACK_BASE(sp, size) << PSP_BASE; ctx->psp_lo |= E2_RWAR_RW_ENABLE << PSP_RW; psp_size = PSP_CALC_STACK_SIZE(sp, size); assert(psp_size); ctx->psp_hi |= psp_size << PSP_SIZE; log_debug(" PSP.base=0x%lx, PSP.size=0x%lx\n", PSP_CALC_STACK_BASE(sp, size), psp_size); ctx->pcsp_lo |= PCSP_CALC_STACK_BASE(sp, size) << PCSP_BASE; ctx->pcsp_lo |= E2_RWAR_RW_ENABLE << PCSP_RW; pcsp_size = PCSP_CALC_STACK_SIZE(sp, size); assert(pcsp_size); ctx->pcsp_hi |= pcsp_size << PCSP_SIZE; log_debug(" PCSP.base=0x%lx, PCSP.size=0x%lx\n", PCSP_CALC_STACK_BASE(sp, size), pcsp_size); ctx->usd_lo |= USD_CALC_STACK_BASE(sp, size) << USD_BASE; usd_size = USD_CALC_STACK_SIZE(sp, size); assert(usd_size); ctx->usd_hi |= usd_size << USD_SIZE; log_debug(" USD.base=0x%lx, USD.size=0x%lx\n", USD_CALC_STACK_BASE(sp, size), usd_size); } static void e2k_calculate_crs(struct context *ctx, uint64_t routine_addr) { uint64_t usd_size = (ctx->usd_hi >> USD_SIZE) & USD_SIZE_MASK; /* Reserve space in hardware stacks for @routine_addr */ /* Remark: We do not update psp.hi to reserve space for arguments, * since routine do not accepts any arguments. */ ctx->pcsp_hi |= SZ_OF_CR0_CR1 << PCSP_IND; ctx->cr0_hi |= (routine_addr >> CR0_IP) << CR0_IP; ctx->cr1_lo |= PSR_ALL_IRQ_ENABLED << CR1_PSR; /* Divide on 16 because it field contains size in terms * of 128 bit values. */ ctx->cr1_hi |= (usd_size >> 4) << CR1_USSZ; } void context_init(struct context *ctx, unsigned int flags, void (*routine_fn)(void), void *sp, unsigned int stack_size) { memset(ctx, 0, sizeof(*ctx)); e2k_calculate_stacks(ctx, sp, stack_size); e2k_calculate_crs(ctx, (uint64_t) routine_fn); if (!(flags & CONTEXT_IRQDISABLE)) { ctx->upsr |= (UPSR_IE | UPSR_NMIE); } }
Der Artikel enthielt auch Arbeiten mit Interrupts, Ausnahmen und Timern, aber da er so groß ausfiel, beschlossen wir, im
nächsten Teil darüber zu sprechen.
Nur für den Fall, ich wiederhole, dieses Material ist keine offizielle Dokumentation! Für offizielle Unterstützung, Dokumentation und den Rest müssen Sie sich direkt an das ICST wenden. Der Code in
Embox ist natürlich offen, aber um ihn zu kompilieren, benötigen Sie einen Cross-Compiler, der wiederum vom
MCST bezogen werden kann .