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 para | Estrutura de endereço ( octal ) | 
|---|
| Page | 0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE | 
| Entrada na tabela de nível 1 | 0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD | 
| Entrada em uma tabela de nível 2 | 0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC | 
| Entrada em uma tabela de nível 3 | 0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB | 
| Entrada na tabela de nível 4 | 0o_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:
 
 
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 :
 
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:
 
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:
 
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 é colocadono 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;  
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: 
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: 
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: 
(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: 
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: 
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: 
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ça0o_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 campomemory_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: 
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: 
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: 
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. , , - .
 
- maprange 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 comstep_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. Substituindomapporflat_map, chegamos aoIterator<Item = u64>invésIterator<Item = Iterator<Item = u64>>.
 
- No estágio final, converteremos os endereços iniciais em tipos PhysFramepara criar o necessárioIterator<Item = PhysFrame>. Em seguida, use esse iterador para criar e retornar um novoBootInfoFrameAllocator.
Agora nós podemos mudar a nossa funçãokernel_mainpara transmitir sua instância BootInfoFrameAllocatorvez EmptyFrameAllocator: 
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çãocreate_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 .