Escalade d'Elbrus - Reconnaissance au combat. Partie technique 1. Registres, piles et autres détails techniques

Comme promis , nous continuons de parler du développement des processeurs Elbrus . Cet article est technique. Les informations données dans l'article ne sont pas des documents officiels, car elles ont été obtenues lors de l'étude d'Elbrus un peu comme une boßte noire. Mais cela sera certainement intéressant pour une meilleure compréhension de l'architecture d'Elbrus, car bien que nous disposions d'une documentation officielle, de nombreux détails ne sont devenus clairs qu'aprÚs de longues expériences, quand Embox a fonctionné.

Rappelez-vous que dans l' article précédent, nous avons parlé du démarrage du systÚme de base et du pilote de port série. Embox a commencé, mais pour avancer, nous avions besoin d'interruptions, d'une minuterie systÚme et, bien sûr, je voudrais inclure un ensemble de tests unitaires, et pour cela, nous avons besoin de setjmp. Cet article se concentrera sur les registres, les piles et autres détails techniques nécessaires pour implémenter toutes ces choses.

Commençons par une brĂšve introduction Ă  l'architecture, qui est le minimum d'informations nĂ©cessaires pour comprendre ce qui sera discutĂ© plus tard. À l'avenir, nous nous rĂ©fĂ©rerons aux informations de cette section.

BrĂšve introduction: piles


Il y a trois piles dans Elbrus:

  • Pile de procĂ©dures (PS)
  • Pile de chaĂźne de procĂ©dure (PCS)
  • Pile utilisateur (US)

Analysons-les plus en détail. Les adresses sur la figure sont conditionnelles, montrent dans quelle direction les mouvements sont dirigés - d'une adresse plus grande à une plus petite ou vice versa.



La pile de procédures (PS) est destinée aux données allouées aux registres «opérationnels».

Par exemple, il peut s'agir d'arguments fonctionnels: dans les architectures «ordinaires», ce concept est le plus proche des registres à usage général. Contrairement aux architectures de processeur «normales», dans E2K, les registres utilisés dans les fonctions sont empilés sur une pile distincte.

La pile d'informations de liaison (PCS) est conçue pour placer des informations sur la procédure (d'appel) précédente et utilisée lors du retour. Les données sur l'adresse de retour, ainsi que dans le cas des registres, sont placées dans un endroit séparé. Par conséquent, la promotion de la pile (par exemple, pour quitter par exception en C ++) est un processus plus long que dans les architectures «ordinaires». D'un autre cÎté, cela élimine les problÚmes de débordement de pile.

Ces deux piles (PS et PCS) sont caractĂ©risĂ©es par une adresse de base, une taille et un dĂ©calage actuel. Ces paramĂštres sont dĂ©finis dans les registres PSP et PCSP, ils sont 128 bits et dans l'assembleur, vous devez vous rĂ©fĂ©rer Ă  des champs spĂ©cifiques (par exemple, haut ou bas). De plus, le fonctionnement des piles est Ă©troitement liĂ© au concept de fichier de registre, plus sur celui ci-dessous. L'interaction avec le fichier se produit via le mĂ©canisme de pompage / Ă©change de registres. Un rĂŽle actif dans ce mĂ©canisme est jouĂ© par le soi-disant «pointeur matĂ©riel vers le haut de la pile» de la procĂ©dure ou pile d'informations de liaison, respectivement. À ce sujet Ă©galement ci-dessous. Il est important qu'Ă  chaque instant les donnĂ©es de ces piles soient soit en RAM soit dans un fichier registre.

