深入理解x86_64页表机制——以phil-opp/blog_os为例

深入理解x86_64页表机制——以phil-opp/blog_os为例

blog_os Writing an OS in Rust blog_os 项目地址: https://gitcode.com/gh_mirrors/bl/blog_os

前言

在现代操作系统中,内存管理是核心功能之一。x86_64架构采用四级页表机制来实现虚拟内存到物理内存的映射。本文将深入探讨这一机制,并展示如何在Rust中实现安全高效的页表操作。

页表基础概念

什么是页表?

页表是一种内存管理方案,它将虚拟内存和物理内存分离管理。地址空间被划分为固定大小的页(通常4KB),页表则记录虚拟页到物理帧的映射关系。

x86_64地址结构

在64位模式下,x86架构使用四级页表结构。一个虚拟地址由以下几部分组成:

47-63位:符号扩展位(必须与第47位相同)
39-47位:P4表索引
30-38位:P3表索引
21-29位:P2表索引
12-20位:P1表索引
0-11位:页内偏移量

每个页表包含512个条目(2^9),每个条目8字节,因此一个页表正好占用一个4KB的页。

页表遍历过程

  1. CPU从CR3寄存器获取P4表的物理地址
  2. 使用地址中的P4索引找到P3表
  3. 使用P3索引找到P2表
  4. 使用P2索引找到P1表
  5. 使用P1索引找到最终的物理帧
  6. 结合页内偏移量得到实际物理地址

Rust实现页表模块

基本数据结构

const ENTRY_COUNT: usize = 512;  // 每个页表的条目数

// 类型别名增强可读性
pub type PhysicalAddress = usize;
pub type VirtualAddress = usize;

// 表示虚拟页的结构体
pub struct Page {
    number: usize,
}

页表条目实现

页表条目是一个64位值,包含物理地址和各种标志位:

bitflags! {
    pub struct EntryFlags: u64 {
        const PRESENT =         1 << 0;  // 页当前在内存中
        const WRITABLE =        1 << 1;  // 可写
        const USER_ACCESSIBLE = 1 << 2;  // 用户态可访问
        const WRITE_THROUGH =   1 << 3;  // 直写缓存
        const NO_CACHE =        1 << 4;  // 禁用缓存
        const ACCESSED =        1 << 5;  // CPU访问标记
        const DIRTY =           1 << 6;  // 脏页标记
        const HUGE_PAGE =       1 << 7;  // 大页标记
        const GLOBAL =          1 << 8;  // 全局页
        const NO_EXECUTE =      1 << 63; // 禁止执行
    }
}

pub struct Entry(u64);

impl Entry {
    // 检查条目是否未使用
    pub fn is_unused(&self) -> bool {
        self.0 == 0
    }
    
    // 设置条目为未使用
    pub fn set_unused(&mut self) {
        self.0 = 0;
    }
    
    // 获取标志位
    pub fn flags(&self) -> EntryFlags {
        EntryFlags::from_bits_truncate(self.0)
    }
    
    // 获取指向的物理帧
    pub fn pointed_frame(&self) -> Option<Frame> {
        if self.flags().contains(PRESENT) {
            Some(Frame::containing_address(
                self.0 as usize & 0x000fffff_fffff000
            ))
        } else {
            None
        }
    }
    
    // 设置条目指向的帧和标志位
    pub fn set(&mut self, frame: Frame, flags: EntryFlags) {
        assert!(frame.start_address() & !0x000fffff_fffff000 == 0);
        self.0 = (frame.start_address() as u64) | flags.bits();
    }
}

页表结构实现

pub struct Table {
    entries: [Entry; ENTRY_COUNT],
}

impl Index<usize> for Table {
    type Output = Entry;

    fn index(&self, index: usize) -> &Entry {
        &self.entries[index]
    }
}

impl IndexMut<usize> for Table {
    fn index_mut(&mut self, index: usize) -> &mut Entry {
        &mut self.entries[index]
    }
}

impl Table {
    // 清空页表
    pub fn zero(&mut self) {
        for entry in self.entries.iter_mut() {
            entry.set_unused();
        }
    }
}

递归映射技术

为什么需要递归映射?

在修改页表时,我们需要访问页表本身。但页表本身也是通过页表映射的,这就产生了"先有鸡还是先有蛋"的问题。递归映射提供了一种优雅的解决方案。

实现原理

将P4表的最后一个条目指向P4表自身。这样:

  • 访问P4表:使用索引511三次
  • 访问P3表:使用索引511两次
  • 访问P2表:使用索引511一次
  • 访问P1表:不使用递归索引

具体实现

  1. 在引导汇编代码中设置递归映射:
mov eax, p4_table
or eax, 0b11 ; present + writable
mov [p4_table + 511 * 8], eax
  1. 在Rust中定义P4表的虚拟地址:
pub const P4: *mut Table = 0xffffffff_fffff000 as *mut _;
  1. 实现下一级表地址计算:
fn next_table_address(&self, index: usize) -> Option<usize> {
    let entry_flags = self[index].flags();
    if entry_flags.contains(PRESENT) && !entry_flags.contains(HUGE_PAGE) {
        let table_address = self as *const _ as usize;
        Some((table_address << 9) | (index << 12))
    } else {
        None
    }
}
  1. 提供安全的访问接口:
pub fn next_table(&self, index: usize) -> Option<&Table> {
    self.next_table_address(index)
        .map(|address| unsafe { &*(address as *const _) })
}

pub fn next_table_mut(&mut self, index: usize) -> Option<&mut Table> {
    self.next_table_address(index)
        .map(|address| unsafe { &mut *(address as *mut _) })
}

安全性考虑

递归映射引入了一个重要不变量:

活动P4表的第511个条目必须始终指向活动P4表本身

这个不变量必须由页表模块维护,任何页表切换操作都必须确保这一点。

总结

本文详细介绍了x86_64架构的四级页表机制,并展示了如何在Rust中实现安全的页表操作。通过递归映射技术,我们能够方便地访问和修改各级页表。这种实现既保证了安全性,又提供了良好的性能。

在后续开发中,我们可以基于这些基础功能实现更高级的内存管理特性,如页面分配、地址空间隔离等。Rust的所有权系统和生命周期检查为我们提供了额外的安全保障,使得这种底层的系统编程更加可靠。

blog_os Writing an OS in Rust blog_os 项目地址: https://gitcode.com/gh_mirrors/bl/blog_os

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

曹俐莉

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值