Sistema operacional no Rust. Memória de página: Avançado

Este artigo explica como o kernel do sistema operacional pode acessar os quadros de memória física. Estudaremos a função de conversão de endereços virtuais em físicos. Também descobriremos como criar novos mapeamentos nas tabelas de páginas.

Este blog está publicado no GitHub . Se você tiver alguma dúvida ou problema, abra o ticket correspondente lá. Todas as fontes do artigo estão aqui .

1. Introdução


No último artigo, aprendemos sobre os princípios da memória de paginação e como funcionam as tabelas de página de quatro níveis no x86_64. Também descobrimos que o carregador já configurou a hierarquia da tabela de páginas para nosso kernel, portanto, o kernel é executado em endereços virtuais. Isso melhora a segurança, mas surge o problema: como acessar endereços físicos reais armazenados nas entradas da tabela de páginas ou no CR3 ?

Na primeira seção do artigo, discutiremos o problema e as diferentes abordagens para resolvê-lo. Em seguida, implementamos uma função que percorre a hierarquia das tabelas de páginas para converter endereços virtuais em físicos. Por fim, aprenda como criar novos mapeamentos nas tabelas de páginas e encontre os quadros de memória não utilizados para criar novas tabelas.

Atualizações de dependência


Para funcionar, você precisa do x86_64 versão 0.4.0 ou posterior. Atualize a dependência em nosso Cargo.toml :

 [dependencies] x86_64 = "0.4.0" # or later 

Acesso a tabelas de páginas


Acessar tabelas de páginas do kernel não é tão fácil quanto parece. Para entender o problema, dê uma olhada na hierarquia de tabela de quatro níveis do artigo anterior:



O importante é que cada entrada da página armazene o endereço físico da próxima tabela. Isso evita a tradução desses endereços, o que reduz o desempenho e leva facilmente a loops sem fim.

O problema é que não podemos acessar diretamente endereços físicos do kernel, pois ele também funciona em endereços virtuais. Por exemplo, quando vamos para o endereço 4 KiB , obtemos acesso ao endereço virtual 4 KiB , e não ao endereço físico onde a tabela de páginas do 4º nível está armazenada. Se queremos acessar o endereço físico de 4 KiB , precisamos usar um endereço virtual, que é traduzido para ele.

Portanto, para acessar os quadros das tabelas de páginas, é necessário mapear algumas páginas virtuais para esses quadros. Existem diferentes maneiras de criar esses mapeamentos.

1. Uma solução simples é a exibição idêntica de todas as tabelas de páginas .



Neste exemplo, vemos a exibição idêntica de quadros. Os endereços físicos das tabelas de páginas são ao mesmo tempo endereços virtuais válidos, para que possamos acessar facilmente as tabelas de páginas de todos os níveis, começando com o registro CR3.

No entanto, essa abordagem confunde o espaço de endereço virtual e dificulta a descoberta de grandes áreas contíguas de memória livre. Digamos que queremos criar uma área de memória virtual de 1000 KiB na figura acima, por exemplo, para exibir um arquivo na memória . Não podemos começar com a região de 28 KiB , porque ela fica em uma página já ocupada em 1004 KiB . Portanto, você precisará procurar mais até encontrarmos um fragmento grande e adequado, por exemplo, com 1008 KiB . Há o mesmo problema de fragmentação da memória segmentada.

Além disso, a criação de novas tabelas de páginas é muito mais complicada, pois precisamos encontrar quadros físicos cujas páginas correspondentes ainda não foram usadas. Por exemplo, para o nosso arquivo, reservamos uma área de 1000 KiB de memória virtual , começando no endereço 1008 KiB . Agora não podemos mais usar nenhum quadro com um endereço físico entre 1000 KiB e 2008 KiB , porque ele não pode ser exibido de forma idêntica.