Il convient également de noter que ces piles (la pile procédurale et la pile d'informations de liaison) se développent. Nous l'avons rencontré lorsque nous avons implémenté context_switch.

La pile d'utilisateurs reçoit Ă©galement l'adresse et la taille de base. Le pointeur actuel est dans le registre USD.lo. À la base, c'est une pile classique qui grandit. Seulement, contrairement aux architectures «ordinaires», les informations provenant d'autres piles (registres et adresses de retour) ne s'y adaptent pas.

À mon avis, une exigence non standard pour les limites et les tailles des piles est l'alignement 4K, et l'adresse de base de la pile et sa taille doivent ĂȘtre alignĂ©es sur 4K. Dans d'autres architectures, je n'ai pas rencontrĂ© une telle restriction. Nous avons rencontrĂ© ce dĂ©tail, encore une fois, lorsque nous avons implĂ©mentĂ© context_switch.

BrĂšve introduction: Registres. Enregistrez les fichiers. Enregistrer les fenĂȘtres


Maintenant que nous avons un peu compris les piles, nous devons comprendre comment les informations y sont présentées. Pour ce faire, nous devons introduire quelques concepts supplémentaires.

Un fichier de registre (RF) est un ensemble de tous les registres. Il y a deux fichiers de registre dont nous avons besoin: un fichier d'informations de connexion (fichier de chaßne - CF), l'autre est appelé fichier de registre (RF), il stocke des registres «opérationnels», qui sont stockés sur la pile procédurale.

La fenĂȘtre de registre est la zone (ensemble de registres) du fichier de registre actuellement disponible.

Je vais vous expliquer plus en détail. Qu'est-ce qu'un ensemble de registres, je pense, que personne n'a besoin d'expliquer.

Il est bien connu que l'un des goulots d'Ă©tranglement dans l'architecture x86 est prĂ©cisĂ©ment un petit nombre de registres. Dans les architectures RISC avec registres, c'est plus simple, gĂ©nĂ©ralement autour de 16 registres, dont plusieurs (2-3) sont occupĂ©s pour les besoins officiels. Pourquoi ne pas simplement crĂ©er 128 registres, car il semblerait que cela augmentera les performances du systĂšme? La rĂ©ponse est assez simple: une instruction de processeur a besoin d'un endroit pour stocker l'adresse du registre, et s'il y en a beaucoup, beaucoup de bits sont Ă©galement nĂ©cessaires pour cela. Par consĂ©quent, ils passent Ă  toutes sortes de trucs, crĂ©ent des registres fantĂŽmes, enregistrent des banques, des fenĂȘtres, etc. Par registres fictifs, j'entends le principe de l'organisation des registres dans ARM. Si une interruption ou une autre situation se produit, alors un ensemble diffĂ©rent de registres portant les mĂȘmes noms (numĂ©ros) est disponible, tandis que les informations stockĂ©es dans l'ensemble d'origine restent lĂ . Les banques de registres, en fait, sont trĂšs similaires aux registres fictifs, il n'y a tout simplement pas de commutation matĂ©rielle des ensembles de registres, et le programmeur choisit la banque (ensemble de registres) Ă  contacter maintenant.

Les fenĂȘtres de registre sont conçues pour optimiser le travail avec la pile. Comme vous le comprenez probablement, dans une architecture «normale», vous entrez une procĂ©dure, enregistrez les registres dans la pile (ou la procĂ©dure d'appel enregistre, dĂ©pend de l'accord) et vous pouvez utiliser des registres, car les informations sont dĂ©jĂ  stockĂ©es sur la pile. Mais l'accĂšs Ă  la mĂ©moire est lent et doit donc ĂȘtre Ă©vitĂ©. Lorsque vous entrez dans la procĂ©dure, rendons simplement disponible un nouvel ensemble de registres, les donnĂ©es de l'ancien seront enregistrĂ©es, ce qui signifie que vous n'avez pas besoin de le vider en mĂ©moire. De plus, lorsque vous revenez Ă  la procĂ©dure d'appel, la fenĂȘtre de registre prĂ©cĂ©dente renverra Ă©galement, par consĂ©quent, toutes les donnĂ©es sur les registres seront pertinentes. C'est le concept d'une fenĂȘtre de registre.



Il est clair que vous devez encore enregistrer les registres sur la pile (en mĂ©moire), mais cela peut ĂȘtre fait lorsque les fenĂȘtres de registres libres sont terminĂ©es.

Et que faire des registres d'entrĂ©e et de sortie (arguments lors de la saisie de la fonction et du rĂ©sultat renvoyĂ©)? Laissez la fenĂȘtre contenir une partie des registres visibles depuis la fenĂȘtre prĂ©cĂ©dente, plus prĂ©cisĂ©ment, une partie des registres sera disponible pour les deux fenĂȘtres. Ensuite, en gĂ©nĂ©ral, lors de l'appel de la fonction, vous n'avez pas besoin d'accĂ©der Ă  la mĂ©moire. Supposons que nos registres ressemblent Ă  ceci



C'est-Ă -dire que r0 dans la premiĂšre fenĂȘtre sera le mĂȘme registre que r2 Ă  zĂ©ro et r1 de la premiĂšre fenĂȘtre dans le mĂȘme registre que r3. Autrement dit, en Ă©crivant en r2 avant d'appeler la procĂ©dure (en changeant le numĂ©ro de fenĂȘtre), nous obtenons la valeur en r0 dans la procĂ©dure appelĂ©e. Ce principe est appelĂ© mĂ©canisme de rotation des fenĂȘtres.

Optimisons un peu plus, car les crĂ©ateurs d'Elbrus l'ont fait. Soit les fenĂȘtres que nous avons ne seront pas de taille fixe, mais variable, la taille de la fenĂȘtre peut ĂȘtre dĂ©finie au moment de l'entrĂ©e dans la procĂ©dure. Nous ferons de mĂȘme avec le nombre de registres tournĂ©s. Bien sĂ»r, cela nous conduira Ă  certains problĂšmes, car si dans les fenĂȘtres rotatives classiques, il existe un index de fenĂȘtre Ă  travers lequel il est dĂ©terminĂ© que vous devez enregistrer les donnĂ©es du fichier de registre sur la pile ou les charger. Mais si vous entrez non pas l'index de la fenĂȘtre, mais l'index du registre Ă  partir duquel notre fenĂȘtre actuelle dĂ©marre, ce problĂšme ne se posera pas. Dans Elbrus, ces indices sont contenus dans les registres PSHTP (pour la pile de procĂ©dures PS) et PCSHTP (pour la pile d'informations procĂ©durales PCS). La documentation fait rĂ©fĂ©rence aux «pointeurs matĂ©riels vers le haut de la pile». Maintenant, vous pouvez rĂ©essayer de lire sur les piles, je pense que ce sera plus clair.

