Sistema operativo en óxido. Memoria de página: avanzada

Este artículo explica cómo el núcleo del sistema operativo puede acceder a los marcos de memoria física. Estudiaremos la función para convertir direcciones virtuales en físicas. También descubriremos cómo crear nuevas asignaciones en las tablas de páginas.

Este blog está publicado en GitHub . Si tiene alguna pregunta o problema, abra el ticket correspondiente allí. Todas las fuentes para el artículo están aquí .

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 mejora la seguridad, pero surge el problema: ¿cómo acceder a las direcciones físicas reales que se almacenan en las entradas de la tabla de páginas o en el CR3 ?

En la primera sección del artículo, discutiremos el problema y los diferentes enfoques para resolverlo. Luego implementamos una función que se cuela a través de la jerarquía de las tablas de páginas para convertir las direcciones virtuales en físicas. Finalmente, aprenda a crear nuevas asignaciones en tablas de páginas y encuentre marcos de memoria no utilizados para crear nuevas tablas.

Actualizaciones de dependencia


Para trabajar, necesita x86_64 versión 0.4.0 o posterior. Actualice la dependencia en nuestro Cargo.toml :

 [dependencies] x86_64 = "0.4.0" # or later 

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.

1. 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.

2. Otra opción es transmitir tablas de páginas solo temporalmente cuando necesite 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 se realiza desde el registro CR3 a través de entradas nulas 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 de 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 coincide con el registro nulo de una tabla de nivel 1 con un marco a 24 KiB . Esto creó un mapeo temporal de la página virtual en 0 KiB al marco físico de la tabla de nivel de página 2 indicada por la flecha punteada. Ahora el núcleo puede acceder a la tabla de nivel 2 escribiendo en una página que comienza en 0 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.

3. Aunque los dos enfoques anteriores funcionan, hay un tercer método: tablas de páginas recursivas . Combina las ventajas de ambos enfoques: compara constantemente todos los marcos de las tablas de páginas sin requerir comparaciones temporales, y también mantiene las páginas asignadas una al lado de la otra, evitando la fragmentación del espacio de direcciones virtuales. Este es el método que usaremos.

Tablas de página recursivas


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 direcciones válidas en la arquitectura x86_64, que discutimos en un 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.

Implementación


Después de toda esta teoría, finalmente podemos proceder con la implementación. Convenientemente, el cargador generó no solo tablas de páginas, sino también una visualización recursiva en el último registro de la tabla de nivel 4. El cargador hizo esto porque de lo contrario habría un problema de huevo o gallina: necesitamos acceder a la tabla de nivel 4 para crear un mapa recursivo pero no podemos acceder a él sin ninguna pantalla.

Ya utilizamos esta asignación recursiva al final del artículo anterior para acceder a la tabla de nivel 4 a través de la dirección codificada 0xffff_ffff_ffff_f000 . Si convertimos esta dirección a octal y la comparamos con la tabla anterior, veremos que corresponde exactamente a la estructura del registro en la tabla de nivel 4 con RRR = 0o777 , AAAA = 0 y los bits de extensión del signo 1 :

  estructura: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
 dirección: 0o_177777_777_777_777_777_0000 

Gracias al conocimiento de las tablas recursivas, ahora podemos crear direcciones virtuales para acceder a todas las tablas activas. Y hacer que la transmisión funcione.

Traducción de direcciones


Como primer paso, cree una función que convierta una dirección virtual en una dirección física, pasando por la jerarquía de las tablas de páginas:

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

 // in src/memory.rs use x86_64::PhysAddr; use x86_64::structures::paging::PageTable; /// Returns the physical address for the given virtual address, or `None` if the /// virtual address is not mapped. pub fn translate_addr(addr: usize) -> Option<PhysAddr> { // introduce variables for the recursive index and the sign extension bits // TODO: Don't hardcode these values 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); // check that level 4 entry is mapped let level_4_table = unsafe { &*(level_4_table_addr as *const PageTable) }; if level_4_table[l4_idx].addr().is_null() { return None; } // check that level 3 entry is mapped let level_3_table = unsafe { &*(level_3_table_addr as *const PageTable) }; if level_3_table[l3_idx].addr().is_null() { return None; } // check that level 2 entry is mapped let level_2_table = unsafe { &*(level_2_table_addr as *const PageTable) }; if level_2_table[l2_idx].addr().is_null() { return None; } // check that level 1 entry is mapped and retrieve physical address from it let level_1_table = unsafe { &*(level_1_table_addr as *const PageTable) }; let phys_addr = level_1_table[l1_idx].addr(); if phys_addr.is_null() { return None; } Some(phys_addr + page_offset) } 

