实验三 虚实地址转换
实验题
1.原理: 原理:在 os/src/entry.asm
中,boot_page_table
的意义是什么?当跳转执行 rust_main
时,不考虑缓存,硬件通过哪些地址找到了 rust_main
的第一条指令?
更改后的entry.asm
如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| # 操作系统启动时所需的指令以及字段 # # 我们在 linker.ld 中将程序入口设置为了 _start,因此在这里我们将填充这个标签 # 它将会执行一些必要操作,然后跳转至我们用 rust 编写的入口函数 # # 关于 RISC-V 下的汇编语言,可以参考 https://github.com/riscv/riscv-asm-manual/blob/master/riscv-asm.md # %hi 表示取 [12,32) 位,%lo 表示取 [0,12) 位
.section .text.entry .globl _start # 目前 _start 的功能:将预留的栈空间写入 $sp,然后跳转至 rust_main _start: # 通过线性映射关系计算 boot_page_table 的物理页号 lui t0, %hi(boot_page_table) li t1, 0xffffffff00000000 sub t0, t0, t1 srli t0, t0, 12 # 8 << 60 是 satp 中使用 Sv39 模式的记号 li t1, (8 << 60) or t0, t0, t1 # 写入 satp 并更新 TLB csrw satp, t0 sfence.vma
# 加载栈的虚拟地址 lui sp, %hi(boot_stack_top) addi sp, sp, %lo(boot_stack_top) # 跳转至 rust_main # 这里同时伴随 hart 和 dtb_pa 两个指针的传入(是 OpenSBI 帮我们完成的) lui t0, %hi(rust_main) addi t0, t0, %lo(rust_main) jr t0
# 回忆:bss 段是 ELF 文件中只记录长度,而全部初始化为 0 的一段内存空间 # 这里声明字段 .bss.stack 作为操作系统启动时的栈 .section .bss.stack .global boot_stack boot_stack: # 16K 启动栈大小 .space 4096 * 16 .global boot_stack_top boot_stack_top: # 栈结尾
# 初始内核映射所用的页表 .section .data .align 12 .global boot_page_table boot_page_table: # .8byte表示长度为8个字节的整数 .8byte 0 .8byte 0 # 第 2 项:0x8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1 .8byte (0x80000 << 10) | 0xcf .zero 505 * 8 # 第 508 项(外设用):0xffff_ffff_0000_0000 -> 0x0000_0000,0xcf 表示 VRWXAD 均为 1 .8byte (0x00000 << 10) | 0xcf .8byte 0 # 第 510 项:0xffff_ffff_8000_0000 -> 0x8000_0000,0xcf 表示 VRWXAD 均为 1 .8byte (0x80000 << 10) | 0xcf .8byte 0
|
我们一步步讲解entry.asm
是如何进行从物理地址到虚拟地址的转变的。首先声明代码段位置并且实现在linker.ld
中声明的__start
函数,随后我们做的就是把boot_page_table
(即页表基址)的虚拟地址转化成物理地址写入satp
寄存器中,我们来看是如何把boot_page_table
的物理页号写入的:
由于在rCore
中,我们选择了RISC -V本身硬件支持Sv39模式作为页表的实现。
在 Sv39 模式中,定义物理地址有 56 位,而虚拟地址有 64 位。虽然虚拟地址有 64 位,只有低 39 位有效。不过这不是说高 25 位可以随意取值,规定 63-39 位的值必须等于第 38 位的值,否则会认为该虚拟地址不合法,在访问时会产生异常。
Sv39 模式同样是基于页的,在物理内存那一节曾经提到物理页(Frame)与物理页号(PPN,Physical Page Number)。在这里物理页号为 44 位,每个物理页大小为 4KB。同理,我们对于虚拟内存定义虚拟页(Page)以及虚拟页号(VPN, Virtual Page Number) 。在这里虚拟页号为 27 位,每个虚拟页大小也为 4KB。物理地址和虚拟地址的最后 12 位都表示页内偏移,即表示该地址在所在物理页(虚拟页)上的什么位置。
1 2 3 4 5 6 7 8 9 10
| lui t0, %hi(boot_page_table) li t1, 0xffffffff00000000 sub t0, t0, t1 srli t0, t0, 12 # 8 << 60 是 satp 中使用 Sv39 模式的记号 li t1, (8 << 60) or t0, t0, t1 # 写入 satp 并更新 TLB csrw satp, t0 sfence.vma
|
首先,我们取boot_page_table
地址的12位到32位,此为物理地址的页号,然后将物理地址页号与0xffffffff00000000
进行相减存到t0
寄存器中,然后将t0
寄存器右移12位,然后我们将(8 << 60)
作为Sv39模式的记号存入到t1
寄存器中,然后将t0
和t1
寄存器进行或运算存入到satp
寄存器中,并且更新快表。这样我们就可以通过satp
寄存器的值来找到页表基址。
随后我们计算栈起始地址并将其写入sp
寄存器中,并且计算出rust_main
函数的虚拟地址并写入临时寄存器中。此时我们已经有了rust_main
的虚拟地址了,我们可以从虚拟地址(即高地址)首先取其高九位(即VPN3)判断其寻找VPN2的地址(此时应为510),然后再进行不断的映射最后经过satp
寄存器找到rust_main
的物理地址进行运行。
- 分析:为什么
Mapping
中的 page_tables
和 mapped_pairs
都保存了一些 FrameTracker
?二者有何不同?
pagr_tables
存放的是所有页表所使用的页面,而mapped_pairs
则存放着进程所需要的页面。
- 分析:假设某进程需要虚拟地址 A 到物理地址 B 的映射,这需要操作系统来完成。那么操作系统在建立映射时有没有访问 B?如果有,它是怎么在还没有映射的情况下访问 B 的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
pub fn map(&mut self, segment: &Segment, init_data: Option<&[u8]>) -> MemoryResult<()> { match segment.map_type { MapType::Linear => { for vpn in segment.page_range().iter() { self.map_one(vpn, Some(vpn.into()), segment.flags)?; } if let Some(data) = init_data { unsafe { (&mut *slice_from_raw_parts_mut(segment.range.start.deref(), data.len())) .copy_from_slice(data); } } } MapType::Framed => { for vpn in segment.page_range().iter() { let mut page_data = [0u8; PAGE_SIZE]; if let Some(init_data) = init_data { if !init_data.is_empty() {
let page_address = VirtualAddress::from(vpn); let start = if segment.range.start > page_address { segment.range.start - page_address } else { 0 }; let stop = min(PAGE_SIZE, segment.range.end - page_address); let dst_slice = &mut page_data[start..stop]; let src_slice = &init_data[(page_address + start - segment.range.start) ..(page_address + stop - segment.range.start)]; dst_slice.copy_from_slice(src_slice); } };
let mut frame = FRAME_ALLOCATOR.lock().alloc()?; self.map_one(vpn, Some(frame.page_number()), segment.flags)?; (*frame).copy_from_slice(&page_data); self.mapped_pairs.push_back((vpn, frame)); } } } Ok(()) }
|
以上是虚拟地址映射到物理地址的代码。
操作系统在建立映射时不一定访问B,但可能在访问B的同时向页面加载一些数据。尽管此时映射并不存在,但依然可以通过线性偏移量访问到B。