Comme vous le comprenez, un tel mĂ©canisme implique que vous avez la possibilitĂ© de contrĂŽler ce qui est en mĂ©moire. Autrement dit, synchronisez le fichier de registre et la pile. Je veux dire un programmeur systĂšme. Si vous ĂȘtes un programmeur d'applications, l'Ă©quipement fournira une entrĂ©e et une sortie transparentes de la procĂ©dure. Autrement dit, s'il n'y a pas suffisamment de registres lorsque vous essayez de sĂ©lectionner une nouvelle fenĂȘtre, la fenĂȘtre de registre se «pompera» automatiquement. Dans ce cas, toutes les donnĂ©es du fichier de registre seront enregistrĂ©es sur la pile appropriĂ©e (en mĂ©moire) et le «pointeur vers le haut du matĂ©riel de la pile» (index de dĂ©calage) sera remis Ă  zĂ©ro. De mĂȘme, l'Ă©change d'un fichier de registre de la pile se produit automatiquement. Mais si vous dĂ©veloppez, par exemple, le changement de contexte, ce qui est exactement ce que nous avons fait, alors vous avez besoin d'un mĂ©canisme pour travailler avec la partie cachĂ©e du fichier de registre. Dans Elbrus, les commandes FLUSHR et FLUSHC sont utilisĂ©es pour cela. FLUSHR - effacement du fichier de registre, toutes les fenĂȘtres Ă  l'exception de la fenĂȘtre actuelle sont vidĂ©es dans la pile procĂ©durale, l'index PSHTP est par consĂ©quent remis Ă  zĂ©ro. FLUSHC - effacement du fichier d'informations de liaison, tout sauf la fenĂȘtre actuelle est vidĂ© sur la pile d'informations de liaison, l'index PCSHTP est Ă©galement remis Ă  zĂ©ro.

Brùve introduction: mise en Ɠuvre dans Elbrus


Maintenant que nous avons discuté du travail non évident avec les registres et les piles, nous parlerons plus spécifiquement de diverses situations dans Elbrus.

Lorsque nous entrons dans la fonction suivante, le processeur crĂ©e deux fenĂȘtres: une fenĂȘtre sur la pile PS et une fenĂȘtre sur la pile PCS.

Une fenĂȘtre de la pile PCS contient les informations nĂ©cessaires pour revenir d'une fonction: par exemple, IP (Instruction Pointer) de l'instruction oĂč vous devrez revenir de la fonction. Avec cela, tout est plus ou moins clair.

