深入理解x86_64页表机制——以phil-opp/blog_os为例
blog_os Writing an OS in Rust 项目地址: 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的页。
页表遍历过程
- CPU从CR3寄存器获取P4表的物理地址
- 使用地址中的P4索引找到P3表
- 使用P3索引找到P2表
- 使用P2索引找到P1表
- 使用P1索引找到最终的物理帧
- 结合页内偏移量得到实际物理地址
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表:不使用递归索引
具体实现
- 在引导汇编代码中设置递归映射:
mov eax, p4_table
or eax, 0b11 ; present + writable
mov [p4_table + 511 * 8], eax
- 在Rust中定义P4表的虚拟地址:
pub const P4: *mut Table = 0xffffffff_fffff000 as *mut _;
- 实现下一级表地址计算:
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
}
}
- 提供安全的访问接口:
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 项目地址: https://gitcode.com/gh_mirrors/bl/blog_os
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考