Escribimos un sistema operativo en Rust. Implementando memoria de página (nuevo)

En este artículo, descubriremos cómo implementar el soporte de memoria de página en nuestro núcleo. Primero, estudiaremos varios métodos para que los marcos de la tabla de páginas físicas estén disponibles para el núcleo, y discutiremos sus ventajas y desventajas. Luego implementamos la función de traducción de direcciones y la función de crear una nueva asignación.

Esta serie de artículos publicados en GitHub . Si tiene alguna pregunta o problema, abra el ticket correspondiente allí. Todas las fuentes para el artículo están en este hilo .

¿Otro artículo sobre paginación?
Si sigue este ciclo, vio el artículo "Memoria de página: Nivel avanzado" a fines de enero. Pero me criticaron por las tablas de páginas recursivas. Por lo tanto, decidí volver a escribir el artículo, usando un enfoque diferente para acceder a los marcos.

Aquí hay una nueva opción. El artículo todavía explica cómo funcionan las tablas de páginas recursivas, pero utilizamos una implementación más simple y poderosa. No eliminaremos el artículo anterior, pero lo marcaremos como obsoleto y no lo actualizaremos.

¡Espero que disfrutes la nueva opción!

Contenido



Introduccion


Desde el último artículo, aprendimos sobre los principios de la memoria de paginación y cómo funcionan las tablas de páginas de cuatro niveles en x86_64 . También encontramos que el cargador ya configuró la jerarquía de la tabla de páginas para nuestro núcleo, por lo que el núcleo se ejecuta en direcciones virtuales. Esto aumenta la seguridad porque el acceso no autorizado a la memoria provoca un error de página en lugar de cambiar aleatoriamente la memoria física.

El artículo terminó sin poder acceder a las tablas de páginas de nuestro núcleo, porque están almacenadas en la memoria física y el núcleo ya se está ejecutando en direcciones virtuales. Aquí continuamos con el tema y exploramos diferentes opciones para acceder a los marcos de la tabla de páginas desde el núcleo. Discutiremos las ventajas y desventajas de cada uno de ellos, y luego elegiremos la opción adecuada para nuestro núcleo.

Se requiere soporte para el cargador de arranque, por lo que lo configuraremos primero. Luego implementamos una función que se ejecuta en toda la jerarquía de las tablas de páginas para traducir las direcciones virtuales en físicas. Finalmente, aprenderemos cómo crear nuevas asignaciones en tablas de páginas y cómo encontrar marcos de memoria no utilizados para crear nuevas tablas.

Actualizaciones de dependencia


Este artículo requiere que registre la versión 0.4.0 o superior del bootloader y la versión 0.5.2 o superior x86_64 en las dependencias. Puede actualizar las dependencias en Cargo.toml :

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

Para ver los cambios en estas versiones, consulte el registro del cargador de arranque y el registro x86_64 .

Acceso a tablas de páginas.


Acceder a las tablas de páginas desde el núcleo no es tan fácil como parece. Para comprender el problema, eche otro vistazo a la jerarquía de tablas de cuatro niveles del artículo anterior:



Lo importante es que cada entrada de página almacena la dirección física de la siguiente tabla. Esto evita la traducción de estas direcciones, lo que reduce el rendimiento y conduce fácilmente a bucles sin fin.

El problema es que no podemos acceder directamente a las direcciones físicas desde el núcleo, ya que también funciona en direcciones virtuales. Por ejemplo, cuando vamos a la dirección 4 KiB , tenemos acceso a la dirección virtual 4 KiB , y no a la dirección física donde se almacena la tabla de páginas del 4to nivel. Si queremos acceder a la dirección física de 4 KiB , entonces necesitamos usar alguna dirección virtual, que se traduce en ella.

Por lo tanto, para acceder a los marcos de las tablas de páginas, debe asignar algunas páginas virtuales a estos marcos. Hay diferentes formas de crear tales asignaciones.

Mapeo de identidad


Una solución simple es la visualización idéntica de todas las tablas de páginas .



En este ejemplo, vemos la visualización idéntica de cuadros. Las direcciones físicas de las tablas de páginas son al mismo tiempo direcciones virtuales válidas, por lo que podemos acceder fácilmente a las tablas de páginas de todos los niveles, comenzando con el registro CR3.

Sin embargo, este enfoque satura el espacio de direcciones virtuales y hace que sea difícil encontrar grandes áreas contiguas de memoria libre. Digamos que queremos crear un área de memoria virtual de 1000 KiB en la figura anterior, por ejemplo, para mostrar un archivo en la memoria . No podemos comenzar con la región 28 KiB , porque descansa en una página ya ocupada en 1004 KiB . Por lo tanto, tendrá que buscar más hasta encontrar un fragmento grande adecuado, por ejemplo, con 1008 KiB . Existe el mismo problema de fragmentación que en la memoria segmentada.