2. Outra opção é transmitir tabelas de páginas apenas temporariamente quando você precisar acessá-las. Para comparações temporárias, é necessária uma exibição idêntica apenas da tabela de primeiro nível:



Nesta figura, uma tabela de nível 1 gerencia os 2 primeiros MiB de espaço de endereço virtual. Isso é possível porque o acesso é realizado a partir do registro CR3 através de entradas nulas nas tabelas dos níveis 4, 3 e 2. O registro com o índice 8 converte a página virtual em 32 KiB em um quadro físico em 32 KiB , identificando a própria tabela de nível 1. Na figura, isso é mostrado por uma seta horizontal.

Ao escrever na tabela de nível 1 mapeada de forma idêntica, nosso kernel pode criar até 511 comparações de tempo (512 menos o registro necessário para o mapeamento de identidade). No exemplo acima, o kernel correspondia ao registro nulo de uma tabela de nível 1 com um quadro em 24 KiB . Isso criou um mapeamento temporário da página virtual em 0 KiB para o quadro físico da tabela de nível da página 2 indicada pela seta pontilhada. Agora o kernel pode acessar a tabela de nível 2 gravando em uma página que começa com 0 KiB .

Portanto, o acesso a um quadro arbitrário da tabela de páginas com mapeamentos temporários consiste nas seguintes ações:

  • Encontre uma entrada gratuita na tabela de nível 1 exibida de forma idêntica.
  • Mapeie essa entrada para o quadro físico da tabela de páginas que queremos acessar.
  • Acesse esse quadro através da página virtual associada à entrada.
  • Defina o registro novamente como não utilizado, removendo o mapeamento temporário.

Com essa abordagem, o espaço de endereço virtual permanece limpo, pois as mesmas 512 páginas virtuais são usadas constantemente. A desvantagem é uma certa dificuldade, especialmente porque uma nova comparação pode exigir a alteração de vários níveis da tabela, ou seja, precisamos repetir o processo descrito várias vezes.

3. Embora ambas as abordagens acima funcionem, existe um terceiro método: tabelas de páginas recursivas . Ele combina as vantagens de ambas as abordagens: compara constantemente todos os quadros das tabelas de páginas sem exigir comparações temporárias e também mantém as páginas mapeadas lado a lado, evitando a fragmentação do espaço de endereço virtual. Este é o método que vamos usar.

Tabelas de páginas recursivas


A idéia é traduzir alguns registros da tabela de quarto nível para ela mesma. Assim, reservamos uma parte do espaço de endereço virtual e mapeamos todos os quadros de tabela atuais e futuros para esse espaço.

Vejamos um exemplo para entender como tudo isso funciona:



A única diferença do exemplo no início do artigo é um registro adicional com o índice 511 na tabela de nível 4, que é mapeado para o quadro físico 4 KiB , localizado nesta tabela em si.

Quando a CPU entra nesse registro, ela não se refere à tabela de nível 3, mas novamente à tabela de nível 4. Isso é semelhante a uma função recursiva que se chama. É importante que o processador suponha que cada registro na tabela de nível 4 aponte para uma tabela de nível 3. Portanto, agora ele trate a tabela de nível 4 como uma tabela de nível 3. Isso funciona porque as tabelas de todos os níveis em x86_64 têm a mesma estrutura.

Seguindo um registro recursivo uma ou mais vezes antes de iniciar a conversão real, podemos reduzir efetivamente o número de níveis pelos quais o processador passa. Por exemplo, se seguirmos o registro recursivo uma vez e depois formos para a tabela de nível 3, o processador achará que a tabela de nível 3 é uma tabela de nível 2. Seguindo em frente, ele considera a tabela de nível 2 como uma tabela de nível 1 e a tabela de nível 1 como mapeada quadro na memória física. Isso significa que agora podemos ler e gravar na tabela de nível 1 da página, porque o processador pensa que esse é um quadro mapeado. A figura abaixo mostra as cinco etapas dessa tradução:



Da mesma forma, podemos seguir uma entrada recursiva duas vezes antes de iniciar a conversão para reduzir o número de níveis passados ​​para dois:



Vamos seguir este procedimento passo a passo. Primeiro, a CPU segue uma entrada recursiva na tabela de nível 4 e pensa que alcançou a tabela de nível 3. Em seguida, segue o registro recursivo novamente e pensa que alcançou o nível 2. Mas, na realidade, ainda está no nível 4. Em seguida, a CPU vai para o novo endereço e entra na tabela de nível 3, mas acha que já está na tabela de nível 1. Por fim, no próximo ponto de entrada da tabela de nível 2, o processador acha que acessou o quadro de memória física. Isso nos permite ler e gravar em uma tabela de nível 2.

Também são acessadas as tabelas dos níveis 3 e 4. Para acessar a tabela do nível 3, seguimos um registro recursivo três vezes: o processador pensa que já está na tabela do nível 1 e, na próxima etapa, atingimos o nível 3, que a CPU considera como um quadro mapeado. Para acessar a tabela de nível 4, basta seguir o registro recursivo quatro vezes até que o processador processe a tabela de nível 4 como um quadro mapeado (em azul na figura abaixo).



O conceito é difícil de entender a princípio, mas, na prática, funciona muito bem.

Cálculo de endereço


Assim, podemos acessar tabelas de todos os níveis seguindo um registro recursivo uma ou mais vezes. Como os índices em tabelas de quatro níveis são derivados diretamente do endereço virtual, endereços virtuais especiais devem ser criados para esse método. Como lembramos, os índices da tabela de páginas são extraídos do endereço da seguinte maneira:



Suponha que desejamos acessar uma tabela de nível 1 que exibe uma página específica. Como aprendemos acima, você precisa passar por um registro recursivo uma vez e depois pelos índices do 4º, 3º e 2º níveis. Para fazer isso, movemos todos os blocos de endereço um bloco para a direita e configuramos o índice do registro recursivo para o local do índice inicial do nível 4:



Para acessar a tabela de nível 2 desta página, movemos todos os blocos de índice dois blocos para a direita e configuramos o índice recursivo no local dos dois blocos de origem: nível 4 e nível 3:



Para acessar a tabela de nível 3, fazemos o mesmo, apenas mudamos para a direita, já com três blocos de endereços.



Finalmente, para acessar a tabela de nível 4, mova todos os quatro blocos para a direita.



Agora você pode calcular endereços virtuais para tabelas de páginas dos quatro níveis. Podemos até calcular um endereço que aponte exatamente para uma entrada específica da tabela de páginas multiplicando seu índice por 8, o tamanho da entrada da tabela de páginas.

A tabela abaixo mostra a estrutura de endereços para acessar vários tipos de quadros:

Endereço virtual paraEstrutura de endereço ( octal )
Page0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
Entrada na tabela de nível 10o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
Entrada em uma tabela de nível 20o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
Entrada em uma tabela de nível 30o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
Entrada na tabela de nível 40o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

Aqui, é o índice de nível 4, é o nível 3, é o nível 2 e DDD é o índice de nível 1 para o quadro exibido, EEEE é o seu deslocamento. RRR é o índice do registro recursivo. Um índice (três dígitos) é convertido em um deslocamento (quatro dígitos) multiplicando por 8 (o tamanho da entrada da tabela da página). Com esse deslocamento, o endereço resultante aponta diretamente para a entrada da tabela de páginas correspondente.

SSSS são bits de expansão do dígito assinado, ou seja, são cópias do bit 47. Esse é um requisito especial para endereços válidos na arquitetura x86_64, que discutimos em um artigo anterior .

Os endereços são octais , pois cada caractere octal representa três bits, o que permite separar claramente os índices de 9 bits das tabelas em diferentes níveis. Isso não é possível no sistema hexadecimal, onde cada caractere representa quatro bits.