Primero, introducimos variables para el índice recursivo (511 = 0o777 ) y los bits de extensión de signo (cada uno es 1). Luego calculamos los índices de las tablas de páginas y el desplazamiento mediante operaciones bit a bit, como se indica en la ilustración:



El siguiente paso es calcular las direcciones virtuales de las tablas de cuatro páginas, como se describe en la sección anterior. Luego, en la función, convertimos cada una de estas direcciones a enlaces de PageTable . Estas son operaciones inseguras porque el compilador no puede saber que estas direcciones son válidas.

Después de calcular la dirección, utilizamos el operador de indexación para ver el registro en la tabla de nivel 4. Si este registro es cero, entonces no hay una tabla de nivel 3 para este registro de nivel 4. Esto significa que addr no addr asignado a ninguna memoria física. Entonces devolvemos None . De lo contrario, sabemos que existe una tabla de nivel 3. Luego repetimos el procedimiento, como en el nivel anterior.

Después de verificar tres páginas de un nivel superior, finalmente podemos leer el registro de la tabla de nivel 1, que nos dice el marco físico con el que se asigna la dirección. Como último paso, agregue el desplazamiento de página y devuelva la dirección.

Si supiéramos con certeza que la dirección estaba asignada, podríamos acceder directamente a la tabla de nivel 1 sin mirar las páginas de un nivel superior. Pero como no sabemos esto, primero debemos verificar si existe una tabla de nivel 1, de lo contrario, nuestra función devolverá un error de página faltante para direcciones no coincidentes.

Prueba


Intentemos usar la función de traducción para direcciones virtuales en nuestra función _start :

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::translate_addr; // the identity-mapped vga buffer page println!("0xb8000 -> {:?}", translate_addr(0xb8000)); // some code page println!("0x20010a -> {:?}", translate_addr(0x20010a)); // some stack page println!("0x57ac001ffe48 -> {:?}", translate_addr(0x57ac001ffe48)); println!("It did not crash!"); blog_os::hlt_loop(); } 


Después de comenzar, vemos el siguiente resultado:



Como se esperaba, la dirección 0xb8000 asociada con el identificador se traduce en la misma dirección física. La página de códigos y la página de la pila se convierten en algunas direcciones físicas arbitrarias, que dependen de cómo el cargador creó la asignación inicial para nuestro núcleo.

RecursivePageTable


x86_64 proporciona un tipo RecursivePageTable que implementa abstracciones seguras para varias operaciones de tabla de páginas. Con este tipo, puede implementar la función translate_addr mucho más sucintamente:

 // 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. /// /// This function is unsafe because it can break memory safety if an invalid /// address is passed. pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr; RecursivePageTable::new(level_4_table).unwrap() } /// Returns the physical address for the given virtual address, or `None` if /// the virtual address is not mapped. pub fn translate_addr(addr: u64, recursive_page_table: &RecursivePageTable) -> Option<PhysAddr> { 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())) } 

El tipo RecursivePageTable encapsula completamente los rastreos de tablas de páginas inseguras, por lo que ya no se necesita el código unsafe en la función translate_addr . La función init permanece insegura debido a la necesidad de garantizar la corrección del nivel level_4_table_addr .