Además, la creación de nuevas tablas de páginas es mucho más complicada, ya que necesitamos encontrar marcos físicos cuyas páginas correspondientes aún no se utilizan. Por ejemplo, para nuestro archivo, reservamos un área de 1000 KiB de memoria virtual , comenzando en la dirección 1008 KiB . Ahora ya no podemos usar ningún marco con una dirección física entre 1000 KiB y 2008 KiB , porque no se puede mostrar de forma idéntica.

Mapa de desplazamiento fijo


Para evitar abarrotar el espacio de direcciones virtuales, puede mostrar las tablas de páginas en un área de memoria separada . Por lo tanto, en lugar de identificar el mapeo, mapeamos cuadros con un desplazamiento fijo en el espacio de direcciones virtuales. Por ejemplo, el desplazamiento puede ser 10 TiB:



Al asignar este rango de memoria virtual únicamente para mostrar tablas de páginas, evitamos los problemas de visualización idéntica. Reservar un área tan grande de espacio de direcciones virtuales solo es posible si el espacio de direcciones virtuales es mucho mayor que el tamaño de la memoria física. En x86_64 esto no es un problema porque el espacio de direcciones de 48 bits es de 256 TiB.

Pero este enfoque tiene la desventaja de que al crear cada tabla de páginas, debe crear una nueva asignación. Además, no permite el acceso a tablas en otros espacios de direcciones, lo que sería útil al crear un nuevo proceso.

Mapeo completo de memoria física


Podemos resolver estos problemas mostrando toda la memoria física , y no solo los cuadros de la tabla de páginas:



Este enfoque permite que el núcleo acceda a la memoria física arbitraria, incluidos los cuadros de la tabla de páginas de otros espacios de direcciones. Se reserva un rango de memoria virtual del mismo tamaño que antes, pero solo no quedan páginas inigualables.

La desventaja de este enfoque es que se necesitan tablas de páginas adicionales para mostrar la memoria física. Estas tablas de páginas deben almacenarse en algún lugar, por lo que utilizan parte de la memoria física, lo que puede ser un problema en dispositivos con una pequeña cantidad de RAM.

Sin embargo, en x86_64 podemos usar enormes páginas de 2 MiB para mostrar en lugar del tamaño predeterminado de 4 KiB. Por lo tanto, para mostrar 32 GiB de memoria física, solo se requieren 132 KiB por tabla de páginas: solo una tabla de tercer nivel y 32 tablas de segundo nivel. Las páginas enormes también se almacenan en caché de manera más eficiente porque usan menos entradas en el búfer de traducción dinámica (TLB).

Exhibición temporal


Para dispositivos con muy poca memoria física, solo puede mostrar tablas de páginas temporalmente cuando necesita acceder a ellas. Para comparaciones temporales, se requiere una visualización idéntica de solo la tabla de primer nivel:



En esta figura, una tabla de nivel 1 gestiona los primeros 2 MiB de espacio de direcciones virtuales. Esto es posible porque el acceso desde el registro CR3 es a través de cero entradas en las tablas de los niveles 4, 3 y 2. El registro con el índice 8 traduce la página virtual a 32 KiB en un marco físico a 32 KiB , identificando así la tabla del nivel 1. En la figura, esto se muestra con una flecha horizontal.

Al escribir en la tabla de nivel 1 asignada de forma idéntica, nuestro núcleo puede crear hasta 511 comparaciones de tiempo (512 menos el registro necesario para el mapeo de identidad). En el ejemplo anterior, el núcleo crea dos comparaciones de tiempo:

  • Asignación de una entrada nula en una tabla de nivel 1 a un marco a 24 KiB . Esto crea un mapeo temporal de la página virtual en 0 KiB al marco físico de la tabla de nivel de página 2 indicado por la flecha punteada.
  • Coincide con el noveno registro de una tabla de nivel 1 con un marco a 4 KiB . Esto crea un mapeo temporal de la página virtual a 36 KiB al marco físico de la tabla de nivel 4 de página indicado por la flecha punteada.

Ahora el núcleo puede acceder a una tabla de nivel 2 escribiendo en una página que comienza en 0 KiB y a una tabla de nivel 4 escribiendo en una página que comienza en 33 KiB .

Por lo tanto, el acceso a un marco arbitrario de la tabla de páginas con asignaciones temporales consta de las siguientes acciones:

  • Encuentre una entrada gratuita en la tabla de nivel 1 que se muestra idénticamente.
  • Asigne esta entrada al marco físico de la tabla de páginas a la que queremos acceder.
  • Acceda a este marco a través de la página virtual asociada con la entrada.
  • Vuelva a establecer el registro como no utilizado, eliminando así la asignación temporal.