Implementação


Depois de toda essa teoria, podemos finalmente prosseguir com a implementação. Convenientemente, o carregador gerou não apenas tabelas de páginas, mas também uma exibição recursiva no último registro da tabela de nível 4. O carregador fez isso porque, caso contrário, haveria um problema de galinha ou ovo: precisamos acessar a tabela de nível 4 para criar um mapa recursivo mas não podemos acessá-lo sem nenhuma exibição.

Já usamos esse mapeamento recursivo no final do artigo anterior para acessar a tabela de nível 4 através do endereço codificado 0xffff_ffff_ffff_f000 . Se convertermos esse endereço em octal e compará-lo com a tabela acima, veremos que ele corresponde exatamente à estrutura do registro na tabela de nível 4 com RRR = 0o777 , AAAA = 0 e os bits de extensão do sinal 1 :

  estrutura: 0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
 endereço: 0o_177777_777_777_777_777_0000 

Graças ao conhecimento de tabelas recursivas, agora podemos criar endereços virtuais para acessar todas as tabelas ativas. E faça a função de transmissão.

Tradução de endereços


Como primeira etapa, crie uma função que converta um endereço virtual em um endereço físico, passando pela hierarquia das tabelas 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) } 

Primeiro, introduzimos variáveis ​​para o índice recursivo (511 = 0o777 ) e os bits de extensão de sinal (cada um é 1). Em seguida, calculamos os índices das tabelas de páginas e o deslocamento por meio de operações bit a bit, conforme indicado na ilustração:



A próxima etapa é calcular os endereços virtuais das tabelas de quatro páginas, conforme descrito na seção anterior. Em seguida, na função, convertemos cada um desses endereços em links de PageTable . Essas são operações inseguras porque o compilador não pode saber que esses endereços são válidos.

Após o cálculo do endereço, usamos o operador de indexação para exibir o registro na tabela de nível 4. Se esse registro for zero, não haverá tabela de nível 3 para esse registro de nível 4. Isso significa que addr não addr mapeado para nenhuma memória física. Então, retornamos None . Caso contrário, sabemos que existe uma tabela de nível 3. Em seguida, repetimos o procedimento, como no nível anterior.

Depois de verificar três páginas de um nível superior, podemos finalmente ler o registro da tabela de nível 1, que indica o quadro físico com o qual o endereço é mapeado. Como a última etapa, adicione o deslocamento da página e retorne o endereço.

Se tivéssemos certeza de que o endereço estava mapeado, poderíamos acessar diretamente a tabela de nível 1 sem consultar as páginas de um nível superior. Mas como não sabemos disso, primeiro precisamos verificar se existe uma tabela de nível 1; caso contrário, nossa função retornará um erro de falta de página para endereços não correspondentes.

Experimente


Vamos tentar usar a função de tradução para endereços virtuais em nossa função _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(); } 


Após o início, vemos o seguinte resultado:



Como esperado, o endereço 0xb8000 associado ao identificador é convertido no mesmo endereço físico. A página de código e a pilha são convertidas em alguns endereços físicos arbitrários, que dependem de como o carregador criou o mapeamento inicial para o nosso kernel.

RecursivePageTable


x86_64 fornece um tipo RecursivePageTable que implementa abstrações seguras para várias operações da tabela de páginas. Usando esse tipo, você pode implementar a função translate_addr muito mais concisa:

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

O tipo RecursivePageTable encapsula totalmente os rastreamentos inseguros da tabela da página, portanto, o código unsafe na função translate_addr não é mais necessário. A função init permanece insegura devido à necessidade de garantir a correção do level_4_table_addr passado.

Nossa função _start deve ser atualizada para assinar novamente a função da seguinte maneira:

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