La fenĂȘtre de la pile PS est un peu plus dĂ©licate. Le concept de registres de la fenĂȘtre courante est introduit. Dans cette fenĂȘtre, vous aurez accĂšs aux registres de la fenĂȘtre actuelle -% dr0,% dr1, ...,% dr15, ... Autrement dit, pour nous, en tant qu'utilisateur, ils sont toujours numĂ©rotĂ©s Ă  partir de 0, mais c'est une numĂ©rotation relative Ă  l'adresse de base de la fenĂȘtre actuelle. Par le biais de ces registres, les arguments sont transmis lors de l'appel de la fonction, la valeur est renvoyĂ©e et la fonction est utilisĂ©e comme registre Ă  usage gĂ©nĂ©ral dans la fonction. En fait, cela a Ă©tĂ© expliquĂ© lors de l'examen du mĂ©canisme de rotation des fenĂȘtres de registre.

La taille de la fenĂȘtre d'enregistrement dans Elbrus peut ĂȘtre contrĂŽlĂ©e. Comme je l'ai dit, cela est nĂ©cessaire Ă  l'optimisation. Par exemple, dans une fonction, nous n'avons besoin que de 4 registres pour passer des arguments et certains calculs, dans ce cas, le programmeur (ou le compilateur) dĂ©cide du nombre de registres Ă  allouer pour la fonction, et en fonction de cela, il dĂ©finit la taille de la fenĂȘtre. La taille de la fenĂȘtre est dĂ©finie par l'opĂ©ration setwd:

setwd wsz=0x10 

SpĂ©cifie la taille de la fenĂȘtre en termes de registres quadruples (registres 128 bits).



Supposons maintenant que vous souhaitiez appeler une fonction Ă  partir d'une fonction. Pour cela, le concept dĂ©jĂ  dĂ©crit d'une fenĂȘtre de registre tournĂ© est appliquĂ©. L'image ci-dessus montre un fragment d'un fichier de registre oĂč une fonction avec fenĂȘtre 1 (verte) appelle une fonction avec fenĂȘtre 2 (orange). Dans chacune de ces deux fonctions, vous aurez accĂšs Ă  vos% dr0,% dr1, ... Mais les arguments seront passĂ©s par les registres dits rotatifs. En d'autres termes, une partie des registres de la fenĂȘtre 1 deviendra les registres de la fenĂȘtre 2 (notez que ces deux fenĂȘtres se croisent). Ces registres sont Ă©galement dĂ©finis par la fenĂȘtre (voir Registres rotatifs dans l'image) et ont l'adresse% db [0],% db [1], ... Ainsi, le registre% dr0 dans la fenĂȘtre 2 n'est rien de plus que le registre% db [0] dans fenĂȘtre 1.

La fenĂȘtre du registre de rotation est dĂ©finie par l'opĂ©ration setbn:

  setbn rbs = 0x3, rsz = 0x8 

rbs dĂ©finit la taille de la fenĂȘtre pivotĂ©e et rsz dĂ©finit l'adresse de base, mais par rapport Ă  la fenĂȘtre de registre actuelle. C'est-Ă -dire Ici, nous avons allouĂ© 3 registres, Ă  partir du 8.

Sur la base de ce qui précÚde, nous montrons à quoi ressemble l'appel de fonction. Pour simplifier, nous supposons que la fonction prend un argument:

 void my_func(uint64_t a) { } 