Con este enfoque, el espacio de direcciones virtuales permanece limpio, ya que las mismas 512 páginas virtuales se usan constantemente. La desventaja es algo engorroso, especialmente porque una nueva comparación puede requerir cambiar varios niveles de la tabla, es decir, necesitamos repetir el proceso descrito varias veces.

Tablas de página recursivas


Otro enfoque interesante que no requiere tablas de páginas adicionales es la coincidencia recursiva .

La idea es traducir algunos registros de la tabla de cuarto nivel en ella misma. Por lo tanto, en realidad reservamos una parte del espacio de direcciones virtuales y asignamos todos los marcos de tabla actuales y futuros a este espacio.

Veamos un ejemplo para entender cómo funciona todo esto:



La única diferencia con el ejemplo al principio del artículo es un registro adicional con el índice 511 en la tabla de nivel 4, que se asigna al marco físico 4 KiB , que se encuentra en esta tabla.

Cuando la CPU pasa a este registro, no se refiere a la tabla de nivel 3, sino que nuevamente se refiere a la tabla de nivel 4. Esto es similar a una función recursiva que se llama a sí misma. Es importante que el procesador asuma que cada entrada en la tabla de nivel 4 apunta a una tabla de nivel 3, por lo que ahora trata la tabla de nivel 4 como una tabla de nivel 3. Esto funciona porque las tablas de todos los niveles en x86_64 tienen la misma estructura.

Al seguir un registro recursivo una o más veces antes de comenzar la conversión real, podemos reducir efectivamente el número de niveles por los que pasa el procesador. Por ejemplo, si seguimos el registro recursivo una vez, y luego vamos a la tabla de nivel 3, el procesador piensa que la tabla de nivel 3 es una tabla de nivel 2. Continuando, considera la tabla de nivel 2 como una tabla de nivel 1, y la tabla de nivel 1 como asignada marco en la memoria física. Esto significa que ahora podemos leer y escribir en la tabla de nivel de página 1 porque el procesador piensa que este es un marco mapeado. La siguiente figura muestra los cinco pasos de dicha traducción:



Del mismo modo, podemos seguir una entrada recursiva dos veces antes de comenzar la conversión para reducir el número de niveles pasados ​​a dos:



Veamos este procedimiento paso a paso. Primero, la CPU sigue una entrada recursiva en la tabla de nivel 4 y piensa que ha alcanzado la tabla de nivel 3. Luego sigue el registro recursivo nuevamente y piensa que ha alcanzado el nivel 2. Pero en realidad todavía está en el nivel 4. Luego la CPU va a la nueva dirección y entra en la tabla de nivel 3, pero cree que ya está en la tabla de nivel 1. Finalmente, en el siguiente punto de entrada en la tabla de nivel 2, el procesador cree que ha accedido al marco de memoria física. Esto nos permite leer y escribir en una tabla de nivel 2.

También se accede a las tablas de los niveles 3 y 4. Para acceder a la tabla del nivel 3, seguimos un registro recursivo tres veces: el procesador cree que ya está en la tabla del nivel 1, y en el siguiente paso llegamos al nivel 3, que la CPU considera como un marco mapeado. Para acceder a la tabla de nivel 4, simplemente seguimos el registro recursivo cuatro veces hasta que el procesador procese la tabla de nivel 4 como un marco mapeado (en azul en la figura a continuación).



El concepto es difícil de entender al principio, pero en la práctica funciona bastante bien.

Cálculo de dirección


Por lo tanto, podemos acceder a las tablas de todos los niveles siguiendo un registro recursivo una o más veces. Dado que los índices en tablas de cuatro niveles se derivan directamente de la dirección virtual, se deben crear direcciones virtuales especiales para este método. Como recordamos, los índices de la tabla de páginas se extraen de la dirección de la siguiente manera:



Supongamos que queremos acceder a una tabla de nivel 1 que muestra una página específica. Como aprendimos anteriormente, debe pasar por un registro recursivo una vez, y luego a través de los índices de los niveles 4º, 3º y 2º. Para hacer esto, movemos todos los bloques de direcciones un bloque a la derecha y establecemos el índice del registro recursivo en el lugar del índice inicial del nivel 4:



Para acceder a la tabla de nivel 2 de esta página, movemos todos los bloques de índice dos bloques a la derecha y establecemos el índice recursivo en el lugar de ambos bloques de origen: nivel 4 y nivel 3:



Para acceder a la tabla de nivel 3, hacemos lo mismo, solo nos desplazamos a la derecha con tres bloques de direcciones.