Nuestra función _start debe actualizarse para volver a firmar la función de la siguiente manera:

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::{self, translate_addr}; const LEVEL_4_TABLE_ADDR: usize = 0o_177777_777_777_777_777_0000; let recursive_page_table = unsafe { memory::init(LEVEL_4_TABLE_ADDR) }; // the identity-mapped vga buffer page println!("0xb8000 -> {:?}", translate_addr(0xb8000, &recursive_page_table)); // some code page println!("0x20010a -> {:?}", translate_addr(0x20010a, &recursive_page_table)); // some stack page println!("0x57ac001ffe48 -> {:?}", translate_addr(0x57ac001ffe48, &recursive_page_table)); println!("It did not crash!"); blog_os::hlt_loop(); } 

Ahora, en lugar de pasar LEVEL_4_TABLE_ADDR para translate_addr y acceder a las tablas de página a través de punteros sin LEVEL_4_TABLE_ADDR seguros, pasamos referencias al tipo RecursivePageTable . Por lo tanto, ahora tenemos una abstracción segura y una semántica clara de propiedad. Esto asegura que no podremos cambiar accidentalmente la tabla de páginas en acceso compartido, porque para cambiarla, debemos tomar posesión de RecursivePageTable exclusivamente.

Esta función da el mismo resultado que la función de traducción original escrita manualmente.

Hacer que las características inseguras sean más seguras


memory::inites una función insegura: requiere un bloque para llamarlo unsafe, porque la persona que llama debe garantizar que se cumplan ciertos requisitos. En nuestro caso, el requisito es que la dirección transmitida se asigne con precisión al marco físico de la tabla de páginas de nivel 4. Todo el cuerpo de la función insegura se coloca

en el bloque unsafepara que todo tipo de operaciones se realicen sin crear bloques adicionales unsafe. Por lo tanto, no necesitamos un bloque inseguro para desreferenciar level_4_table_ptr:

 pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = &mut *level_4_table_ptr; // <- this operation is unsafe RecursivePageTable::new(level_4_table).unwrap() } 

El problema es que no vemos de inmediato qué partes son inseguras. Por ejemplo, sin mirar la definición de una función, RecursivePageTable::new no podemos decir si es segura o no. Por lo tanto, es muy fácil omitir accidentalmente algún código inseguro.

Para evitar este problema, puede agregar una función incorporada segura:

 // in src/memory.rs pub unsafe fn init(level_4_table_addr: usize) -> RecursivePageTable<'static> { /// Rust currently treats the whole body of unsafe functions as an unsafe /// block, which makes it difficult to see which operations are unsafe. To /// limit the scope of unsafe we use a safe inner function. fn init_inner(level_4_table_addr: usize) -> RecursivePageTable<'static> { let level_4_table_ptr = level_4_table_addr as *mut PageTable; let level_4_table = unsafe { &mut *level_4_table_ptr }; RecursivePageTable::new(level_4_table).unwrap() } init_inner(level_4_table_addr) } 

Ahora se unsaferequiere nuevamente el bloqueo para desreferenciar level_4_table_ptr, e inmediatamente vemos que estas son las únicas operaciones inseguras. Rust actualmente tiene un RFC abierto para cambiar esta propiedad fallida de funciones inseguras.

Crea un nuevo mapeo


Cuando leemos las tablas de páginas y creamos la función de conversión, el siguiente paso es crear una nueva asignación en la jerarquía de tablas de páginas.

La complejidad de esta operación depende de la página virtual que queremos mostrar. En el caso más simple, ya existe una tabla de páginas de nivel 1 para esta página, y solo tenemos que hacer una entrada. En el caso más difícil, la página está en el área de memoria para la cual el nivel 3 aún no existe, por lo que primero debe crear nuevas tablas de nivel 3, nivel 2 y nivel 1.

Comencemos con un caso simple cuando no necesite crear nuevas tablas. El cargador se carga en el primer megabyte del espacio de direcciones virtuales, por lo que sabemos que para esta región hay una tabla válida de nivel 1. Para nuestro ejemplo, podemos seleccionar cualquier página no utilizada en esta área de memoria, por ejemplo, la página en la dirección 0x1000. Utilizamos el 0xb8000marco del búfer de texto VGA como el marco deseado . Es muy fácil comprobar cómo funciona nuestra traducción de direcciones.

