Nous écrivons un système d'exploitation sur Rust. Implémentation de la mémoire de page (nouveau)

Dans cet article, nous découvrirons comment implémenter la prise en charge de la mémoire de page dans notre noyau. Tout d'abord, nous étudierons différentes méthodes afin que les cadres de la table de pages physiques deviennent disponibles pour le noyau, et discuterons de leurs avantages et inconvénients. Ensuite, nous implémentons la fonction de traduction d'adresse et la fonction de création d'un nouveau mapping.

Cette série d'articles publiée sur GitHub . Si vous avez des questions ou des problèmes, ouvrez-y le ticket correspondant. Toutes les sources de l'article sont dans ce fil .

Un autre article sur la pagination?
Si vous suivez ce cycle, vous avez vu l'article «Mémoire de page: niveau avancé» fin janvier. Mais j'ai été critiqué pour les tableaux de pages récursifs. Par conséquent, j'ai décidé de réécrire l'article, en utilisant une approche différente pour accéder aux cadres.

Voici une nouvelle option. L'article explique toujours comment fonctionnent les tableaux de pages récursifs, mais nous utilisons une implémentation plus simple et plus puissante. Nous ne supprimerons pas l'article précédent, mais le marquerons comme obsolète et ne le mettrons pas à jour.

J'espère que vous apprécierez la nouvelle option!

Table des matières



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 augmente la sécurité car l'accès non autorisé à la mémoire provoque une erreur de page au lieu de modifier de façon aléatoire la mémoire physique.

L'article a fini par ne pas pouvoir accéder aux tables de pages à partir de notre noyau, car elles sont stockées dans la mémoire physique et le noyau s'exécute déjà sur des adresses virtuelles. Ici, nous continuons le sujet et explorons différentes options pour accéder aux cadres du tableau des pages à partir du noyau. Nous discuterons des avantages et des inconvénients de chacun d'eux, puis choisirons l'option appropriée pour notre cœur.

La prise en charge du chargeur de démarrage est requise, nous allons donc la configurer en premier. Ensuite, nous implémentons une fonction qui parcourt toute la hiérarchie des tables de pages afin de traduire les adresses virtuelles en adresses physiques. Enfin, nous apprendrons comment créer de nouveaux mappages dans des tables de pages et comment trouver des cadres de mémoire inutilisés pour créer de nouvelles tables.

Mises à jour des dépendances


Cet article vous oblige à enregistrer le bootloader version 0.4.0 ou supérieure et x86_64 version 0.5.2 ou supérieure dans les dépendances. Vous pouvez mettre à jour les dépendances dans Cargo.toml :

 [dependencies] bootloader = "0.4.0" x86_64 = "0.5.2" 

Pour les modifications de ces versions, consultez le journal du chargeur de démarrage et le journal x86_64 .

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.

Cartographie d'identité


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.

Carte de décalage fixe


Pour éviter d'encombrer l'espace d'adressage virtuel, vous pouvez afficher les tableaux de pages dans une zone de mémoire distincte . Par conséquent, au lieu d'identifier le mappage, nous mappons des trames avec un décalage fixe dans l'espace d'adressage virtuel. Par exemple, le décalage peut être de 10 TiB:



En allouant cette plage de mémoire virtuelle uniquement pour l'affichage des tables de pages, nous évitons les problèmes d'affichage identiques. La réservation d'une si grande zone d'espace d'adressage virtuel n'est possible que si l'espace d'adressage virtuel est beaucoup plus grand que la taille de la mémoire physique. Sur x86_64 ce n'est pas un problème car l'espace d'adressage 48 bits est de 256 TiB.

Mais cette approche présente l'inconvénient que lors de la création de chaque table de pages, vous devez créer un nouveau mappage. De plus, il ne permet pas d'accéder aux tables dans d'autres espaces d'adressage, ce qui serait utile lors de la création d'un nouveau processus.

Mappage complet de la mémoire physique


Nous pouvons résoudre ces problèmes en affichant toute la mémoire physique , et pas seulement les cadres de table de pages:



Cette approche permet au noyau d'accéder à la mémoire physique arbitraire, y compris les cadres de table de pages d'autres espaces d'adressage. Une plage de mémoire virtuelle est réservée de la même taille qu'auparavant, mais seulement il n'y a plus de pages sans correspondance.

