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