Ensuite, pour appeler cette fonction, vous devez prĂ©parer une fenĂȘtre de registres rotatifs (nous l'avons dĂ©jĂ  fait via setbn). Ensuite, dans le registre% db0, nous mettons la valeur qui sera transmise Ă  my_func. AprĂšs cela, vous devez appeler l'instruction CALL et n'oubliez pas de lui dire oĂč commence la fenĂȘtre des registres tournĂ©s. Nous sautons maintenant la prĂ©paration de l'appel (la commande disp), car il ne respecte pas la casse. Par consĂ©quent, dans l'assembleur, un appel Ă  cette fonction devrait ressembler Ă  ceci:

  addd 0, %dr9, %db[0] disp %ctpr1, my_func call %ctpr1, wbs = 0x8 

Donc, avec des registres un peu compris. Examinons maintenant la pile d'informations de liaison. Il stocke les registres dits CR. En fait, deux - CR0, CR1. Et ils contiennent déjà les informations nécessaires au retour de la fonction.



Les registres CR0 et CR1 de la fenĂȘtre de la fonction qui a appelĂ© la fonction avec les registres marquĂ©s en orange sont verts. Les registres CR0 contiennent le pointeur d'instruction de la fonction appelante et un certain fichier de prĂ©dicat (PF-Predicate File), une histoire Ă  ce sujet dĂ©passe dĂ©finitivement le cadre de cet article.

Les registres CR1 contiennent des donnĂ©es telles que PSR (Ă©tat du traitement de texte), numĂ©ro de fenĂȘtre, tailles de fenĂȘtre, etc. Dans Elbrus, tout est si flexible que chaque procĂ©dure stocke des informations dans CR1 mĂȘme si une opĂ©ration Ă  virgule flottante est incluse dans la procĂ©dure, et un registre contenant des informations sur les exceptions logicielles, mais pour cela, bien sĂ»r, vous devez payer pour enregistrer des informations supplĂ©mentaires.

Il est trĂšs important de ne pas oublier que le fichier de registre et le fichier d'informations de liaison peuvent ĂȘtre pompĂ©s et Ă©changĂ©s hors de la mĂ©moire principale et vice versa (Ă  partir des piles PS et PCS dĂ©crites ci-dessus). Ce point est important lors de l'implĂ©mentation de setjmp dĂ©crit plus loin.

SETJMP / LONGJMP


Et enfin, au moins en comprenant comment les piles et les registres sont organisés dans Elbrus, vous pouvez commencer à faire quelque chose d'utile, c'est-à-dire ajouter de nouvelles fonctionnalités à Embox.

Dans Embox, le systÚme de test unitaire nécessite setjmp / longjmp, nous avons donc dû implémenter ces fonctions.

Pour la mise en Ɠuvre, il est nĂ©cessaire de sauvegarder / restaurer les registres: CR0, CR1, PSP, PCSP, USD, - dĂ©jĂ  familiers pour nous d'une brĂšve introduction. En fait, la sauvegarde / restauration est implĂ©mentĂ©e dans notre front, mais il y a une nuance importante qui a souvent Ă©tĂ© suggĂ©rĂ©e dans la description des piles et des registres, Ă  savoir: les piles doivent ĂȘtre synchronisĂ©es, car elles se trouvent non seulement dans la mĂ©moire, mais aussi dans le fichier de registre. Cette nuance signifie que vous devez prendre soin de plusieurs fonctionnalitĂ©s, sans lesquelles rien ne fonctionnera.

La premiĂšre fonction consiste Ă  dĂ©sactiver les interruptions lors de l'enregistrement et de la restauration. Lors de la restauration d'une interruption, il est obligatoire d'interdire, sinon, une situation peut survenir dans laquelle nous entrons dans le gestionnaire d'interruption avec des piles Ă  demi commutĂ©es (en rĂ©fĂ©rence au pompage du swap de fichier de registre dĂ©crit dans la «courte description»). Et lors de l'enregistrement, le problĂšme est qu'aprĂšs avoir entrĂ© et quittĂ© l'interruption, le processeur peut Ă  nouveau Ă©changer une partie du fichier de registre de la RAM (et cela ruinera les conditions invariantes PSHTP = 0 et PSSHTP = 0, un peu plus Ă  leur sujet). C'est pourquoi, dans setjmp et longjmp, les interruptions doivent ĂȘtre dĂ©sactivĂ©es. Il convient Ă©galement de noter ici que les spĂ©cialistes du MCST nous ont conseillĂ© d'utiliser des crochets atomiques au lieu de dĂ©sactiver les interruptions, mais pour l'instant nous utilisons la mise en Ɠuvre la plus simple (comprĂ©hensible pour nous).

La deuxiĂšme caractĂ©ristique est liĂ©e au pompage / pompage d'un fichier de registre depuis la mĂ©moire. C'est comme suit. Le fichier de registre a une taille limitĂ©e et est donc souvent pompĂ© en mĂ©moire et vice versa. Par consĂ©quent, si nous enregistrons simplement les valeurs des registres PSP et PSHTP, alors nous fixerons la valeur du pointeur actuel en mĂ©moire et dans le fichier de registre. Mais comme le fichier de registre change, au moment de la restauration du contexte, il indiquera des donnĂ©es dĂ©jĂ  incorrectes (pas celles que nous avons «enregistrĂ©es»). Pour Ă©viter cela, vous devez vider l'intĂ©gralitĂ© du fichier de registre en mĂ©moire. Ainsi, lors de l'enregistrement dans setjmp, nous avons des registres PSP.ind en mĂ©moire et des registres PSHTP.ind dans la fenĂȘtre de registre. Il s'avĂšre que vous devez sauvegarder tous les registres PCSP.ind + PCSHTP.ind. Voici la fonction qui effectue cette opĂ©ration:

 /* First arg is PCSP, 2nd arg is PCSHTP * Returns new PCSP value with updated PCSP.ind */ .type update_pcsp_ind,@function $update_pcsp_ind: setwd wsz = 0x4, nfx = 0x0 /* Here and below, 10 is size of PCSHTP.ind. Here we * extend the sign of PCSHTP.ind */ shld %dr1, (64 - 10), %dr1 shrd %dr1, (64 - 10), %dr1 /* Finally, PCSP.ind += PCSHTP.ind */ addd %dr1, %dr0, %dr0 E2K_ASM_RETURN 

Il est Ă©galement nĂ©cessaire de clarifier un petit point dans ce code dĂ©crit dans le commentaire, Ă  savoir, il est nĂ©cessaire de dĂ©velopper par programme le caractĂšre dans l'index PCSHTP.ind, car l'index peut ĂȘtre nĂ©gatif et stockĂ© dans du code supplĂ©mentaire. Pour ce faire, nous passons d'abord Ă  (64-10) vers la gauche (registre 64 bits), Ă  un champ de 10 bits, puis Ă  l'arriĂšre.

Il en va de mĂȘme pour la PSP (pile de procĂ©dures)

 /* First arg is PSP, 2nd arg is PSHTP * Returns new PSP value with updated PSP.ind */ .type update_psp_ind,@function $update_psp_ind: setwd wsz = 0x4, nfx = 0x0 /* Here and below, 12 is size of PSHTP.ind. Here we * extend the sign of PSHTP.ind as stated in documentation */ shld %dr1, (64 - 12), %dr1 shrd %dr1, (64 - 12), %dr1 muld %dr1, 2, %dr1 /* Finally, PSP.ind += PSHTP.ind */ addd %dr1, %dr0, %dr0 E2K_ASM_RETURN 

Avec une lĂ©gĂšre diffĂ©rence (le champ est de 12 bits, et les registres y sont comptĂ©s en termes de 128 bits, c'est-Ă -dire que la valeur doit ĂȘtre multipliĂ©e par 2).

Setjmp code lui-mĂȘme

 C_ENTRY(setjmp): setwd wsz = 0x14, nfx = 0x0 /* It's for db[N] registers */ setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 /* We must disable interrupts here */ disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 /* Store current IPL to dr9 */ addd 0, %db[0], %dr9 /* Store some registers to jmp_buf */ rrd %cr0.hi, %dr1 rrd %cr1.lo, %dr2 rrd %cr1.hi, %dr3 rrd %usd.lo, %dr4 rrd %usd.hi, %dr5 /* Prepare RF stack to flush in longjmp */ 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 /* Prepare CF stack to flush in longjmp */ 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] /* Enable interrupts */ addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 /* return 0 */ adds 0, 0, %r0 E2K_ASM_RETURN 

Lors de l'implĂ©mentation de longjmp, il est important de ne pas oublier la synchronisation des deux fichiers de registre, par consĂ©quent, vous devez vider non seulement la fenĂȘtre de registre (flushr), mais aussi vider le fichier de reliure (flushc). DĂ©crivons la macro:

 #define E2K_ASM_FLUSH_CPU \ flushr; \ nop 2; \ flushc; \ nop 3; 

Maintenant que toutes les informations sont en mémoire, nous pouvons en toute sécurité enregistrer la récupération dans longjmp.

 C_ENTRY(longjmp): setwd wsz = 0x14, nfx = 0x0 setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 /* We must disable interrupts here */ disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 /* Store current IPL to dr9 */ addd 0, %db[0], %dr9 /* We have to flush both RF and CF to memory because saved values * of P[C]SHTP can be not valid here. */ E2K_ASM_FLUSH_CPU /* Load registers previously saved in setjmp. */ 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 /* Enable interrupts */ addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 /* Actually, we return to setjmp caller with second * argument of longjmp stored on r1 register. */ adds 0, %r1, %r0 E2K_ASM_RETURN 

Changement de contexte


AprÚs avoir compris setjmp / longjmp, l'implémentation de base de context_switch nous a semblé assez claire. En effet, comme dans le premier cas, nous devons sauvegarder / restaurer les registres des informations de connexion et des piles, plus nous devons restaurer correctement le registre d'état du processeur (UPSR).

Je vais vous expliquer. Comme dans le cas de setjmp, lors de la sauvegarde des registres, vous devez d'abord rĂ©initialiser le fichier de registre et le fichier d'informations de liaison en mĂ©moire (flushr + flushc). AprĂšs cela, nous devons enregistrer les valeurs actuelles des registres CR0 et CR1 de sorte que lorsque nous reviendrons, sautez exactement d'oĂč le flux actuel a Ă©tĂ© commutĂ©. Ensuite, nous enregistrons les descripteurs des piles PS, PCS et US. Et enfin, vous devez prendre soin de la restauration correcte du mode d'interruption - Ă  ces fins, nous enregistrons Ă©galement le registre UPSR.

Code assembleur context_switch:

 C_ENTRY(context_switch): setwd wsz = 0x10, nfx = 0x0 /* Save prev UPSR */ rrd %upsr, %dr2 std %dr2, [%dr0 + E2K_CTX_UPSR] /* Disable interrupts before saving/restoring context */ rrd %upsr, %dr2 andnd %dr2, (UPSR_IE | UPSR_NMIE), %dr2 rwd %dr2, %upsr E2K_ASM_FLUSH_CPU /* Save prev CRs */ 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] /* Save prev stacks */ 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] /* Load next CRs */ 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 /* Load next stacks */ 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 /* Restore next UPSR */ ldd [%dr1 + E2K_CTX_UPSR], %dr2 rwd %dr2, %upsr E2K_ASM_RETURN 

