Rust上的操作系统。 页面内存:高级

本文介绍了操作系统的内核如何访问物理内存帧。 我们将研究将虚拟地址转换为物理地址的功能。 我们还将弄清楚如何在页表中创建新的映射。

该博客发布在GitHub上 。 如果您有任何疑问或问题,请在此处打开相应的票证。 本文的所有资料都在这里

引言


上一篇文章中,我们了解了分页内存的原理以及x86_64上的四级页表如何工作。 我们还发现加载器已经为内核设置了页表层次结构,因此内核在虚拟地址上运行。 这样可以提高安全性,但是会出现问题:如何访问存储在页表项或CR3实际物理地址?

在本文的第一部分中,我们将讨论该问题以及解决该问题的不同方法。 然后,我们实现了一个功能,它可以潜入页表的层次结构,将虚拟地址转换为物理地址。 最后,学习如何在页表中创建新的映射并查找未使用的内存框架以创建新表。

依赖关系更新


要工作,您需要x86_64 0.4.0或更高版本。 更新我们的Cargo.toml的依赖Cargo.toml

 [dependencies] x86_64 = "0.4.0" # or later 

访问页表


从内核访问页表并不像看起来那样容易。 要了解此问题,请再看看上一篇文章中的四级表层次结构:



重要的是每个页面条目都存储下一张表的物理地址。 这避免了这些地址的转换,从而降低了性能并容易导致无限循环。

问题是我们不能直接从内核访问物理地址,因为它也可以在虚拟地址上工作。 例如,当我们访问地址4 KiB ,我们可以访问虚拟地址4 KiB ,而不能访问存储第四级页面表的物理地址。 如果我们要访问4 KiB的物理地址,则需要使用一些虚拟地址,将其转换为虚拟地址。

因此,要访问页表的框架,您需要将一些虚拟页面映射到这些框架。 有多种创建此类映射的方法。

一种简单的解决方案是所有页表的显示相同



在此示例中,我们看到相同的帧显示。 页表的物理地址同时是有效的虚拟地址,因此我们可以轻松地从寄存器CR3开始访问所有级别的页表。

但是,这种方法会使虚拟地址空间变得混乱,并使得很难找到较大的连续区域的空闲内存。 假设我们要在上图中创建一个1000 KiB虚拟内存区域,例如, 在memory中显示一个文件 。 我们不能从28 KiB区域开始,因为它位于已经占用的页面1004 KiB 。 因此,您将不得不进一步寻找,直到找到合适的大片段,例如1008 KiB 。 存在与分段存储器相同的碎片问题。

另外,新页表的创建要复杂得多,因为我们需要找到尚未使用相应页的物理框架。 例如,对于我们的文件,我们保留了1000 KiB的虚拟内存区域,从地址1008 KiB 。 现在,我们不能再使用物理地址在1000 KiB2008 KiB之间的任何帧,因为它不能显示相同。

2.另一个选择是在需要访问页表时临时广播它们。 对于临时比较,只需要第一级表的相同显示即可:



在此图中,级别1表管理虚拟地址空间的前2个MiB。 这是可能的,因为来自CR3寄存器的访问是通过级别4、3和2的表中的零条目进行的。具有索引8的记录将32 KiB处的虚拟页转换为32 KiB的物理帧,从而标识了1级表本身。在图中用水平箭头表示。

通过写入相同映射的1级表,我们的内核最多可以创建511次时间比较(512减去身份映射所需的记录)。 在上面的示例中,内核将1级表的空记录与24 KiB处的帧进行了匹配。 这将虚拟页面从0 KiB临时映射到页面2级别表的物理帧,由虚线箭头指示。 现在,内核可以通过写入从0 KiB开始的页面来访问2级表。

因此,使用临时映射访问页表的任意框架包括以下操作:

  • 在相同显示的1级表中找到一个免费条目。
  • 将此条目映射到我们要访问的页表的物理框架。
  • 通过与条目关联的虚拟页面访问此框架。
  • 将记录重新设置为未使用,从而删除临时映射。

使用这种方法,虚拟地址空间保持干净,因为经常使用相同的512个虚拟页面。 缺点是麻烦,特别是因为新的比较可能需要更改表的多个级别,也就是说,我们需要重复上述过程几次。

3.尽管以上两种方法都有效,但是还有第三种方法: 递归页表 。 它结合了这两种方法的优点:它不断地比较页表的所有帧,而无需临时比较,并且并排保持相邻页,避免虚拟地址空间的碎片化。 这就是我们将要使用的方法。