Finalmente, para acceder a la tabla de nivel 4, mueva todo cuatro bloques a la derecha.



Ahora puede calcular direcciones virtuales para tablas de páginas de los cuatro niveles. Incluso podemos calcular una dirección que apunta exactamente a una entrada específica de la tabla de páginas multiplicando su índice por 8, el tamaño de la entrada de la tabla de páginas.

La siguiente tabla muestra la estructura de direcciones para acceder a varios tipos de marcos:

Dirección virtual paraEstructura de direcciones ( octal )
Pagina0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Entrada en la mesa de nivel 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Entrada en una mesa de nivel 20o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Entrada en una mesa de nivel 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Entrada en la mesa del nivel 40o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Aquí es el índice de nivel 4, es el nivel 3, es el nivel 2 y DDD es el índice de nivel 1 para el cuadro visualizado, EEEE es su desplazamiento. RRR es el índice del registro recursivo. Un índice (tres dígitos) se convierte en un desplazamiento (cuatro dígitos) al multiplicar por 8 (el tamaño de la entrada de la tabla de páginas). Con este desplazamiento, la dirección resultante apunta directamente a la entrada de la tabla de páginas correspondiente.

SSSS son bits de expansión del dígito firmado, es decir, son todas copias del bit 47. Este es un requisito especial para las direcciones válidas en la arquitectura x86_64, que discutimos en el artículo anterior .

Las direcciones son octales , ya que cada carácter octal representa tres bits, lo que le permite separar claramente los índices de tablas de 9 bits en diferentes niveles. Esto no es posible en el sistema hexadecimal, donde cada carácter representa cuatro bits.

Código de óxido


Puede construir tales direcciones en código Rust utilizando operaciones bit a 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); 

Este código supone que una asignación recursiva del último registro de nivel 4 con el índice 0o777 (511) coincide recursivamente. Actualmente, este no es el caso, por lo que el código aún no funcionará. Vea a continuación cómo decirle al cargador que configure una asignación recursiva.

Como alternativa a la realización manual de operaciones bit a bit, puede usar el tipo RecursivePageTable de la caja x86_64 , que proporciona abstracciones seguras para diversas operaciones de tabla. Por ejemplo, el siguiente código muestra cómo convertir una dirección virtual a su dirección física correspondiente:

 // 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())) 

Nuevamente, este código requiere un mapeo recursivo correcto. Con esta asignación, el level_4_table_addr faltante level_4_table_addr calcula como en el primer ejemplo de código.



El mapeo recursivo es un método interesante que muestra cuán poderosa puede ser la correspondencia a través de una sola tabla. Es relativamente fácil de implementar y requiere una configuración mínima (solo una entrada recursiva), por lo que esta es una buena opción para los primeros experimentos.

Pero tiene algunas desventajas:

  • Una gran cantidad de memoria virtual (512 GiB). Esto no es un problema en un gran espacio de direcciones de 48 bits, pero puede conducir a un comportamiento de caché subóptimo.
  • Facilita el acceso solo al espacio de direcciones actualmente activo. El acceso a otros espacios de direcciones todavía es posible cambiando la entrada recursiva, pero se requiere una coincidencia temporal para el cambio. Describimos cómo hacer esto en un artículo anterior (obsoleto).
  • Depende en gran medida del formato de tabla de páginas x86 y puede no funcionar en otras arquitecturas.

Soporte de arranque


Todos los enfoques descritos anteriormente requieren cambios en las tablas de página y la configuración correspondiente. Por ejemplo, para mapear la memoria física de forma idéntica o recursiva mapear registros de una tabla de cuarto nivel. El problema es que no podemos realizar esta configuración sin acceso a las tablas de páginas.

Entonces, necesito ayuda del gestor de arranque. Tiene acceso a las tablas de páginas, por lo que puede crear cualquier pantalla que necesitemos. En su implementación actual, la caja del bootloader admite los dos enfoques anteriores utilizando funciones de carga :

  • La función map_physical_memory mapea la memoria física completa en algún lugar del espacio de direcciones virtuales. Por lo tanto, el kernel obtiene acceso a toda la memoria física y puede aplicar un enfoque con la visualización de la memoria física completa .
  • Usando la función recursive_page_table , el cargador muestra recursivamente una entrada de tabla de página de cuarto nivel. Esto permite que el núcleo funcione de acuerdo con el método descrito en la sección "Tablas de páginas recursivas" .

, , ( , ). 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.

Ahora podemos usar esta función para generar los registros de la tabla de cuarto nivel:

 // 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 — , . .

Resumen


, , , . .

, . bootloader cargo. &BootInfo .

, , MappedPageTable x86_64 . , FrameAllocator , .

?


, .

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


All Articles