Lo implementamos en una nueva función create_mapingen el módulo memory:

 // in src/memory.rs use x86_64::structures::paging::{FrameAllocator, PhysFrame, Size4KiB}; pub fn create_example_mapping( recursive_page_table: &mut RecursivePageTable, frame_allocator: &mut impl FrameAllocator<Size4KiB>, ) { use x86_64::structures::paging::PageTableFlags as Flags; let page: Page = Page::containing_address(VirtAddr::new(0x1000)); let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000)); let flags = Flags::PRESENT | Flags::WRITABLE; let map_to_result = unsafe { recursive_page_table.map_to(page, frame, flags, frame_allocator) }; map_to_result.expect("map_to failed").flush(); } 

La función acepta una referencia mutable a RecursivePageTable(la cambiará) y FrameAllocator, que se explica a continuación. Luego aplica la función map_toen la bandeja Mapperpara mapear la página en la dirección 0x1000con el marco físico en la dirección 0xb8000. La función no es segura, porque es posible violar la seguridad de la memoria con argumentos no válidos.

Además de los argumentos pagey frame, la función map_totoma dos argumentos más. El tercer argumento es el conjunto de banderas para la tabla de páginas. Establecemos la bandera PRESENTnecesaria para todas las entradas válidas y la bandera WRITABLEpara la escritura.

El cuarto argumento debería ser alguna estructura que implemente el rasgo FrameAllocator. Este argumento es necesario por el método.map_toporque crear nuevas tablas de páginas puede requerir marcos no utilizados. La aplicación requiere el rasgo argumento Size4KiB, como los tipos Pagey PhysFrameson universales para el rasgo PageSize, trabajando con 4 páginas estándar KiB y con enormes páginas 2 MiB / 1 GIB.

La función map_topuede fallar, por lo que vuelve Result. Dado que este es solo un ejemplo de código que no debería ser confiable, simplemente lo usamos expectcon pánico cuando se produce un error. Si tiene éxito, la función devuelve un tipo MapperFlushque proporciona una manera fácil de borrar la página recientemente emparejada del método del búfer de traducción asociativa (TLB) flush. Me gustaResult, el tipo usa el atributo #[must_use]y emite una advertencia si accidentalmente olvidamos aplicarlo.

Como sabemos que la dirección 0x1000no requiere nuevas tablas de páginas, FrameAllocatorsiempre puede volver None. Para probar la función, cree esto 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 } } 

(Si aparece el error 'el método allocate_frameno es miembro del rasgo FrameAllocator', debe actualizar x86_64a la versión 0.4.0.)

Ahora podemos probar la nueva función de traducción:

 // in src/main.rs #[cfg(not(test))] #[no_mangle] pub extern "C" fn _start() -> ! { […] // initialize GDT, IDT, PICS use blog_os::memory::{create_example_mapping, EmptyFrameAllocator}; const LEVEL_4_TABLE_ADDR: usize = 0o_177777_777_777_777_777_0000; let mut recursive_page_table = unsafe { memory::init(LEVEL_4_TABLE_ADDR) }; create_example_mapping(&mut recursive_page_table, &mut EmptyFrameAllocator); unsafe { (0x1900 as *mut u64).write_volatile(0xf021f077f065f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Primero, creamos una asignación para la página en la dirección 0x1000, llamando a la función create_example_mappingcon un enlace mutable a la instancia RecursivePageTable. Esto traduce la página 0x1000a un búfer de texto VGA, por lo que veremos algunos resultados en la pantalla.

Luego escribimos un valor en esta página 0xf021f077f065f04e, que corresponde a la línea "¡Nuevo!" sobre un fondo blanco Simplemente no es necesario que escriba este valor inmediatamente en la parte superior de la página 0x1000, porque la línea superior se moverá a continuación de la pantalla printlny lo escribirá en un desplazamiento 0x900que se encuentra aproximadamente en el medio de la pantalla. Como sabemos por el artículo "Modo de texto VGA" , escribir en el búfer VGA debe ser volátil, por lo que utilizamos el método write_volatile.

