实验三 虚实地址转换

实验题

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寄存器中,然后将t0t1寄存器进行或运算存入到satp寄存器中,并且更新快表。这样我们就可以通过satp寄存器的值来找到页表基址。

随后我们计算栈起始地址并将其写入sp寄存器中,并且计算出rust_main函数的虚拟地址并写入临时寄存器中。此时我们已经有了rust_main的虚拟地址了,我们可以从虚拟地址(即高地址)首先取其高九位(即VPN3)判断其寻找VPN2的地址(此时应为510),然后再进行不断的映射最后经过satp寄存器找到rust_main的物理地址进行运行。

  1. 分析:为什么 Mapping 中的 page_tablesmapped_pairs 都保存了一些 FrameTracker?二者有何不同?

pagr_tables存放的是所有页表所使用的页面,而mapped_pairs则存放着进程所需要的页面。

  1. 分析:假设某进程需要虚拟地址 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
/// 加入一段映射,可能会相应地分配物理页面
///
/// 未被分配物理页面的虚拟页号暂时不会写入页表当中,它们会在发生 PageFault 后再建立页表项。
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];
// 如果提供了数据,则使用这些数据来填充 page_data
if let Some(init_data) = init_data {
if !init_data.is_empty() {
// 这里必须进行一些调整,因为传入的数据可能并非按照整页对齐

// 拷贝时必须考虑区间与整页不对齐的情况
// start(仅第一页时非零)
// | stop(仅最后一页时非零)
// 0 |---data---| 4096
// |------------page------------|
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。