经由过程Rust编写操作系统之内存的分页与治理引见(下) | 申博官网
登录
  • 欢迎进入申博官网!
  • 如果您觉得申博官网对你有帮助,那么赶紧使用Ctrl+D 收藏申博官网并分享出去吧
  • 这里是申博官方网!
  • 申博官网是菲律宾sunbet官网品牌平台!
  • 申博开户专业品牌平台!

经由过程Rust编写操作系统之内存的分页与治理引见(下)

申博_安全防护 申博 117次浏览 已收录 0个评论

在上一篇文章,我们已对分段和分页的优缺点举行了引见,并终究决议用分页手艺对编写操纵体系。本文,我会接着引见分页的细致运用历程和个中所碰到的题目。

在x86_64上举行分页

x86_64体系构造运用4级页表,页面大小为4KiB。每一个页表独立于级别,具有512个条目标牢固大小。每一个条目标大小为8个字节,因而每一个表格为4KiB(512 * 8B = 4KiB)大,因而恰好合适放在一个页面中。

差别级的页表索引会直接从假造地点中派生出来:

经由过程Rust编写操作系统之内存的分页与治理引见(下)

我们看到每一个表索引由9位构成,这是有意义的,因为每一个表有2^9 = 512个条目。最低的12位是4KiB页面中的偏移量(2^12字节= 4KiB)。位48到64被删除,这意味着x86_64现实上不是64位的,因为它只支撑48位地点。如今开发者设计经由过程5级页表将地点大小扩大到57位,但却还没有支撑此功用的处置惩罚器。

纵然删除了位48到64,也不能将它们设置为恣意值。相反,这个范围内的一切位都必需是位47的副本,以便坚持地点的唯一性,并许可未来扩大,如5级页表。这叫做标记扩大,因为它和补码中的标记扩大很类似。当地点没有准确地举行署名扩大时,CPU会抛出异常响应信息。

细致示例申明

让我们经由过程一个示例来细致相识转换历程是怎样举行的:

经由过程Rust编写操作系统之内存的分页与治理引见(下)

当前运动的4级页表的物理地点(第4级页表的根)存储在CR3寄存器中,然后,每一个页表条目都指向下一级表的物理帧。末了,1级表的条目指向映照的帧。请注重,页表中的一切地点都是物理地点,而不是假造地点,不然CPU也须要转换这些地点,这可以致使无休止的递归。

上面的页表条理构造映照了两个页面(用蓝色示意),从页表索引我们可以推断出这两个页面的假造地点是0x803FE7F000和0x803FE00000。让我们看看当顺序试图从地点0x803FE7F5CE读取数据时会发作什么。起首,我们将地点转换为二进制,并肯定该地点的页表索引和页偏移量:

经由过程Rust编写操作系统之内存的分页与治理引见(下)

有了这些索引,我们如今可以遍历页表条理构造来肯定地点的映照帧:

1. 我们起首从CR3寄存器中读取4级表的地点。

2. 4级索引是1,所以我们检察表中索引为1的条目,它通知我们3级表存储在地点16KiB。

3. 我们从该地点加载level 3表,并检察索引为0的条目,它指向位于24KiB的2级表。

4. 2级索引是511,因而我们检察该页的末了一项来找出1级表的地点。

5. 经由过程1级表中索引为127的条目,我们终究发明该页被映照到帧12KiB,或许十六进制中的0xc000。

6. 末了一步是将页面偏移量增加到帧地点,以取得物理地点0xc000 + 0x5ce = 0xc5ce。

经由过程Rust编写操作系统之内存的分页与治理引见(下)

1级表中的页面权限为r,示意只读。假如我们试图写入该页面,硬件会强制实行这些权限,从而激发异常。因为更高级别页面中的权限限定了较初级别的可以权限,因而假如我们将3级条目设置为只读,纵然初级页指定了读/写权限,那运用此条目标页面照样不具有写入权限。

须要注重的是,只管本示例仅运用每一个表的一个实例,但在每一个地点空间中平常有多个级别的实例。比方:

1. 一个4级表;

2. 512个3级表(因为4级表有512个条目);

3. 512 * 512 个2级表(因为512个3级表中的每一个表都有512个条目);

4. 512 * 512 * 512 个1级表(每一个2级表有512个条目)。

页表的花样

x86_64架构上的页表基本上是由512个条目构成的数组,在Rust语法中,数组情势以下所示:

#[repr(align(4096))]
pub struct PageTable {
    entries: [PageTableEntry; 512],
}

如repr属性所示,页表须要页面对齐,即在4KiB边境上对齐。这一请求可确保页表老是填满完整的页面,并许可优化,使条目异常紧凑。

每一个条目为8字节(64位)大,花样以下:

经由过程Rust编写操作系统之内存的分页与治理引见(下)

我们看到只要位12-51被用来存储物理帧地点,其他的位被用作标志或许可以被操纵体系自在运用。这是可以的,因为我们老是指向一个4096字节对齐的地点,或许指向一个页面对齐的页表,或许指向一个映照帧的开首。这意味着位0-11一直为0,所以没有来由存储这些位,因为硬件可以在运用地点之前将它们设置为0。关于位52-63也是云云,因为x86_64体系构造只支撑52位物理地点(类似于它只支撑48位假造地点)。

让我们细致引见一下个中可用的标志:

1. present标志将映照的页面与未映照的页面辨别开来,当主内存被耗尽时,可以运用它,临时将页面存储到磁盘。当页面随后被接见时,会发作一个称为页面毛病的特别异常。此时,操纵体系可以经由过程从磁盘从新加载缺失的页面,然后继承实行顺序来对此作出回响反映。

2. writable 和 no execute标志离别掌握页面内容是可写的照样包括可实行指令。

3. 当对页面举行读取或写入操纵时,CPU会自动设置accessed标志和dirty标志,操纵体系可以应用这些信息,比方,决议换出 (swap out)哪些页面,或许自上次保存到磁盘以来是不是修正了页面内容。

iPhone蓝牙流量泄露手机号码等重要信息

如果你的苹果设备蓝牙是开启的状态,那么附近的所有人都可以获取你的设备状态,观看设备名、WiFi状态、操作系统版本、电池信息,甚至可以获取你的手机号。 简介 Apple设备以互联生态而著称:即在一个设备上使用一个APP后可以再另一个设备上可以继续使用该APP。同时可以再离线的情况下访问你的文件。这好像与苹果的“What happens on your iPhone, stays on your iPhone”策略是相悖的,但是吗? 首先看一下苹果的隐私策略是如何工作的。 Wireless. Wireless everywhere. 如果你想与朋友分享照

4. write through caching 和 disable cache标志许可离别掌握每一个页面的高速缓存状况。

5. user accessible标志使页面可用于用户空间代码,不然只要在CPU处于内核形式时才可接见该页面。这个特征可以在用户空间顺序运转时坚持内核映照,从而加速体系挪用的速率,然则Spectre破绽可以许可用户空间顺序读取这些页面。

6. global标志会向硬件发出信号,表明某个页面在一切地点空间中都可用,因而不须要从地点空间切换的转换缓存中删除。此标志平常与已消灭的user accessible标志一同运用,以将内核代码映照到一切地点空间。

7. 经由过程让2级或第3级页表的条目直接指向映照的帧,huge page标志许可建立更大的页面,设置此位后,关于2级条目,页面大小增添到了2MiB (512 * 4KiB=2MiB),关于3级条目,页面大小以至增添乐1GiB (512 * 2MiB =1GiB),运用huge page标志的长处是须要更少的转换缓存行和更少的页表。

因为x86_64Crate已供应了页表及其条目标范例,因而我们不须要本身建立这些构造。

TLB (Translation Lookaside Buffer)