L'inconvénient de cette approche est que des tables de pages supplémentaires sont nécessaires pour afficher la mémoire physique. Ces tableaux de pages doivent être stockés quelque part, afin qu'ils utilisent une partie de la mémoire physique, ce qui peut être un problème sur les appareils avec une petite quantité de RAM.

Cependant, sur x86_64, nous pouvons utiliser d' énormes pages de 2 Mio pour afficher au lieu de la taille par défaut de 4 Ko. Ainsi, pour afficher 32 Go de mémoire physique, seulement 132 Ko par table de pages sont nécessaires: une seule table de troisième niveau et 32 ​​tables de deuxième niveau. Les pages volumineuses sont également mises en cache plus efficacement car elles utilisent moins d'entrées dans le tampon de traduction dynamique (TLB).

Affichage temporaire


Pour les appareils avec très peu de mémoire physique, vous ne pouvez afficher les tableaux de pages que 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 est effectué à partir du registre CR3 via des entrées nulles dans les tableaux des niveaux 4, 3 et 2. L'enregistrement avec index 8 traduit la page virtuelle à 32 KiB en un cadre 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 crée deux comparaisons temporelles:

  • Mappage d'une entrĂ©e nulle dans une table de niveau 1 Ă  une trame Ă  24 KiB . Cela crĂ©e un mappage temporaire de la page virtuelle Ă  0 KiB au cadre physique du tableau de niveau de page 2 indiquĂ© par la flèche en pointillĂ©s.
  • Faites correspondre le 9e enregistrement d'une table de niveau 1 avec un cadre Ă  4 KiB . Cela crĂ©e une correspondance temporaire de la page virtuelle Ă  36 KiB avec le cadre physique de la table de niveau de page 4 indiquĂ©e par la flèche en pointillĂ©s.

Maintenant, le noyau peut accéder à une table de niveau 2 en écrivant sur une page qui commence à 0 KiB et à une table de niveau 4 en écrivant sur une page qui commence à 33 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.

Tables de pages récursives


Une autre approche intéressante qui ne nécessite pas du tout de tables de pages supplémentaires est la correspondance récursive .

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 une entrée récursive 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 pourStructure d'adresse ( octale )
La page0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Entrée dans le tableau de niveau 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Entrée dans une table de niveau 20o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Entrée dans une table de niveau 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Entrée dans le tableau de niveau 40o_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.

Code rouille


Vous pouvez construire de telles adresses dans le code Rust à l'aide d'opérations au niveau du bit:

 // the virtual address whose corresponding page tables you want to access let addr: usize = […]; let r = 0o777; // recursive index let sign = 0o177777 << 48; // sign extension // retrieve the page table indices of the address that we want to translate let l4_idx = (addr >> 39) & 0o777; // level 4 index let l3_idx = (addr >> 30) & 0o777; // level 3 index let l2_idx = (addr >> 21) & 0o777; // level 2 index let l1_idx = (addr >> 12) & 0o777; // level 1 index let page_offset = addr & 0o7777; // calculate the table addresses let level_4_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (r << 12); let level_3_table_addr = sign | (r << 39) | (r << 30) | (r << 21) | (l4_idx << 12); let level_2_table_addr = sign | (r << 39) | (r << 30) | (l4_idx << 21) | (l3_idx << 12); let level_1_table_addr = sign | (r << 39) | (l4_idx << 30) | (l3_idx << 21) | (l2_idx << 12); 

Ce code suppose qu'un mappage récursif du dernier enregistrement de niveau 4 avec l'index 0o777 (511) est récursivement mis en correspondance. Ce n'est actuellement pas le cas, donc le code ne fonctionnera pas encore. Voir ci-dessous comment dire au chargeur de configurer un mappage récursif.

Au lieu d'effectuer des opérations au niveau du bit manuellement, vous pouvez utiliser le type RecursivePageTable de la caisse x86_64 , qui fournit des abstractions sûres pour diverses opérations de table. Par exemple, le code ci-dessous montre comment convertir une adresse virtuelle en son adresse physique correspondante:

 // in src/memory.rs use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable}; use x86_64::{VirtAddr, PhysAddr}; /// Creates a RecursivePageTable instance from the level 4 address. let level_4_table_addr = […]; let level_4_table_ptr = level_4_table_addr as *mut PageTable; let recursive_page_table = unsafe { let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap(); } /// Retrieve the physical address for the given virtual address let addr: u64 = […] let addr = VirtAddr::new(addr); let page: Page = Page::containing_address(addr); // perform the translation let frame = recursive_page_table.translate_page(page); frame.map(|frame| frame.start_address() + u64::from(addr.page_offset())) 