递归页表


这个想法是将一些记录从第四级表转换成它本身。 因此,我们实际上保留了虚拟地址空间的一部分,并将所有当前和将来的表框架映射到该空间。

让我们看一个例子,以了解这一切如何工作:



与本文开头的示例唯一的不同是,在第4级表中具有索引511的附加记录被映射到位于该表本身中的物理帧4 KiB

当CPU继续执行该记录时,它不会引用3级表,而是再次引用4级表,这类似于调用自身的递归函数。 重要的是,处理器必须假定4级表中的​​每个条目都指向3级表,所以现在将4级表视为3级表,因为x86_64中所有级别的表都具有相同的结构,所以这种方法有效。

通过在开始实际转换之前跟踪一次或多次递归记录,我们可以有效地减少处理器经历的级别数。 例如,如果我们跟踪一次递归记录,然后转到3级表,则处理器认为3级表是2级表,接着,他将2级表视为1级表,并将1级表视为已映射物理内存中的帧。 这意味着我们现在可以读写页面1级表,因为处理器认为这是一个映射的帧。 下图显示了这种转换的五个步骤:



同样,在开始转换之前,我们可以跟踪两次递归项以减少传递给两个级别的数量:



让我们逐步完成此过程。 首先,CPU跟踪4级表中的​​递归条目,并认为它已到达3级表,然后再次跟踪递归记录,并认为它已达到2级。但实际上,它仍处于4级,然后CPU转到了新地址。并进入第3级表,但认为它已经在第1级表中。最后,在第2级表的下一个入口点,处理器认为它已经访问了物理内存帧。 这使我们可以读写2级表。

还访问了3级和4级表。要访问3级表,我们遵循三遍递归记录:处理器认为它已经在1级表中,并且在下一步中我们达到3级,CPU将其视为映射帧。 要访问4级表本身,我们只需遵循递归记录四次,直到处理器将4级表本身作为映射帧处理(下图中的蓝色)。



起初很难理解这个概念,但实际上它运作良好。

地址计算


因此,我们可以通过遵循一次或多次递归记录来访问所有级别的表。 由于四级表中的索引是直接从虚拟地址派生的,因此必须为此方法创建特殊的虚拟地址。 我们记得,页表索引是从地址中提取的,如下所示:



假设我们要访问显示特定页面的1级表。 如上所述,您需要遍历一次递归记录,然后遍历第四,第三和第二级的索引。 为此,我们将所有地址块向右移动一个块,并将递归记录的索引设置为4级初始索引的位置:



要访问此页面的2级表,我们将所有索引块向右移动两个块,并将递归索引设置为两个源块的位置:4级和3级:



要访问3级表,我们进行了相同的操作,我们只需要向右移动三个地址块即可。



最后,要访问4级表,请将所有内容向右移动四个块。



现在,您可以计算所有四个级别的页表的虚拟地址。 我们甚至可以通过将其索引乘以8(即页表项的大小)来计算精确指向特定页表项的地址。

下表显示了用于访问各种类型帧的地址的结构:

的虚拟地址地址结构( 八进制
页数0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE
进入1级表0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD
在2级表中输入0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC
在3级表中输入0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB
进入第4级表格0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA

在这里, 是4级索引, 是3级, 是2级, DDD是所显示帧的1级索引, EEEE是其偏移量。 RRR是递归记录的索引。 索引(三位数)通过乘以8(页表项的大小)而转换为偏移量(四位数)。 使用此偏移量,结果地址直接指向相应的页表条目。

SSSS是带符号数字的扩展位,也就是说,它们都是第47位的副本。这是对x86_64体系结构中有效地址的特殊要求,我们在上一篇文章中已对此进行了讨论。

地址是八进制的 ,因为每个八进制字符代表三位,这使您可以清楚地区分不同级别的表的9位索引。 在每个字符代表四个位的十六进制系统中,这是不可能的。

实作


经过所有这些理论,我们终于可以继续执行了。 方便的是,加载器不仅生成页面表,还生成了4级表的最后一条记录中的递归显示,之所以这样做,是因为否则会出现鸡或蛋的问题:我们需要访问4级表以创建递归映射但是我们无法在没有任何显示的情况下访问它。

在上一篇文章的结尾,我们已经使用了此递归映射来通过硬编码地址0xffff_ffff_ffff_f000访问4级表。 如果将这个地址转换为八进制并与上表进行比较,我们将看到它与4级表中的​​记录结构完全对应,其中RRR = 0o777AAAA = 0和符号1的扩展位:

 结构:0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA
地址:0o_177777_777_777_777_777_0000 

多亏了递归表的知识,我们现在可以创建虚拟地址来访问所有活动表。 并具有播放功能。

地址翻译


第一步,创建一个函数,该函数将虚拟地址转换为物理地址,并通过页表的层次结构进行传递:

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

首先,我们为递归索引(511 = 0o777 )和符号扩展位(每个为1)引入变量。 然后,我们通过按位运算来计算页表的索引和偏移量,如图所示:



下一步是计算四个页表的虚拟地址,如上一节所述。 接下来,在函数中,我们将每个地址转换为PageTable链接。 这些是不安全的操作,因为编译器无法知道这些地址是否有效。

计算完地址后,我们使用索引运算符查看4级表中的​​记录,如果该记录为零,则该4级记录没有3级表,这意味着addr没有映射到任何物理内存。 因此,我们返回None 。 否则,我们知道存在3级表。 然后,像上一级一样,重复该过程。

在检查了更高级别的三页之后,我们最终可以读取级别1表的记录,该记录告诉我们地址所映射的物理帧。 最后一步,添加页面偏移量-并返回地址。

如果我们确定知道地址已映射,则可以直接访问1级表,而无需查看更高级别的页面。 但是由于我们不知道这一点,因此我们首先需要检查是否存在1级表,否则我们的函数将为不匹配的地址返回缺少页面的错误。

试一下


让我们尝试在_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(); } 


启动后,我们看到以下结果:



如预期的那样,与标识符关联的地址0xb8000转换为相同的物理地址。 代码页和堆栈页被转换为任意的物理地址,这取决于加载程序如何为内核创建初始映射。

RecursivePageTable


x86_64提供了RecursivePageTable类型,该类型为各种页面表操作实现安全抽象。 使用这种类型,您可以更简洁地实现translate_addr函数:

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

RecursivePageTable类型完全封装了不安全的页表爬网,因此不再需要translate_addr函数中的unsafe代码。 由于需要保证传递的level_4_table_addr的正确性,因此init函数仍然不安全。

必须更新我们的_start函数以重新签名该函数,如下所示:

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

现在,我们将引用传递给RecursivePageTable类型,而不是将LEVEL_4_TABLE_ADDR传递给translate_addr并通过不安全的原始指针访问页表。 因此,我们现在有了一个安全的抽象和明确的所有权语义。 这确保了我们不会在共享访问中意外更改页表,因为要更改它,我们需要排他地拥有RecursivePageTable

此功能提供的结果与手动编写的原始翻译功能相同。

使不安全的功能更安全


memory::init是一个不安全的函数:它需要一个块来调用它unsafe,因为调用者必须保证满足某些要求。在我们的情况下,要求将发送的地址精确地映射到第4级页表的物理帧,将不安全功能

unsafe整个主体放置在该块中,以便执行各种操作而无需创建其他块unsafe。因此,我们不需要不安全的块来取消引用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() } 

问题在于我们无法立即看到哪些零件不安全。例如,如果不查看函数 定义,RecursivePageTable::new就无法说出它是否安全。因此,很容易意外跳过一些不安全的代码。

为避免此问题,可以添加安全的内置函数:

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

现在,unsafe再次需要使用该块进行取消引用level_4_table_ptr,并且我们立即看到这些是唯一不安全的操作。Rust目前开放一个RFC,以更改这种不安全功能的不成功属性。

创建一个新的映射


当我们读取页表并创建转换函数时,下一步是在页表层次结构中创建新的映射。

此操作的复杂性取决于我们要显示的虚拟页面。在最简单的情况下,此页面已经存在一个1级页面的表格,我们只需要输入一个即可。在最困难的情况下,该页面位于尚不存在3级的内存区域中,因此首先您需要创建3级,2级和1级的新表。

让我们从一个不需要创建新表的简单案例开始。加载程序被加载到虚拟地址空间的第一个兆字节中,因此我们知道该区域有一个有效的1级表,例如,我们可以选择该存储区中任何未使用的页面,例如address处的页面0x1000。我们0xb8000将VGA文本缓冲区的帧用作所需的帧。检查我们的地址翻译的工作原理非常简单。