本文,我们将ranslation Lookaside Buffer译为页表缓冲。它内里寄存的是一些页表文件(假造地点到物理地点的转换表)。当处置惩罚器要在主内存寻址时,不是直接在内存的物理地点里查找的,而是经由过程一组假造地点转换到主内存的物理地点,TLB就是担任将假造内存地点翻译成现实的物理内存地点,而CPU寻址时会优先在TLB中举行寻址。

一个4级的页表就会使得假造地点的转换异常慢,因为每一个转换须要4个内存接见。为了进步机能,x86_64体系构造会在TLB中缓存末了几个转换,这就许可缓存转换举行时直接将其跳过即可。

与其他CPU缓存差别,TLB不是完整通明的,当页表的内容发作更改时,TLB不会更新或删除转换,这意味着内核必需在修正页表时手动更新TLB。为此,有一条名为invlpg(“invalidate page”)的特别CPU指令,它可以从TLB中删除指定页面的转换,以便鄙人一次接见时从页表中再次加载它。还可以经由过程从新加载CR3寄存器(它模仿地点空间切换)来完整革新TLB,x86_64Crate为tlb模块中的两个变体供应了Rust函数。

牢记,在每次修正页表时革新TLB是很主要的,不然CPU可以会继承运用旧的转换,这可以致使异常难以调试的非肯定性毛病。

毛病的涌现

停止如今,有一件事我们还没有提到:我们的内核已在分页时运转。我们在”A minimal Rust Kernel” 文章中增加的指导加载顺序已建立了一个4级分页条理构造,它将内核的每一个页面映照到一个对应的物理帧中,指导加载顺序如许做是因为在64位形式下x86_64必需分页。

这意味着我们在内核中运用的每一个内存地点都是一个假造地点,接见地点0xb8000处的VGA缓冲区之所以有用,是因为指导加载顺序标识映照了内存页面,这意味着它将假造页面0xb8000映照到了物理帧0xb8000。

分页使我们的内核已变得相对平安,因为每次超越限定的内存接见都邑致使页面毛病异常,而不是写入随机物理内存。指导加载顺序以至为每一个页面设置了准确的接见权限,这意味着只要包括代码的页面是可实行的,只要数据页面是可写的。

页面毛病

如今,就让我们尝试经由过程接见内核外部的一些内存来触发页面毛病。起首,我们建立一个页面毛病处置惩罚顺序,并将其注册到我们的IDT中,如许我们就可以看到一个页面毛病异常,而不是平常的“double fault” 。

简朴地说,double fault是当CPU没法挪用异常处置惩罚顺序时发作的特别异常。比方,当一个页面毛病被触发,然则中缀描述符表(IDT)中没有注册页面毛病处置惩罚顺序时,就会发作这类状况。所以它有点类似于编程语言中的catch-all块,但有破例,比方c++中的catch(…)或Java或c#中的catch(Exception e)。

double fault的行动类似于一般异常(normal exception),它的向量为8,我们可以在IDT中为它定义一个一般的处置惩罚函数。供应两重毛病处置惩罚顺序异常主要,因为假如没有处置惩罚double fault,就会发作致命的三重毛病。三重毛病没法捕捉,大多数硬件会对体系重置做出回响反映。

// in src/interrupts.rs
lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        […]
        idt.page_fault.set_handler_fn(page_fault_handler); // new
        idt
    };
}
use x86_64::structures::idt::PageFaultErrorCode;
extern "x86-interrupt" fn page_fault_handler(
    stack_frame: &mut InterruptStackFrame,
    _error_code: PageFaultErrorCode,
) {
    use x86_64::registers::control::Cr2;
    println!("EXCEPTION: PAGE FAULT");
    println!("Accessed Address: {:?}", Cr2::read());
    println!("{:#?}", stack_frame);
    hlt_loop();
}

CR2寄存器由CPU在页面毛病上自动设置,并包括致使页面毛病的已接见假造地点。我们运用x86_64Crate的Cr2::read函数来读取和打印它。平常,PageFaultErrorCode范例会供应关于致使页面毛病的内存接见范例的更多信息,但如今存在一个通报无效毛病代码的LLVM破绽,因而我们临时疏忽它。在没有解决页面毛病的状况下,我们没法继承实行,因而只能输入hlt_loop看成末端。