Cuando lo ejecutamos en QEMU, vemos esto:



La inscripción en la pantalla.

El código funcionó porque ya había una tabla de nivel 1 para mostrar la página 0x1000. Si intentamos traducir una página para la que aún no existe dicha tabla, la función map_todevolverá un error, ya que intentará seleccionar marcos para crear nuevas tablas de páginas EmptyFrameAllocator. Lo veremos si intentamos traducir la página en 0xdeadbeaf000lugar de 0x1000:

 // in src/memory.rs pub fn create_example_mapping(…) { […] let page: Page = Page::containing_address(VirtAddr::new(0xdeadbeaf000)); […] } // in src/main.rs #[no_mangle] pub extern "C" fn _start() -> ! { […] unsafe { (0xdeadbeaf900 as *mut u64).write_volatile(0xf021f077f065f04e)}; […] } 

Al comenzar, un pánico comienza con el siguiente mensaje de error:

 entró en pánico en 'map_to falló: FrameAllocationFailed', /.../result.rs:999haps 

Para mostrar páginas que aún no tienen una tabla de nivel de página 1, debe crear la correcta FrameAllocator. Pero, ¿cómo saber qué cuadros son gratuitos y cuánta memoria física está disponible?

Información de arranque


Las diferentes computadoras tienen diferentes cantidades de memoria física y las diferentes áreas reservadas por dispositivos como VGA difieren. Solo el BIOS o el firmware UEFI saben exactamente qué áreas de memoria se pueden usar y cuáles están reservadas. Ambos estándares de firmware proporcionan funciones para obtener una tarjeta de asignación de memoria, pero solo se pueden llamar al comienzo de la descarga. Por lo tanto, nuestro gestor de arranque ya ha solicitado esta (y otra) información del BIOS.

Para pasar información al núcleo del sistema operativo, el cargador como argumento al llamar a la función _startproporciona un enlace a la estructura de información del arranque. Agregue este argumento a nuestra función:

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

La estructura BootInfoaún se está finalizando, así que no se sorprenda cuando se cuelgue al actualizar a futuras versiones del gestor de arranque que no son compatibles con semver . Por el momento se cuenta con tres campos p4_table_addr, memory_mapy package:

  • El campo p4_table_addrcontiene una dirección virtual recursiva de la tabla de páginas de nivel 4. Gracias a esto, no es necesario registrar la dirección de forma rígida 0o_177777_777_777_777_777_0000.
  • El campo memory_mapes de mayor interés, ya que contiene una lista de todas las áreas de memoria y su tipo (no utilizado, reservado u otros).
  • El campo packagees la función actual para asociar datos adicionales con el cargador. La implementación no está completa, por lo que podemos ignorarla por ahora.

Antes de usar el campo memory_mappara crear el correcto FrameAllocator, queremos garantizar el tipo correcto de argumento boot_info.

Macro entry_point


Como se _startllama externamente, no se verifica la firma de la función. Esto significa que los argumentos arbitrarios no conducirán a errores de compilación, pero pueden causar un bloqueo o un comportamiento de tiempo de ejecución indefinido.

Para verificar la firma, la caja bootloaderpara definir la función Rust como punto de entrada utiliza una macro entry_pointcon tipos validados. Reescribimos nuestra función para esta macro:

 // in src/main.rs use bootloader::{bootinfo::BootInfo, entry_point}; entry_point!(kernel_main); #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS let mut recursive_page_table = unsafe { memory::init(boot_info.p4_table_addr as usize) }; […] // create and test example mapping println!("It did not crash!"); blog_os::hlt_loop(); } 

Para el punto de entrada, ya no necesita usar extern "C"o no_mangle, ya que la macro establece el punto de entrada real de bajo nivel _start. La función kernel_mainahora se ha convertido en una función Rust completamente normal, por lo que podemos elegir un nombre arbitrario para ella. Es importante que ya esté escrito, de modo que se produzca un error de compilación si cambia la firma de la función, por ejemplo, agregando un argumento o cambiando su tipo.