我们create_maping在模块的新功能实现它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(); } 

该函数接受对RecursivePageTable(和将对其进行更改)和的可变引用,FrameAllocator下面对此进行了说明。然后,它应用map_to托盘中的功能Mapper将页面映射到该地址0x1000,并将物理框架映射到该地址0xb8000。该函数不安全,因为使用无效参数可能会破坏内存安全性。

除了参数pageframe功能map_to有两个更多的参数。第三个参数是页表的标志集。我们PRESENT为所有有效条目设置了必要的标志WRITABLE为可写性设置了标志

第四个参数应该是实现该特征的某种结构FrameAllocator。该方法需要此参数。map_to因为创建新的页表可能需要未使用的框架。实现需要的参数特征Size4KiB,如类型PagePhysFrame普遍的特点PageSize,标准的4个KiB页面和具有巨大页2 MIB / 1吉布工作。

该函数map_to可能会失败,因此返回Result。由于这只是一个不可靠的代码示例,因此expect在发生错误时,我们只需使用panic 即可。如果成功,函数将返回一种类型MapperFlush该类型提供了一种从关联翻译缓冲区(TLB)方法中清除最近匹配的页面的简便方法flush。喜欢Result,该类型会使用该属性#[must_use],如果我们意外忘记应用该属性,则会发出警告。

由于我们知道该地址0x1000不需要新的页表,因此它FrameAllocator可以始终返回None要测试功能,请创建以下代码EmptyFrameAllocator

 // in src/memory.rs /// A FrameAllocator that always returns `None`. pub struct EmptyFrameAllocator; impl FrameAllocator<Size4KiB> for EmptyFrameAllocator { fn allocate_frame(&mut self) -> Option<PhysFrame> { None } } 

(如果出现错误“方法allocate_frame不是特征的成员FrameAllocator”,则需要升级x86_64到版本0.4.0。)

现在我们可以测试新的翻译功能:

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

首先,我们在地址处为页面创建一个映射0x1000,并create_example_mapping使用指向实例的可变链接来调用该函数RecursivePageTable0x1000会将页面转换为VGA文本缓冲区,因此我们将在屏幕上看到一些结果。

然后,我们在此页面中写入一个值,该值0xf021f077f065f04e对应于“ New!”在白色背景上。只是不需要将此值立即写到页面顶部0x1000,因为顶行将从屏幕上移出println,并以0x900大约位于屏幕中间的偏移量写入它“ VGA文本模式”一文中我们知道,写入VGA缓冲区应该是易失的,因此我们使用该方法write_volatile

当我们在QEMU中运行它时,我们看到以下内容:



屏幕上的题字。

该代码有效,因为已经有一个1级表来显示该页面0x1000如果我们尝试转换尚不存在此类表的页面,该函数map_to将返回错误,因为它将尝试从中选择框架以创建新的页面表EmptyFrameAllocator如果我们尝试翻译页面0xdeadbeaf000而不是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)}; […] } 

启动时,会从以下错误消息开始恐慌:

 惊慌于'map_to失败:FrameAllocationFailed',/.../result.rs:999haps 

要显示尚无页面级别1表的页面,您需要创建正确的表FrameAllocator但是,您如何知道哪些帧是空闲的以及多少物理内存可用?

开机资讯


不同的计算机具有不同的物理内存量,并且VGA等设备保留的不同区域也不同。只有BIOS或UEFI固件才能确切知道可以使用哪些存储区以及保留哪些存储区。两种固件标准都提供了获取内存分配卡的功能,但只能在下载开始时调用它们。因此,我们的引导加载程序已经从BIOS请求了此(和其他)信息。

为了将信息传递给OS的内核,加载器在调用函数时作为参数_start提供了指向启动信息结构的链接。将此参数添加到我们的函数中:

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

该结构BootInfo仍在最终确定中,因此当升级到与semver不兼容的将来版本的bootloader时崩溃时,不要感到惊讶他目前拥有三个领域p4_table_addrmemory_map并且package

  • 该字段p4_table_addr包含第4级页面的表的递归虚拟地址,因此,不必硬注册该地址0o_177777_777_777_777_777_0000
  • 该字段memory_map最受关注,因为它包含所有内存区域及其类型(未使用,保留或其他)的列表。
  • 该字段package是用于将其他数据与加载程序相关联的当前函数。实现尚未完成,因此我们暂时可以忽略它。

