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