Tenga en cuenta que ahora estamos enviando a una memory::initdirección codificada, pero boot_info.p4_table_addr. Por lo tanto, el código funcionará incluso si la versión futura del gestor de arranque selecciona otra entrada en la tabla de tabla de nivel de página 4 para la visualización recursiva.

Selección de marco


Ahora, gracias a la información del BIOS, tenemos acceso a la tarjeta de asignación de memoria, para que pueda hacer un distribuidor de trama normal. Comencemos con el esqueleto general:

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

El campo se framesinicializa mediante un iterador de marco arbitrario . Esto le permite simplemente delegar llamadas allocal método Iterator :: next .

La inicialización BootInfoFrameAllocatortiene lugar en una nueva función 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.into_iter().step_by(4096)); // create `PhysFrame` types from the start addresses let frames = frame_addresses.map(|addr| { PhysFrame::containing_address(PhysAddr::new(addr)) }); BootInfoFrameAllocator { frames } } 

Esta función, utilizando un combinador, convierte el mapa de asignación de memoria original en un iterador de las tramas físicas utilizadas:

  • iter MemoryRegion . filter , . , , (, ) , InUse . , , - .
  • map range Rust .
  • El tercer paso es el más difícil: convertimos cada rango en un iterador usando el método into_iter, y luego seleccionamos cada 4096a dirección con step_by. Como el tamaño de la página es de 4096 bytes (4 KiB), obtenemos la dirección del comienzo de cada marco. La página del cargador alinea todas las áreas de memoria utilizadas, por lo que no necesitamos un código de alineación o redondeo. Reemplazando mapcon flat_map, obtenemos en su Iterator<Item = u64>lugar Iterator<Item = Iterator<Item = u64>>.
  • En la etapa final, convertiremos las direcciones iniciales a tipos PhysFramepara construir la requerida Iterator<Item = PhysFrame>. Luego use este iterador para crear y devolver uno nuevo BootInfoFrameAllocator.

Ahora podemos cambiar nuestra función kernel_mainpara que pase la instancia en su BootInfoFrameAllocatorlugar EmptyFrameAllocator:

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // initialize GDT, IDT, PICS use x86_64::structures::paging::{PageTable, RecursivePageTable}; let mut recursive_page_table = unsafe { memory::init(boot_info.p4_table_addr as usize) }; // new let mut frame_allocator = memory::init_frame_allocator(&boot_info.memory_map); blog_os::memory::create_mapping(&mut recursive_page_table, &mut frame_allocator); unsafe { (0xdeadbeaf900 as *mut u64).write_volatile(0xf021f077f065f04e)}; println!("It did not crash!"); blog_os::hlt_loop(); } 

Ahora la traducción de la dirección es exitosa, y nuevamente vemos el mensaje en blanco y negro "¡Nuevo!" En la pantalla .Detrás de escena, el método map_tocrea tablas de páginas faltantes de la siguiente manera:

  • Extrae un marco no utilizado de frame_allocator.
  • Coincide con una entrada de tabla de nivel superior con este marco. Ahora se puede acceder al marco a través de una tabla de páginas recursivas.
  • Cero el marco para crear una nueva tabla de páginas vacía.
  • Va a la tabla del siguiente nivel.

Aunque nuestra función create_mapinges solo un ejemplo, ahora podemos crear nuevas asignaciones para páginas arbitrarias. Esto es muy útil al asignar memoria e implementar múltiples subprocesos en futuros artículos.

Resumen


En este artículo, aprendió a usar una tabla recursiva de nivel 4 para traducir todos los marcos a direcciones virtuales computables. Utilizamos este método para implementar la función de traducción de direcciones y crear una nueva asignación en las tablas de páginas.

Vimos que crear nuevas asignaciones requiere marcos no utilizados para nuevas tablas. Tal distribuidor de trama puede implementarse en base a la información del BIOS que el gestor de arranque pasa a nuestro núcleo.

Que sigue


En el siguiente artículo, crearemos un área de memoria de almacenamiento dinámico para nuestro kernel, que nos permitirá asignar memoria y usar diferentes tipos de colecciones .

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


All Articles