如今我们可以尝试接见内核以外的一些内存:

// in src/main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello World{}", "!");
    blog_os::init();
    // new
    let ptr = 0xdeadbeaf as *mut u32;
    unsafe { *ptr = 42; }
    // as before
    #[cfg(test)]
    test_main();
    println!("It did not crash!");
    blog_os::hlt_loop();
}

在运转时,我们可以看到页面毛病处置惩罚顺序被挪用:

经由过程Rust编写操作系统之内存的分页与治理引见(下)

可以看出,CR2寄存器确切包括0xdeadbeaf,这是我们试图接见的地点。

从上图可以看到,当前的指令指针是0x20430a,所以我们晓得这个地点指向一个代码页。代码页会被指导加载顺序以只读体式格局映照,因而从该地点读取有用但写入会致使页面毛病。你可以经由过程将0xdeadbeaf指针更改成0x20430a,来尝试此操纵。

// Note: The actual address might be different for you. Use the address that
// your page fault handler reports.
let ptr = 0x20430a as *mut u32;
// read from a code page -> works
unsafe { let x = *ptr; }
// write to a code page -> page fault
unsafe { *ptr = 42; }

看末了一行,我们可以发明读取接见是有用的,然则写入接见会致使页面毛病。

接见页表

让我们来看看我们的内核运转的页表:

// in src/main.rs
#[no_mangle]
pub extern "C" fn _start() -> ! {
    println!("Hello World{}", "!");
    blog_os::init();
    use x86_64::registers::control::Cr3;
    let (level_4_page_table, _) = Cr3::read();
    println!("Level 4 page table at: {:?}", level_4_page_table.start_address());
    […] // test_main(), println(…), and hlt_loop()
}

x86_64的Cr3::read 函数从Cr3寄存器返回当前运动的4级页表,它返回一个包括 PhysFrame 和Cr3Flags元组。不过,我们只对帧感兴趣,因而疏忽了元组的第二个元素。

当我们运转它时,可以看到以下输出:

Level 4 page table at: PhysAddr(0x1000)

因而,当前运动的4级页表存储在物理内存中的地点0x1000处,由PhysAddr封装器范例示意。如今的题目是:我们怎样经由过程内核接见这个表?

当分页处于运动状况时,我们是不可以直接接见物理内存的,因为顺序可以很容易地绕过内存保护并接见其他顺序的内存。因而,接见该表的唯一要领是经由过程一些映照到地点0x1000处的物理帧的假造页面。为页表帧建立映照的题目是一个罕见的题目,因为内核须要按期接见页表,比方在为新线程分派客栈时。

下一篇文章,我们将细致诠释这个题目标解决方案,敬请关注。

总结

本文引见了两种内存保护手艺:分段和分页。分段运用可变大小的内存地区,而且存在外部碎片,而分页运用牢固大小的页面,并许可对接见权限举行更细粒度的掌握。

分页手艺会将页面的映照信息存储在具有一个或多个级别的页表中,x86_64体系构造运用4级页表,页面大小为4KiB。硬件自动遍历页表并将效果转换缓存到TLB中。此缓冲区不是通明的,须要在页表更改时手动革新。

此次我们的内核已在分页之上运转,而且不法内存接见会致使页面毛病异常。我们尝试接见当前运动的页表,但我们没法实行此操纵,因为CR3寄存器存储了我们没法直接从内核接见的物理地点。

下一篇文章我们将诠释怎样在内核中完成分页支撑,该要领供应了从内核接见物理内存的差别要领,这使得接见内核运转的页表成为可以,我们末了的目标是可以完成将假造地点转换为物理地点以及在页表中建立新映照的函数。

原文地点: https://www.4hou.com/web/19188.html


申博|网络安全巴士站声明:该文看法仅代表作者自己,与本平台无关。版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明经由过程Rust编写操作系统之内存的分页与治理引见(下)
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址