Cet article explique comment le noyau du système d'exploitation peut accéder aux trames de mémoire physique. Nous étudierons la fonction de conversion des adresses virtuelles en adresses physiques. Nous découvrirons également comment créer de nouveaux mappages dans les tableaux de pages.
Ce blog est publié sur
GitHub . Si vous avez des questions ou des problèmes, ouvrez-y le ticket correspondant. Toutes les sources de l'article sont
ici .
Présentation
Dans le
dernier article, nous avons découvert les principes de la mémoire de pagination et le fonctionnement des tables de pages à quatre niveaux sur x86_64. Nous avons également constaté que le chargeur avait déjà configuré la hiérarchie des tables de pages pour notre noyau, donc le noyau s'exécute sur des adresses virtuelles. Cela améliore la sécurité, mais le problème se pose: comment accéder aux adresses physiques réelles qui sont stockées dans les entrées de table de pages ou le
CR3
?
Dans la première section de l'article, nous discuterons du problème et des différentes approches pour le résoudre. Ensuite, nous implémentons une fonction qui se faufile à travers la hiérarchie des tables de pages pour convertir des adresses virtuelles en adresses physiques. Enfin, apprenez à créer de nouveaux mappages dans les tableaux de pages et à trouver des cadres de mémoire inutilisés pour créer de nouveaux tableaux.
Mises à jour des dépendances
Pour fonctionner, vous avez besoin de
x86_64
version 0.4.0 ou ultérieure. Mettez à jour la dépendance dans notre
Cargo.toml
:
[dependencies] x86_64 = "0.4.0" # or later
Accès aux tableaux de pages
Accéder aux tables de pages à partir du noyau n'est pas aussi simple qu'il y paraît. Pour comprendre le problème, jetez un autre coup d'œil à la hiérarchie des tables à quatre niveaux de l'article précédent:
L'important est que chaque entrée de page stocke l'adresse
physique du tableau suivant. Cela évite la traduction de ces adresses, ce qui réduit les performances et conduit facilement à des boucles sans fin.
Le problème est que nous ne pouvons pas accéder directement aux adresses physiques à partir du noyau, car cela fonctionne également sur les adresses virtuelles. Par exemple, lorsque nous allons à l'adresse
4 KiB
, nous avons accès à l'adresse
virtuelle 4 KiB
, et non à l'adresse
physique où est stocké le tableau des pages du 4e niveau. Si nous voulons accéder à l'adresse physique de
4 KiB
, nous devons utiliser une adresse virtuelle, qui est traduite en elle.
Par conséquent, pour accéder aux cadres des tables de pages, vous devez mapper certaines pages virtuelles à ces cadres. Il existe différentes façons de créer de tels mappages.
1. Une solution simple est l'
affichage identique de tous les tableaux de pages .
Dans cet exemple, nous voyons l'affichage identique des images. Les adresses physiques des tables de pages sont en même temps des adresses virtuelles valides, afin que nous puissions facilement accéder aux tables de pages de tous les niveaux, à commencer par le registre CR3.
Cependant, cette approche encombre l'espace d'adressage virtuel et rend difficile de trouver de grandes zones contiguës de mémoire libre. Disons que nous voulons créer une zone de mémoire virtuelle de 1000 Ko dans la figure ci-dessus, par exemple, pour
afficher un fichier en mémoire . Nous ne pouvons pas commencer par la région des
28 KiB
, car elle repose sur une page déjà occupée à
1004 KiB
. Par conséquent, vous devrez regarder plus loin jusqu'à ce que nous trouvions un grand fragment approprié, par exemple, avec
1008 KiB
. Il y a le même problème de fragmentation que dans la mémoire segmentée.
De plus, la création de nouveaux tableaux de pages est beaucoup plus compliquée, car nous devons trouver des cadres physiques dont les pages correspondantes ne sont pas encore utilisées. Par exemple, pour notre fichier, nous avons réservé une zone de 1000 Ko de mémoire
virtuelle , à partir de l'adresse
1008 KiB
. Maintenant, nous ne pouvons plus utiliser de trame avec une adresse physique comprise entre
1000 KiB
et
2008 KiB
, car elle ne peut pas être affichée de manière identique.
2. Une autre option consiste à
diffuser des tableaux de pages uniquement temporairement lorsque vous devez y accéder. Pour les comparaisons temporaires, un affichage identique de la seule table de premier niveau est requis:
Sur cette figure, une table de niveau 1 gère les 2 premiers Mo d'espace d'adressage virtuel. Cela est possible car l'accès à partir du registre CR3 se fait par zéro entrée dans les tables des niveaux 4, 3 et 2. L'enregistrement avec l'index
8 traduit la page virtuelle à
32 KiB
en une trame physique à
32 KiB
, identifiant ainsi la table de niveau 1 elle-même. Dans la figure, cela est indiqué par une flèche horizontale.
En écrivant dans la table de niveau 1 à mappage identique, notre noyau peut créer jusqu'à 511 comparaisons temporelles (512 moins l'enregistrement nécessaire pour le mappage d'identité). Dans l'exemple ci-dessus, le noyau correspondait à l'enregistrement nul d'une table de niveau 1 avec une trame à
24 KiB
. Cela a créé un mappage temporaire de la page virtuelle à
0 KiB
au cadre physique du tableau de niveau de la page 2 indiqué par la flèche en pointillés. Maintenant, le noyau peut accéder à la table de niveau 2 en écrivant sur une page qui commence à
0 KiB
.
Ainsi, l'accès à un cadre arbitraire de la table des pages avec des mappages temporaires comprend les actions suivantes:
- Trouvez une entrée gratuite dans le tableau de niveau 1 affiché de manière identique.
- Mappez cette entrée au cadre physique du tableau de pages auquel nous voulons accéder.
- Accédez à ce cadre via la page virtuelle associée à l'entrée.
- Redéfinissez l'enregistrement sur inutilisé, supprimant ainsi le mappage temporaire.
Avec cette approche, l'espace d'adressage virtuel reste propre, car les mêmes 512 pages virtuelles sont constamment utilisées. L'inconvénient est une certaine lourdeur, d'autant plus qu'une nouvelle comparaison peut nécessiter de modifier plusieurs niveaux de la table, c'est-à-dire que nous devons répéter le processus décrit plusieurs fois.
3. Bien que les deux approches ci-dessus fonctionnent, il existe une troisième méthode:
les tableaux de pages récursifs . Il combine les avantages des deux approches: il compare constamment tous les cadres des tables de pages sans nécessiter de comparaisons temporaires, et conserve également les pages mappées côte à côte, évitant la fragmentation de l'espace d'adressage virtuel. C'est la méthode que nous utiliserons.
Tables de pages récursives
L'idée est de traduire certains enregistrements de la table de quatrième niveau en elle-même. Ainsi, nous réservons en fait une partie de l'espace d'adressage virtuel et mappons tous les cadres de table actuels et futurs à cet espace.
Regardons un exemple pour comprendre comment tout cela fonctionne:
La seule différence avec l'exemple au début de l'article est un enregistrement supplémentaire avec l'index
511
dans la table de niveau 4, qui est mappé à la trame physique
4 KiB
, qui se trouve dans cette table elle-même.
Lorsque le processeur va sur cet enregistrement, il ne fait pas référence à la table de niveau 3, mais se réfère à nouveau à la table de niveau 4. Ceci est similaire à une fonction récursive qui s'appelle elle-même. Il est important que le processeur suppose que chaque entrée de la table de niveau 4 pointe vers une table de niveau 3. Par conséquent, il traite maintenant la table de niveau 4 comme une table de niveau 3. Cela fonctionne car les tables de tous les niveaux dans x86_64 ont la même structure.
En suivant une ou plusieurs fois un enregistrement récursif avant de démarrer la conversion réelle, nous pouvons effectivement réduire le nombre de niveaux que le processeur traverse. Par exemple, si nous suivons une fois l'enregistrement récursif, puis passons à la table de niveau 3, le processeur pense que la table de niveau 3 est une table de niveau 2. En poursuivant, il considère la table de niveau 2 comme une table de niveau 1 et la table de niveau 1 comme mappée trame dans la mémoire physique. Cela signifie que nous pouvons maintenant lire et écrire dans la table de niveau de page 1 car le processeur pense qu'il s'agit d'un cadre mappé. La figure ci-dessous montre les cinq étapes d'une telle traduction:
De même, nous pouvons suivre une entrée récursive deux fois avant de démarrer la conversion pour réduire le nombre de niveaux passés à deux:
Passons en revue cette procédure étape par étape. Tout d'abord, le CPU suit une entrée récursive dans la table de niveau 4 et pense qu'il a atteint la table de niveau 3. Ensuite, il suit à nouveau l'enregistrement récursif et pense qu'il a atteint le niveau 2. Mais en réalité, il est toujours au niveau 4. Ensuite, le CPU va à la nouvelle adresse et entre dans la table de niveau 3, mais pense qu'il est déjà dans la table de niveau 1. Enfin, au point d'entrée suivant dans la table de niveau 2, le processeur pense qu'il a accédé à la trame de mémoire physique. Cela nous permet de lire et d'écrire dans une table de niveau 2.
Les tables des niveaux 3 et 4 sont également accessibles. Pour accéder à la table du niveau 3, nous suivons un enregistrement récursif trois fois: le processeur pense qu'il est déjà dans la table du niveau 1, et à l'étape suivante, nous atteignons le niveau 3, que le CPU considère comme un cadre mappé. Pour accéder à la table de niveau 4 elle-même, nous suivons simplement l'enregistrement récursif quatre fois jusqu'à ce que le processeur traite la table de niveau 4 elle-même comme un cadre mappé (en bleu dans la figure ci-dessous).
Le concept est difficile à comprendre au début, mais dans la pratique, il fonctionne plutôt bien.
Calcul d'adresse
Ainsi, nous pouvons accéder aux tables de tous les niveaux en suivant une ou plusieurs fois un enregistrement récursif. Étant donné que les index des tables de quatre niveaux sont dérivés directement de l'adresse virtuelle, des adresses virtuelles spéciales doivent être créées pour cette méthode. Comme nous le rappelons, les index des tables de pages sont extraits de l'adresse comme suit:
Supposons que nous voulons accéder à un tableau de niveau 1 qui affiche une page spécifique. Comme nous l'avons appris ci-dessus, vous devez passer par un enregistrement récursif une fois, puis par les indices des 4e, 3e et 2e niveaux. Pour ce faire, nous déplaçons tous les blocs d'adresse d'un bloc vers la droite et mettons l'index de l'enregistrement récursif à la place de l'index initial de niveau 4:
Pour accéder au tableau de niveau 2 de cette page, nous déplaçons tous les blocs d'index deux blocs vers la droite et définissons l'index récursif à la place des deux blocs source: niveau 4 et niveau 3:
Pour accéder au tableau de niveau 3, on fait de même, on décale juste à droite déjà trois blocs d'adresse.
Enfin, pour accéder à la table de niveau 4, déplacez les quatre blocs vers la droite.
Vous pouvez maintenant calculer des adresses virtuelles pour les tables de pages des quatre niveaux. Nous pouvons même calculer une adresse qui pointe exactement vers une entrée de table de pages spécifique en multipliant son index par 8, la taille de l'entrée de table de pages.
Le tableau ci-dessous montre la structure des adresses pour accéder à différents types de trames:
Adresse virtuelle pour | Structure d'adresse ( octale ) |
---|
La page | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE |
Entrée dans le tableau de niveau 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD |
Entrée dans une table de niveau 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC |
Entrée dans une table de niveau 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB |
Entrée dans le tableau de niveau 4 | 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA |
Ici,
est l'indice de niveau 4,
est de niveau 3,
est de niveau 2 et
DDD
est l'indice de niveau 1 pour la trame affichée,
EEEE
est son décalage.
RRR
est l'indice de l'enregistrement récursif. Un index (trois chiffres) est converti en décalage (quatre chiffres) en multipliant par 8 (la taille de l'entrée du tableau de pages). Avec ce décalage, l'adresse résultante pointe directement vers l'entrée de table de pages correspondante.
SSSS
sont des bits d'extension du chiffre signé, c'est-à-dire qu'ils sont tous des copies du bit 47. Il s'agit d'une exigence spéciale pour les adresses valides dans l'architecture x86_64, dont nous avons discuté dans un
article précédent .
Les adresses sont
octales , car chaque caractère octal représente trois bits, ce qui vous permet de séparer clairement les index de 9 bits des tables à différents niveaux. Ce n'est pas possible dans le système hexadécimal, où chaque caractère représente quatre bits.
Implémentation
Après toute cette théorie, nous pouvons enfin procéder à la mise en œuvre. Idéalement, le chargeur a généré non seulement des tables de pages, mais également un affichage récursif dans le dernier enregistrement de la table de niveau 4. Le chargeur l'a fait car sinon il y aurait un problème de poule ou d'oeuf: nous devons accéder à la table de niveau 4 pour créer une carte récursive mais nous ne pouvons y accéder sans aucun affichage.
Nous avons déjà utilisé ce mappage récursif à la fin de l'article précédent pour accéder à la table de niveau 4 via l'adresse codée en dur
0xffff_ffff_ffff_f000
. Si nous convertissons cette adresse en octal et la comparons avec le tableau ci-dessus, nous verrons qu'elle correspond exactement à la structure de l'enregistrement dans le tableau de niveau 4 avec
RRR
=
0o777
,
AAAA
=
0
et les bits d'extension du signe
1
:
structure: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
adresse: 0o_177777_777_777_777_777_0000
Grâce à la connaissance des tables récursives, nous pouvons désormais créer des adresses virtuelles pour accéder à toutes les tables actives. Et faites la fonction de diffusion.
Traduction d'adresse
Dans un premier temps, créez une fonction qui convertit une adresse virtuelle en une adresse physique, en passant par la hiérarchie des tables de pages:
Tout d'abord, nous introduisons des variables pour l'index récursif (511 =
0o777
) et les bits d'extension de signe (chacun est 1). Ensuite, nous calculons les index des tables de pages et le décalage par le biais d'opérations au niveau du bit, comme indiqué dans l'illustration:
L'étape suivante consiste à calculer les adresses virtuelles des quatre tables de pages, comme décrit dans la section précédente. Ensuite, dans la fonction, nous convertissons chacune de ces adresses en liens
PageTable
. Ce sont des opérations dangereuses car le compilateur ne peut pas savoir que ces adresses sont valides.
Après avoir calculé l'adresse, nous utilisons l'opérateur d'index pour afficher l'enregistrement dans la table de niveau 4. Si cet enregistrement est nul, il n'y a pas de table de niveau 3 pour cet enregistrement de niveau 4. Cela signifie que
addr
mappé à aucune mémoire physique. Nous retournons donc
None
. Sinon, nous savons qu'il existe une table de niveau 3. Ensuite, nous répétons la procédure, comme au niveau précédent.
Après avoir vérifié trois pages d'un niveau supérieur, nous pouvons enfin lire l'enregistrement de la table de niveau 1, qui nous indique le cadre physique avec lequel l'adresse est mappée. Comme dernière étape, ajoutez-y le décalage de page - et renvoyez l'adresse.
Si nous savions avec certitude que l'adresse était mappée, nous pourrions accéder directement à la table de niveau 1 sans regarder les pages d'un niveau supérieur. Mais comme nous ne le savons pas, nous devons d'abord vérifier si une table de niveau 1 existe, sinon notre fonction renverra une erreur de page manquante pour les adresses sans correspondance.
Essayez
Essayons d'utiliser la fonction de traduction des adresses virtuelles dans notre fonction
_start
:
Après le démarrage, nous voyons le résultat suivant:
Comme prévu, l'adresse 0xb8000 associée à l'identifiant se traduit par la même adresse physique. La page de codes et la page de pile sont converties en quelques adresses physiques arbitraires, qui dépendent de la façon dont le chargeur a créé le mappage initial pour notre noyau.
RecursivePageTable
x86_64 fournit un type
RecursivePageTable
qui implémente des abstractions sûres pour diverses opérations de table de pages. En utilisant ce type, vous pouvez implémenter la fonction
translate_addr
manière beaucoup plus succincte:
Le type
RecursivePageTable
encapsule entièrement l'analyse non sécurisée des tables de pages, de sorte que le code
unsafe
de la fonction
translate_addr
n'est plus nécessaire. La fonction
init
reste dangereuse en raison de la nécessité de garantir l'exactitude du
level_4_table_addr
passé.
Notre fonction
_start
doit être mise à jour pour signer à nouveau la fonction comme suit:
Maintenant, au lieu de transmettre
LEVEL_4_TABLE_ADDR
à
translate_addr
et d'accéder aux tables de pages via des pointeurs bruts non sécurisés, nous transmettons des références au type
RecursivePageTable
. Ainsi, nous avons maintenant une abstraction sûre et une sémantique claire de la propriété. Cela garantit que nous ne serons pas en mesure de modifier accidentellement la table des pages en accès partagé, car sa modification nécessite la possession exclusive de
RecursivePageTable
.
Cette fonction donne le même résultat que la fonction de traduction originale écrite manuellement.
Rendre les fonctionnalités dangereuses plus sûres
memory::init
est une fonction dangereuse: elle nécessite un bloc pour l'appeler unsafe
, car l'appelant doit garantir que certaines conditions sont remplies. Dans notre cas, l'exigence est que l'adresse transmise soit précisément mappée à la trame physique de la table de pages de niveau 4.Le unsafe
corps entier de la fonction non sécurisée est placé dans le bloc afin que toutes sortes d'opérations soient effectuées sans créer de blocs supplémentaires unsafe
. Par conséquent, nous n'avons pas besoin d'un bloc dangereux pour le déréférencement level_4_table_ptr
: pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr;
Le problème est que nous ne voyons pas immédiatement quelles pièces sont dangereuses. Par exemple, sans regarder la définition d'une fonction, RecursivePageTable::new
nous ne pouvons pas dire si elle est sûre ou non. Il est donc très facile de sauter accidentellement du code dangereux.Pour éviter ce problème, vous pouvez ajouter une fonction intégrée sécurisée:
Maintenant, le bloc est à unsafe
nouveau requis pour le déréférencement level_4_table_ptr
, et nous voyons immédiatement que ce sont les seules opérations dangereuses. Rust a actuellement un RFC ouvert pour modifier cette propriété infructueuse des fonctions dangereuses.Créer une nouvelle cartographie
Lorsque nous lisons les tables de pages et créons la fonction de conversion, l'étape suivante consiste à créer un nouveau mappage dans la hiérarchie des tables de pages.La complexité de cette opération dépend de la page virtuelle que nous voulons afficher. Dans le cas le plus simple, un tableau de pages de niveau 1 existe déjà pour cette page, et il suffit de faire une entrée. Dans le cas le plus difficile, la page se trouve dans la zone mémoire pour laquelle le niveau 3 n'existe pas encore, vous devez donc d'abord créer de nouvelles tables de niveau 3, niveau 2 et niveau 1.Commençons par un cas simple lorsque vous n'avez pas besoin de créer de nouvelles tables. Le chargeur est chargé dans le premier mégaoctet de l'espace d'adressage virtuel, nous savons donc que pour cette région, il existe une table de niveau 1. Pour notre exemple, nous pouvons sélectionner n'importe quelle page inutilisée dans cette zone de mémoire, par exemple, la page à l'adresse 0x1000
. Nous utilisons le 0xb8000
cadre du tampon de texte VGA comme cadre souhaité . Il est si facile de vérifier le fonctionnement de notre traduction d'adresses.Nous l'implémentons dans une nouvelle fonction create_maping
du module memory
:
La fonction accepte une référence mutable à RecursivePageTable
(elle la changera) et FrameAllocator
, ce qui est expliqué ci-dessous. Ensuite, il applique la fonction map_to
dans le bac Mapper
pour mapper la page à l'adresse 0x1000
avec le cadre physique à l'adresse 0xb8000
. La fonction n'est pas sûre, car il est possible de violer la sécurité de la mémoire avec des arguments non valides.En plus des arguments page
et frame
, la fonction map_to
prend deux autres arguments. Le troisième argument est l'ensemble des indicateurs de la table de pages. Nous définissons le drapeau PRESENT
nécessaire pour toutes les entrées valides et le drapeau WRITABLE
pour l'écriture.Le quatrième argument devrait être une structure qui implémente le trait FrameAllocator
. Cet argument est nécessaire à la méthode.map_to
car la création de nouveaux tableaux de pages peut nécessiter des cadres inutilisés. La mise en œuvre nécessite le trait d'argument Size4KiB
, que les types Page
et PhysFrame
sont universels pour le trait PageSize
, le travail avec 4 pages standards Kio et pages énormes 2 MiB / 1 Gio.La fonction map_to
peut échouer, elle revient donc Result
. Comme il ne s'agit que d'un exemple de code qui ne devrait pas être fiable, nous l'utilisons simplement expect
avec panique lorsqu'une erreur se produit. En cas de succès, la fonction renvoie un type MapperFlush
qui fournit un moyen simple d'effacer la page récemment mise en correspondance de la méthode TLB (associative translation buffer) flush
. CommeResult
, le type utilise l'attribut #[must_use]
et émet un avertissement si nous oublions accidentellement de l'appliquer.Comme nous savons que l'adresse 0x1000
ne nécessite pas de nouvelles tables de pages, elle FrameAllocator
peut toujours revenir None
. Pour tester la fonction, créez ceci EmptyFrameAllocator
:
(Si l'erreur «la méthode allocate_frame
n'est pas membre de trait FrameAllocator
» apparaît , vous devez mettre x86_64
à niveau vers la version 0.4.0.)Nous pouvons maintenant tester la nouvelle fonction de traduction:
Tout d'abord, nous créons un mappage pour la page à l'adresse 0x1000
, en appelant la fonction create_example_mapping
avec un lien mutable vers l'instance RecursivePageTable
. Cela traduit la page 0x1000
en un tampon de texte VGA, donc nous verrons un résultat à l'écran.Ensuite, nous écrivons une valeur dans cette page 0xf021f077f065f04e
, qui correspond à la ligne "Nouveau!" sur fond blanc. Vous n'avez tout simplement pas besoin d'écrire cette valeur immédiatement en haut de la page 0x1000
, car la ligne supérieure se déplacera ensuite à partir de l'écran println
et l'écrira avec un décalage 0x900
situé approximativement au milieu de l'écran. Comme nous le savons dans l'article «Mode texte VGA» , l'écriture dans le tampon VGA doit être volatile, nous utilisons donc la méthode write_volatile
.Lorsque nous l'exécutons dans QEMU, nous voyons ceci:L'inscription à l'écran.Le code a fonctionné car il y avait déjà une table de niveau 1 pour afficher la page 0x1000
. Si nous essayons de traduire une page pour laquelle une telle table n'existe pas encore, la fonction map_to
renverra une erreur, car elle essaiera de sélectionner des cadres pour créer de nouvelles tables de pages EmptyFrameAllocator
. Nous verrons cela si nous essayons de traduire la page 0xdeadbeaf000
au lieu de 0x1000
:
Au démarrage, une panique commence avec le message d'erreur suivant: paniqué à 'map_to a échoué: FrameAllocationFailed', /.../result.rs:999haps
Pour afficher les pages qui n'ont pas encore de table de niveau 1, vous devez créer la bonne FrameAllocator
. Mais comment savoir quelles images sont libres et combien de mémoire physique est disponible?Informations de démarrage
Différents ordinateurs ont différentes quantités de mémoire physique et différentes zones réservées par des périphériques tels que VGA diffèrent. Seul le micrologiciel BIOS ou UEFI sait exactement quelles zones de mémoire peuvent être utilisées et lesquelles sont réservées. Les deux normes de micrologiciel fournissent des fonctions pour obtenir une carte d'allocation de mémoire, mais elles ne peuvent être appelées qu'au tout début du téléchargement. Par conséquent, notre chargeur de démarrage a déjà demandé ces informations (et d'autres) au BIOS.Pour transmettre des informations au noyau de l'OS, le chargeur comme argument lors de l'appel de la fonction _start
donne un lien vers la structure d'information du démarrage. Ajoutez cet argument à notre fonction:
La structure est BootInfo
toujours en cours de finalisation, alors ne soyez pas surpris lorsqu'elle se bloque lors de la mise à niveau vers les futures versions du chargeur de démarrage qui ne seront pas compatibles avec semver . À l'heure actuelle , il a trois champs p4_table_addr
, memory_map
et package
:- Le champ
p4_table_addr
contient une adresse virtuelle récursive de la table des pages de niveau 4. Grâce à cela, il n'est pas nécessaire d'enregistrer l'adresse en dur 0o_177777_777_777_777_777_0000
.
- Le champ
memory_map
est le plus intéressant, car il contient une liste de toutes les zones de mémoire et leur type (inutilisé, réservé ou autres).
- Le champ
package
est la fonction actuelle pour associer des données supplémentaires au chargeur. L'implémentation n'est pas terminée, nous pouvons donc l'ignorer pour l'instant.
Avant d'utiliser le champ memory_map
pour créer le bon FrameAllocator
, nous voulons garantir le bon type d'argument boot_info
.Macro entry_point
Puisqu'elle _start
est appelée en externe, la signature de la fonction n'est pas vérifiée. Cela signifie que des arguments arbitraires n'entraîneront pas d'erreurs de compilation, mais peuvent provoquer un blocage ou un comportement d'exécution non défini.Pour vérifier la signature, la caisse bootloader
pour définir la fonction Rust comme point d'entrée utilise une macro entry_point
avec des types validés. Nous réécrivons notre fonction pour cette macro:
Pour le point d'entrée, vous n'avez plus besoin d'utiliser extern "C"
ou no_mangle
, puisque la macro définit le véritable point d'entrée de bas niveau _start
. La fonction est kernel_main
maintenant devenue une fonction Rust complètement normale, nous pouvons donc lui donner un nom arbitraire. Il est important qu'il soit déjà tapé, afin qu'une erreur de compilation se produise si vous modifiez la signature de la fonction, par exemple, en ajoutant un argument ou en changeant son type.Notez que nous envoyons maintenant à une memory::init
adresse codée en dur, mais boot_info.p4_table_addr
. Ainsi, le code fonctionnera même si la future version du chargeur de démarrage sélectionne une autre entrée dans la table du niveau de page 4 pour un affichage récursif.Sélection du cadre
Maintenant, grâce aux informations du BIOS, nous avons accès à la carte d'allocation de mémoire, afin que vous puissiez faire un distributeur de trame normal. Commençons par le squelette général:
Le champ est frames
initialisé par un itérateur de trame arbitraire . Cela vous permet de simplement déléguer des appels alloc
à la méthode Iterator :: next .L'initialisation BootInfoFrameAllocator
a lieu dans une nouvelle fonction init_frame_allocator
:
Cette fonction, à l'aide d'un combinateur, convertit la carte d'allocation de mémoire d'origine en un itérateur des trames physiques utilisées:iter
MemoryRegion
. filter
, . , , (, ) , InUse
. , , - .
map
range Rust .
- La troisième étape est la plus difficile: nous convertissons chaque plage en itérateur en utilisant la méthode
into_iter
, puis sélectionnons chaque 4096e adresse avec step_by
. Puisque la taille de la page est de 4096 octets (4 Ko), nous obtenons l'adresse du début de chaque trame. La page du chargeur aligne toutes les zones de mémoire utilisées, nous n'avons donc pas besoin d'un code d'alignement ou d'arrondi. Remplacer map
par flat_map
, nous obtenons à la Iterator<Item = u64>
place Iterator<Item = Iterator<Item = u64>>
.
- Au stade final, nous convertirons les adresses de départ en types
PhysFrame
afin de construire celle qui est requise Iterator<Item = PhysFrame>
. Utilisez ensuite cet itérateur pour créer et renvoyer un nouveau BootInfoFrameAllocator
.
Maintenant, nous pouvons changer notre fonction kernel_main
pour qu'elle passe l'instance à la BootInfoFrameAllocator
place EmptyFrameAllocator
:
Maintenant, la traduction d'adresse est réussie - et nous voyons à nouveau le message en noir et blanc "Nouveau!" Sur l'écran .
En arrière-plan, la méthode map_to
crée les tableaux de pages manquants comme suit:- Extrait une trame inutilisée de
frame_allocator
.
- Correspond à une entrée de table de niveau supérieur avec ce cadre. Le cadre est désormais accessible via une table de pages récursive.
- Met le cadre à zéro pour créer un nouveau tableau de pages vide.
- Passe au tableau de niveau suivant.
Bien que notre fonction create_maping
ne soit qu'un exemple, nous pouvons maintenant créer de nouveaux mappages pour des pages arbitraires. Ceci est très utile lors de l'allocation de mémoire et de l'implémentation du multithreading dans les prochains articles.Résumé
Dans cet article, vous avez appris à utiliser une table récursive de niveau 4 pour traduire toutes les trames en adresses virtuelles calculables. Nous avons utilisé cette méthode pour implémenter la fonction de traduction d'adresse et créer un nouveau mappage dans les tables de pages.Nous avons vu que la création de nouveaux mappages nécessite des cadres inutilisés pour les nouvelles tables. Un tel répartiteur de trames peut être implémenté sur la base des informations du BIOS que le chargeur de démarrage transmet à notre noyau.Et ensuite
Dans le prochain article, nous allons créer une zone de mémoire de tas pour notre noyau, ce qui nous permettra d' allouer de la mémoire et d'utiliser différents types de collections .