Agora, em vez de passar LEVEL_4_TABLE_ADDR para translate_addr e acessar as tabelas de páginas por meio de ponteiros brutos não seguros, passamos referências ao tipo RecursivePageTable . Assim, agora temos uma abstração segura e uma semântica clara de propriedade. Isso garante que não seremos capazes de alterar acidentalmente a tabela de páginas no acesso compartilhado, pois a alteração requer a posse exclusiva de RecursivePageTable .

Esta função fornece o mesmo resultado que a função de tradução original escrita manualmente.

Tornando os recursos não seguros mais seguros


memory::inité uma função insegura: requer um bloco para chamá-lo unsafe, porque o chamador deve garantir que certos requisitos sejam atendidos. No nosso caso, o requisito é que o endereço transmitido seja mapeado com precisão para o quadro físico da tabela de páginas de nível 4. Todo o corpo da função insegura é colocado

no bloco unsafepara que todos os tipos de operações sejam executados sem a criação de blocos adicionais unsafe. Portanto, não precisamos de um bloco não seguro 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() } 

O problema é que não vemos imediatamente quais partes são inseguras. Por exemplo, sem olhar para a definição de uma função, RecursivePageTable::new não podemos dizer se é segura ou não. Portanto, é muito fácil ignorar acidentalmente algum código não seguro.

Para evitar esse problema, você pode adicionar uma função interna 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) } 

Agora, o bloco é unsafenovamente necessário para desreferenciar level_4_table_ptr, e imediatamente vemos que essas são as únicas operações inseguras. Atualmente, o Rust possui uma RFC aberta para alterar essa propriedade malsucedida de funções não seguras.

Crie um novo mapeamento


Quando lemos as tabelas de páginas e criamos a função de conversão, o próximo passo é criar um novo mapeamento na hierarquia de tabelas de páginas.

A complexidade desta operação depende da página virtual que queremos exibir. No caso mais simples, já existe uma tabela de páginas de nível 1 para esta página e precisamos apenas fazer uma entrada. No caso mais difícil, a página está na área de memória para a qual o nível 3 ainda não existe; portanto, primeiro você precisa criar novas tabelas de nível 3, nível 2 e nível 1.

Vamos começar com um caso simples, quando você não precisa criar novas tabelas. O carregador é carregado no primeiro megabyte do espaço de endereço virtual, portanto, sabemos que para esta região há uma tabela válida de nível 1. No nosso exemplo, podemos selecionar qualquer página não utilizada nesta área de memória, por exemplo, a página no endereço 0x1000. Usamos o 0xb8000quadro do buffer de texto VGA como o quadro desejado . É muito fácil verificar como funciona a tradução de endereços.

Nós o implementamos em uma nova função create_mapingno 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(); } 

A função aceita uma referência mutável para RecursivePageTable(ela será alterada) e FrameAllocator, explicada abaixo. Em seguida, aplica a função map_tona bandeja Mapperpara mapear a página no endereço 0x1000com o quadro físico no endereço 0xb8000. A função não é segura, porque é possível violar a segurança da memória com argumentos inválidos.

Além dos argumentos pagee frame, a função map_torecebe mais dois argumentos. O terceiro argumento é o conjunto de sinalizadores para a tabela de páginas. Definimos o sinalizador PRESENTnecessário para todas as entradas válidas e o sinalizador WRITABLEpara gravabilidade.

O quarto argumento deve ser alguma estrutura que implemente a característica FrameAllocator. Este argumento é necessário pelo métodomap_toporque a criação de novas tabelas de páginas pode exigir quadros não utilizados. A implementação requer o traço argumento Size4KiB, como tipos Pagee PhysFramesão universal para a característica PageSize, trabalhando com 4 páginas padrão KiB e com enormes páginas 2 MiB / 1 IB.