Encore une fois, ce code nécessite un mappage récursif correct. Avec ce mappage, le level_4_table_addr manquant level_4_table_addr calculé comme dans le premier exemple de code.



Le mappage récursif est une méthode intéressante qui montre à quel point la correspondance peut être puissante à travers une seule table. Il est relativement facile à implémenter et ne nécessite qu'une configuration minimale (une seule entrée récursive), c'est donc un bon choix pour les premières expériences.

Mais cela présente certains inconvénients:

  • Une grande quantitĂ© de mĂ©moire virtuelle (512 Gio). Ce n'est pas un problème dans un grand espace d'adressage 48 bits, mais peut conduire Ă  un comportement de cache sous-optimal.
  • Il donne facilement accès uniquement Ă  l'espace d'adressage actuellement actif. L'accès Ă  d'autres espaces d'adressage est toujours possible en modifiant l'entrĂ©e rĂ©cursive, mais une correspondance temporaire est requise pour la commutation. Nous avons dĂ©crit comment procĂ©der dans un article prĂ©cĂ©dent (obsolète).
  • Cela dĂ©pend fortement du format de table des pages x86 et peut ne pas fonctionner sur d'autres architectures.

Prise en charge du chargeur de démarrage


Toutes les approches décrites ci-dessus nécessitent des modifications des tableaux de pages et des paramètres correspondants. Par exemple, pour mapper de façon identique ou récursive la mémoire physique des enregistrements d'une table de quatrième niveau. Le problème est que nous ne pouvons pas définir ces paramètres sans accéder aux tableaux de pages.

J'ai donc besoin de l'aide du chargeur de démarrage. Il a accès aux tableaux de pages, il peut donc créer tous les affichages dont nous avons besoin. Dans sa mise en œuvre actuelle, la caisse du bootloader prend en charge les deux approches ci-dessus en utilisant les fonctions de chargement :

  • La fonction map_physical_memory mappe la mĂ©moire physique complète quelque part dans l'espace d'adressage virtuel. Ainsi, le noyau accède Ă  toute la mĂ©moire physique et peut appliquer une approche avec l' affichage de la mĂ©moire physique complète .
  • Ă€ l'aide de la fonction recursive_page_table , le chargeur affiche rĂ©cursivement une entrĂ©e de table de pages de quatrième niveau. Cela permet au noyau de fonctionner selon la mĂ©thode dĂ©crite dans la section "Tables de pages rĂ©cursives" .

, , ( , ). map_physical_memory :

 [dependencies] bootloader = { version = "0.4.0", features = ["map_physical_memory"]} 

, . , .


bootloader BootInfo , . , , semver . : memory_map physical_memory_offset :

  • memory_map . , , VGA. BIOS UEFI, . , . .
  • physical_memory_offset . , . .

BootInfo &'static BootInfo _start . :

 // in src/main.rs use bootloader::BootInfo; #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start(boot_info: &'static BootInfo) -> ! { // new argument […] } 

, .


_start , . , , .

, , bootloader entry_point . :

 // in src/main.rs use bootloader::{BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] } 

extern "C" no_mangle , _start . kernel_main Rust, . , , , , ,


, , . -, , . , , . , .

memory :

 // in src/lib.rs pub mod memory; 

src/memory.rs .


, , , CR3 . : active_level_4_table :

 // in src/memory.rs use x86_64::structures::paging::PageTable; /// Returns a mutable reference to the active level 4 table. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable { use x86_64::{registers::control::Cr3, VirtAddr}; let (level_4_table_frame, _) = Cr3::read(); let phys = level_4_table_frame.start_address(); let virt = VirtAddr::new(phys.as_u64() + physical_memory_offset); let page_table_ptr: *mut PageTable = virt.as_mut_ptr(); &mut *page_table_ptr // unsafe } 

4- CR3 . , physical_memory_offset . , *mut PageTable as_mut_ptr , &mut PageTable . &mut & , .