Un autre point important est l'initialisation du thread OS. Dans Embox, chaque thread a une certaine procédure principale

 void _NORETURN thread_trampoline(void); 

dans lequel tous les autres travaux du flux seront exĂ©cutĂ©s. Ainsi, nous devons en quelque sorte prĂ©parer les piles pour appeler cette fonction, c'est ici que nous sommes confrontĂ©s au fait qu'il y a trois piles, et qu'elles ne croissent pas dans la mĂȘme direction. Par architecture, notre flux est créé avec une seule pile, ou plutĂŽt, c'est un seul endroit sous la pile, en haut nous avons une structure qui dĂ©crit le flux lui-mĂȘme et ainsi de suite, ici nous avons dĂ» prendre soin de diffĂ©rentes piles, sans oublier qu'elles doivent ĂȘtre alignĂ©es sur 4 ko, n'oubliez pas toutes sortes de droits d'accĂšs et ainsi de suite.

En conséquence, nous avons décidé pour le moment de diviser l'espace sous la pile en trois parties, un quart sous la pile d'informations de liaison, un quart sous la pile procédurale et la moitié sous la pile utilisateur.

J'apporte le code pour que vous puissiez évaluer sa taille, vous devez considérer qu'il s'agit d'une initialisation minimale.
 /* This value is used for both stack base and size align. */ #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); } } 


L'article contenait également du travail avec des interruptions, des exceptions et des minuteries, mais comme il s'est avéré si important, nous avons décidé d'en parler dans la partie suivante .

Au cas oĂč, je le rĂ©pĂšte, ce matĂ©riel n'est pas une documentation officielle! Pour le support officiel, la documentation et le reste, vous devez contacter directement l'ICST. Le code dans Embox , bien sĂ»r, est ouvert, mais pour le compiler, vous aurez besoin d'un compilateur croisĂ©, qui, encore une fois, peut ĂȘtre obtenu auprĂšs du MCST .

Source: https://habr.com/ru/post/fr447704/


All Articles