在使用该字段memory_map创建正确的字段之前FrameAllocator,我们要保证参数的类型正确boot_info

巨集 entry_point


由于_start是从外部调用的,因此不检查功能的签名。这意味着任意参数都不会导致编译错误,但可能导致崩溃或未定义的运行时行为。

为了验证签名,bootloader用于将Rust函数定义为入口点的板条箱使用entry_point具有经过验证的类型的宏我们为此宏重写函数:

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

对于入口点,您不再需要使用extern "C"no_mangle,因为宏会设置真正的低层入口点_start该函数kernel_main现在已成为完全正常的Rust函数,因此我们可以为其选择任意名称。重要的是它已经被键入,因此,如果您更改函数的签名(例如,通过添加参数或更改其类型),则会发生编译错误。

请注意,现在我们正在发送到一个memory::init硬编码的地址,但是boot_info.p4_table_addr因此,即使将来的引导加载程序版本在页面级别4的表表中选择另一个条目进行递归显示,代码也将起作用。

选框


现在,由于有了BIOS中的信息,我们才可以访问内存分配卡,以便您可以制作普通的帧分配器。让我们从通用骨架开始:

 // in src/memory.rs pub struct BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { frames: I, } impl<I> FrameAllocator<Size4KiB> for BootInfoFrameAllocator<I> where I: Iterator<Item = PhysFrame> { fn allocate_frame(&mut self) -> Option<PhysFrame> { self.frames.next() } } 

该字段frames由任意迭代器初始化这使您可以简单地将调用委派allocIterator :: next方法

初始化BootInfoFrameAllocator发生在一个新函数中init_frame_allocator

 // in src/memory.rs use bootloader::bootinfo::{MemoryMap, MemoryRegionType}; /// Create a FrameAllocator from the passed memory map pub fn init_frame_allocator( memory_map: &'static MemoryMap, ) -> BootInfoFrameAllocator<impl Iterator<Item = PhysFrame>> { // get usable regions from memory map let regions = memory_map .iter() .filter(|r| r.region_type == MemoryRegionType::Usable); // map each region to its address range let addr_ranges = regions.map(|r| r.range.start_addr()..r.range.end_addr()); // transform to an iterator of frame start addresses let frame_addresses = addr_ranges.flat_map(|r| r.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 } } 

此函数使用组合器将原始内存分配图转换为使用的物理帧的迭代器:

  • iter MemoryRegion . filter , . , , (, ) , InUse . , , - .
  • map range Rust .
  • 第三步是最困难的:使用方法将每个范围转换为一个迭代器into_iter,然后使用选择每个第4096个地址step_by由于页面大小为4096字节(4 KiB),因此我们获得了每个帧开始的地址。加载程序页面会对齐所有已用的内存区域,因此我们不需要对齐或舍入代码。更换mapflat_map,我们得到Iterator<Item = u64>代替Iterator<Item = Iterator<Item = u64>>
  • 在最后阶段,我们将起始地址转换为类型PhysFrame,以构建所需的地址Iterator<Item = PhysFrame>然后使用此迭代器创建并返回一个新的迭代器BootInfoFrameAllocator

现在,我们可以改变我们的功能kernel_main广播它的实例BootInfoFrameAllocator,而不是EmptyFrameAllocator

 // in src/main.rs #[cfg(not(test))] fn kernel_main(boot_info: &'static BootInfo) -> ! { […] // 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(); } 

现在地址翻译成功了,我们再次在屏幕上看到黑白消息“ New!”。在幕后,该方法map_to创建丢失的页表,如下所示:

  • 从中提取未使用的帧frame_allocator
  • 将顶级表条目与此框架匹配。现在可以通过递归页表访问该框架。
  • 将框架归零以创建一个新的空页表。
  • 转到下一级表。

尽管我们的功能create_maping只是一个示例,但是我们现在可以为任意页面创建新的映射。在以后的文章中分配内存和实现多线程时,这非常有用。

总结


在本文中,您学习了如何使用4级递归表将所有帧转换为可计算的虚拟地址。我们使用此方法来实现地址转换功能并在页表中创建新的映射。

我们看到,创建新映射需要新表的未使用框架。可以根据引导加载程序传递给我们内核的BIOS中的信息来实现这种帧分配器。

接下来是什么


在下一篇文章中,我们将为内核创建一个堆内存区域,这将使我们能够分配内存并使用不同类型的集合

Source: https://habr.com/ru/post/zh-CN439066/


All Articles