unsafe, Rust unsafe fn . , . . RFC Rust.

:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::active_level_4_table; let l4_table = unsafe { active_level_4_table(boot_info.physical_memory_offset) }; for (i, entry) in l4_table.iter().enumerate() { if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); } } println!("It did not crash!"); blog_os::hlt_loop(); } 

physical_memory_offset BootInfo . iter enumerate i . , 512 .

, :



, . , , , .

, :

 // in the for loop in src/main.rs use x86_64::{structures::paging::PageTable, VirtAddr}; if !entry.is_unused() { println!("L4 Entry {}: {:?}", i, entry); // get the physical address from the entry and convert it let phys = entry.frame().unwrap().start_address(); let virt = phys.as_u64() + boot_info.physical_memory_offset; let ptr = VirtAddr::new(virt).as_mut_ptr(); let l3_table: &PageTable = unsafe { &*ptr }; // print non-empty entries of the level 3 table for (i, entry) in l3_table.iter().enumerate() { if !entry.is_unused() { println!(" L3 Entry {}: {:?}", i, entry); } } } 

, , . , , .

, , . , .


, . , :

 // in src/memory.rs use x86_64::{PhysAddr, VirtAddr}; /// Translates the given virtual address to the mapped physical address, or /// `None` if the address is not mapped. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { translate_addr_inner(addr, physical_memory_offset) } 

translate_addr_inner , . , Rust unsafe fn . , unsafe .

:

 // in src/memory.rs /// Private function that is called by `translate_addr`. /// /// This function is safe to limit the scope of `unsafe` because Rust treats /// the whole body of unsafe functions as an unsafe block. This function must /// only be reachable through `unsafe fn` from outside of this module. fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: u64) -> Option<PhysAddr> { use x86_64::structures::paging::page_table::FrameError; use x86_64::registers::control::Cr3; // read the active level 4 frame from the CR3 register let (level_4_table_frame, _) = Cr3::read(); let table_indexes = [ addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index() ]; let mut frame = level_4_table_frame; // traverse the multi-level page table for &index in &table_indexes { // convert the frame into a page table reference let virt = frame.start_address().as_u64() + physical_memory_offset; let table_ptr: *const PageTable = VirtAddr::new(virt).as_ptr(); let table = unsafe {&*table_ptr}; // read the page table entry and update `frame` let entry = &table[index]; frame = match entry.frame() { Ok(frame) => frame, Err(FrameError::FrameNotPresent) => return None, Err(FrameError::HugeFrame) => panic!("huge pages not supported"), }; } // calculate the physical address by adding the page offset Some(frame.start_address() + u64::from(addr.page_offset())) } 

active_level_4_table CR3 , . , .

VirtAddr . , for . , . frame , . . 1.

physical_memory_offset . PageTableEntry::frame . , None . 2 1 , .