A função map_topode falhar e, portanto, retorna Result. Como este é apenas um exemplo de código que não deve ser confiável, simplesmente o usamos expectcom pânico quando ocorre um erro. Se for bem-sucedida, a função retornará um tipo MapperFlushque fornece uma maneira fácil de limpar a página correspondida recentemente do método TLB (associative translation buffer) flush. CurtirResult, o tipo usa o atributo #[must_use]e emite um aviso se esquecermos acidentalmente de aplicá-lo.

Como sabemos que o endereço 0x1000não requer novas tabelas de páginas, ele FrameAllocatorsempre pode retornar None. Para testar a função, crie isto 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 } } 

(Se o erro 'o método allocate_framenão é um membro da característica FrameAllocator' aparecer , você precisará atualizar x86_64para a versão 0.4.0.)

Agora podemos testar a nova função de conversão:

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

Primeiro, criamos um mapeamento para a página no endereço 0x1000, chamando a função create_example_mappingcom um link mutável para a instância RecursivePageTable. Isso traduz a página 0x1000em um buffer de texto VGA, portanto veremos algum resultado na tela.

Em seguida, escrevemos um valor nesta página 0xf021f077f065f04e, que corresponde à linha "Novo!" sobre um fundo branco Só não é necessário escrever esse valor diretamente na parte superior da página 0x1000, porque a linha superior se moverá a seguir da tela printlne gravará no deslocamento 0x900, localizado aproximadamente no meio da tela. Como sabemos no artigo "Modo de texto VGA" , a gravação no buffer VGA deve ser volátil, por isso usamos o método write_volatile.

Quando o executamos no QEMU, vemos o seguinte:



A inscrição na tela.

O código funcionou porque já havia uma tabela de nível 1 para exibir a página 0x1000. Se tentarmos traduzir uma página para a qual essa tabela ainda não existe, a função map_toretornará um erro, porque tentará selecionar quadros para criar novas tabelas de páginas EmptyFrameAllocator. Veremos isso se tentarmos traduzir a página em 0xdeadbeaf000vez 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)}; […] } 

Ao iniciar, um pânico começa com a seguinte mensagem de erro:

 entre em pânico em 'map_to falhou: FrameAllocationFailed', /.../result.rs:999haps 

Para exibir páginas que ainda não possuem uma tabela de nível 1, é necessário criar a correta FrameAllocator. Mas como você sabe quais quadros são gratuitos e quanta memória física está disponível?

Informações de inicialização


Computadores diferentes têm quantidades diferentes de memória física e áreas diferentes reservadas por dispositivos como VGA. Somente o firmware BIOS ou UEFI sabe exatamente quais áreas de memória podem ser usadas e quais são reservadas. Os dois padrões de firmware fornecem funções para obter um cartão de alocação de memória, mas só podem ser chamados no início do download. Portanto, nosso gerenciador de inicialização já solicitou essas (e outras) informações do BIOS.

Para passar informações ao kernel do sistema operacional, o carregador como argumento ao chamar a função _startfornece um link para a estrutura de informações da inicialização. Adicione este argumento à nossa função:

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

A estrutura BootInfoainda está sendo finalizada, portanto, não se surpreenda com falhas ao atualizar para versões futuras do gerenciador de inicialização que não são compatíveis com o semver . No momento em que tem três campos p4_table_addr, memory_mape package:

  • O campo p4_table_addrcontém um endereço virtual recursivo da tabela de páginas de nível 4. Graças a isso, não é necessário registrar o endereço com força 0o_177777_777_777_777_777_0000.
  • O campo memory_mapé de maior interesse, pois contém uma lista de todas as áreas de memória e seu tipo (não utilizado, reservado ou outros).
  • O campo packageé a função atual para associar dados adicionais ao carregador. A implementação não está concluída, portanto podemos ignorá-la por enquanto.

Antes de usar o campo memory_mappara criar o caminho certo FrameAllocator, queremos garantir o tipo correto de argumento boot_info.

Macro entry_point


Como _starté chamada externamente, a assinatura da função não é verificada. Isso significa que argumentos arbitrários não levarão a erros de compilação, mas podem causar uma falha ou comportamento indefinido do tempo de execução.