, :

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; use x86_64::VirtAddr; let addresses = [ // the identity-mapped vga buffer page 0xb8000, // some code page 0x20010a, // some stack page 0x57ac_001f_fe48, // virtual address mapped to physical address 0 boot_info.physical_memory_offset, ]; for &address in &addresses { let virt = VirtAddr::new(address); let phys = unsafe { translate_addr(virt, boot_info.physical_memory_offset) }; println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

, :



, 0xb8000 . , , . physical_memory_offset 0 , , . .

MappedPageTable


— , x86_64 . , translate_addr , .

— , :


, . x86_64 , : MappedPageTable RecursivePageTable . , - (, ). , .

physical_memory_offset , MappedPageTable. , init memory :

 use x86_64::structures::paging::{PhysFrame, MapperAllSizes, MappedPageTable}; use x86_64::PhysAddr; /// Initialize a new MappedPageTable. /// /// This function is unsafe because the caller must guarantee that the /// complete physical memory is mapped to virtual memory at the passed /// `physical_memory_offset`. Also, this function must be only called once /// to avoid aliasing `&mut` references (which is undefined behavior). pub unsafe fn init(physical_memory_offset: u64) -> impl MapperAllSizes { let level_4_table = active_level_4_table(physical_memory_offset); let phys_to_virt = move |frame: PhysFrame| -> *mut PageTable { let phys = frame.start_address().as_u64(); let virt = VirtAddr::new(phys + physical_memory_offset); virt.as_mut_ptr() }; MappedPageTable::new(level_4_table, phys_to_virt) } // make private unsafe fn active_level_4_table(physical_memory_offset: u64) -> &'static mut PageTable {…} 

MappedPageTable , . impl Trait . , RecursivePageTable .

MappedPageTable::new : 4 phys_to_virt , *mut PageTable . active_level_4_table . , physical_memory_offset .

active_level_4_table , init .

MapperAllSizes::translate_addr memory::translate_addr , kernel_main :

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS // new: different imports use blog_os::memory; use x86_64::{structures::paging::MapperAllSizes, VirtAddr}; // new: initialize a mapper let mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let addresses = […]; // same as before for &address in &addresses { let virt = VirtAddr::new(address); // new: use the `mapper.translate_addr` method let phys = mapper.translate_addr(virt); println!("{:?} -> {:?}", virt, phys); } println!("It did not crash!"); blog_os::hlt_loop(); } 

, , :



, physical_memory_offset 0x0 . MappedPageTable , . , map_to , . memory::translate_addr , , .


, . .

map_to Mapper , . , : , ; , ; frame_allocator . , , .

create_example_mapping


— create_example_mapping , 0xb8000 , VGA. , , : , .

create_example_mapping :

 // in src/memory.rs use x86_64::structures::paging::{Page, Size4KiB, Mapper, FrameAllocator}; /// Creates an example mapping for the given page to frame `0xb8000`. pub fn create_example_mapping( page: Page, mapper: &mut impl Mapper<Size4KiB>, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { mapper.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

page , , mapper frame_allocator . mapper Mapper<Size4KiB> , map_to . Size4KiB , Mapper PageSize , 4 , 2 1 . 4 , Mapper<Size4KiB> MapperAllSizes .

PRESENT , , WRITABLE , . map_to : , unsafe . . « » .

map_to , Result . , , expect . MapperFlush , (TLB) flush . Result , [ #[must_use] ] , .

FrameAllocator


create_example_mapping , FrameAllocator . , , . 1 , . , 3 , 3, 2 1.

, . , None . EmptyFrameAllocator :

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

, . , , 1. , , 0x1000 .

, 0x1000 , :

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory; use x86_64::{structures::paging::Page, VirtAddr}; let mut mapper = unsafe { memory::init(boot_info.physical_memory_offset) }; let mut frame_allocator = memory::EmptyFrameAllocator; // map a previously unmapped page let page = Page::containing_address(VirtAddr::new(0x1000)); memory::create_example_mapping(page, &mut mapper, &mut frame_allocator); // write the string `New!` to the screen through the new mapping let page_ptr: *mut u64 = page.start_address().as_mut_ptr(); unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

0x1000 , create_example_mapping mapper frame_allocator . 0x1000 VGA, , .

400 . , VGA println . 0x_f021_f077_f065_f04e , “New!” . « VGA» , VGA , write_volatile .

QEMU, :



0x1000 “New!” . , .

, 1 0x1000 . , 1, map_to , EmptyFrameAllocator . , , 0xdeadbeaf000 0x1000 :

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } 

, :

 panicked at 'map_to failed: FrameAllocationFailed', /…/result.rs:999:5 

, 1, FrameAllocator . , ?


. :

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

frames . alloc Iterator::next .

BootInfoFrameAllocator memory_map , BootInfo . « » , BIOS/UEFI. , .

MemoryRegion , , (, , . .) . , , BootInfoFrameAllocator .

BootInfoFrameAllocator init_frame_allocator :

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

MemoryMap :

  • -, iter MemoryRegion . filter . , , , (, ) , InUse . , , Usable - .
  • map range Rust .
  • : into_iter , 4096- step_by . 4096 (= 4 ) — , . , . flat_map map , Iterator<Item = u64> Iterator<Item = Iterator<Item = u64>> .
  • PhysFrame , Iterator<Item = PhysFrame> . BootInfoFrameAllocator .

kernel_main , BootInfoFrameAllocator EmptyFrameAllocator :

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); […] } 

- “New!” . map_to :

  • frame_allocator .
  • .
  • .
  • .

create_example_mapping — , . .

Résumé


, , , . .

, . bootloader cargo. &BootInfo .

, , MappedPageTable x86_64 . , FrameAllocator , .

?


, .

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


All Articles