Para verificar a assinatura, a caixa bootloaderpara definir a função Rust como um ponto de entrada usa uma macro entry_pointcom tipos validados. Reescrevemos nossa função 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 ponto de entrada, você não precisa mais usar extern "C"ou no_mangle, uma vez que a macro define o ponto de entrada real de baixo nível _start. A função kernel_mainagora se tornou uma função Rust completamente normal, para que possamos escolher um nome arbitrário para ela. É importante que ele já esteja digitado, para que ocorra um erro de compilação se você alterar a assinatura da função, por exemplo, adicionando um argumento ou alterando seu tipo.

Observe que agora estamos enviando para um memory::initendereço codificado, mas boot_info.p4_table_addr. Portanto, o código funcionará mesmo que a versão futura do carregador de inicialização selecione outra entrada na tabela da tabela no nível 4 da página para exibição recursiva.

Seleção de quadro


Agora, graças às informações do BIOS, temos acesso ao cartão de alocação de memória, para que você possa fazer um distribuidor de quadros normal. Vamos começar com o esqueleto geral:

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

O campo é framesinicializado por um iterador de quadro arbitrário . Isso permite que você simplesmente delegue chamadas allocpara o método Iterator :: next .

A inicialização BootInfoFrameAllocatorocorre em uma nova função 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 função, usando um combinador, converte o mapa de alocação de memória original em um iterador dos quadros físicos usados:

  • iter MemoryRegion . filter , . , , (, ) , InUse . , , - .
  • map range Rust .
  • A terceira etapa é a mais difícil: convertemos cada intervalo em um iterador usando o método into_itere, em seguida, selecionamos cada 4096º endereço com step_by. Como o tamanho da página é de 4096 bytes (4 KiB), obtemos o endereço do início de cada quadro. A página do carregador alinha todas as áreas de memória usadas, portanto, não precisamos de um código de alinhamento ou arredondamento. Substituindo mappor flat_map, chegamos ao Iterator<Item = u64>invés Iterator<Item = Iterator<Item = u64>>.
  • No estágio final, converteremos os endereços iniciais em tipos PhysFramepara criar o necessário Iterator<Item = PhysFrame>. Em seguida, use esse iterador para criar e retornar um novo BootInfoFrameAllocator.

Agora nós podemos mudar a nossa função kernel_mainpara transmitir sua instância BootInfoFrameAllocatorvez 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(); } 

Agora a tradução do endereço foi bem-sucedida - e novamente vemos a mensagem em preto e branco “Novo!” Na tela .Nos bastidores, o método map_tocria tabelas de páginas ausentes da seguinte maneira:

  • Extrai um quadro não utilizado de frame_allocator.
  • Corresponde a uma entrada da tabela de nível superior com esse quadro. O quadro agora está acessível através de uma tabela de páginas recursiva.
  • Zera o quadro para criar uma nova tabela de páginas vazia.
  • Vai para a próxima tabela de nível.

Embora nossa função create_mapingseja apenas um exemplo, agora podemos criar novos mapeamentos para páginas arbitrárias. Isso é muito útil ao alocar memória e implementar multithreading em artigos futuros.

Sumário


Neste artigo, você aprendeu como usar uma tabela recursiva de nível 4 para converter todos os quadros em endereços virtuais computáveis. Usamos esse método para implementar a função de conversão de endereço e criar um novo mapeamento nas tabelas de páginas.

Vimos que a criação de novos mapeamentos requer quadros não utilizados para novas tabelas. Esse distribuidor de quadros pode ser implementado com base nas informações do BIOS que o gerenciador de inicialização passa para o nosso kernel.

O que vem a seguir


No próximo artigo, criaremos uma área de memória heap para o nosso kernel, que permitirá alocar memória e usar diferentes tipos de coleções .

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


All Articles