|
|
| sv39分页
|
|
| 内存控制相关CSR寄存器
|
| 默认MMU不使能
|
| 访问地址直接是物理地址
|
| 修改satp CSR启用分页模式
|
| RV64 架构下 satp 的字段分布。
|
|
MODE
| 0
|
所有访存都被视为物理地址
|
|
| 8
|
SV39 分页
|
| 所有 S/U 特权级的访存被视为一个 39 位的虚拟地址
|
| MMU 会将其转换成 56 位的物理地址;
|
| 如果转换失败,则会触发异常。
|
|
|
|
|
| address格式组成
|
|
|
| 每个virtual page和phy page都按 4 KB 对齐
|
|
|
|
| page offset
|
| 因此virtual address和phy address分成两部分
|
| 低12 bit都是page offset
|
| 用来定位address位于一个4kb page里的哪个位置
|
|
|
| PFN
|
| phy frame number,将整个物理地址分成page大小后对应的每个page的index
|
| VPN最后会映射到一个PFN
|
|
| VPN
|
virtual address的高27 bit[38:12]是virtual page number
|
| PPN
|
phy address的高44 bit[55:12]是phy page number
|
|
|
page number用来定位virtual/phy address属于哪一个virtual/phy page frame
|
|
|
转换
| page为单位
|
| 前后page offset不变
|
| MMU从virtual address取出27bit的VPN
|
| page table中查找到对应的PPN
|
| 找到,就将PPN与page offset合并,获得44+12=56 bit的物理地址
|
|
| RV64架构的sv39,只有低39bit有真正意义
|
| 要求64bit的virtual address的[63:39]这25bit
|
| 与第38bit相同
|
| 否则MMU认为是不合法地址
|
| 只有2^39的地址,最低256g(38bit为0)和最高的256g(38bit为1)可以通过MMU检查
|
|
|
|
| address和page number
|
|
|
PAGE_SIZE = 4096
|
|
|
PAGE_SIZE_BITS = 12
|
| page_offset
|
| page_offset(&self) -> usize { self.0 & (PAGE_SIZE - 1) }
|
相当于取低12bit内容
|
|
| 从PPN到phy addr
|
| fn from(v: PhysPageNum) -> Self { Self(v.0 << PAGE_SIZE_BITS) }
|
左移12bit
|
|
| phy addr需要保证与page大小4k对齐
|
| 才能通过右移转换为PPN
|
|
| fn from(v: PhysAddr) -> Self {
|
|
| assert_eq!(v.page_offset(), 0);
|
|
| v.floor()
|
|
| }
|
|
|
|
|
| pub fn floor(&self) -> PhysPageNum { PhysPageNum(self.0 / PAGE_SIZE) }
|
向下取整floor
|
| pub fn ceil(&self) -> PhysPageNum { PhysPageNum((self.0 + PAGE_SIZE - 1) / PAGE_SIZE) }
|
向上取整ceil
|
|
|
| page table entry,PTE
|
|
| [53:10] 44bit是PPN
|
[7:0] 8bit是标志位
| V(Valid) 位为 1 时,页表项才是合法的;
|
| R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;
|
| U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
|
| G 我们不理会;
|
| A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
|
| D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。
|
| 代码里用bitflags,用u8封装成一个标志位集合类型
|
|
| 通过PPN和一个PTEFlags生成PTE
|
| PageTableEntry {
|
| bits: ppn.0 << 10 | flags.bits as usize,
|
| }
|
|
| PTE拿PPN
|
(self.bits >> 10 & ((1usize << 44) - 1)).into()
| 1usize << 44) - 1
|
只取44bits的PPN
|
|
| PTE拿flags
|
PTEFlags::from_bits(self.bits as u8).unwrap()
|
|
|
| PFN的分配与回收
|
| 通过多级VPN最后索引到的就是PFN
|
| 其中每一级VPN会索引到一个PPN,这个PPN指向了下一级的page table
|
| 直到最后一个page table的pte的ppn是最终需要的pfn(pfn和ppn道理上都是物理页号所以pfn直接用ppn结构体表示)
|
|
|
| 需要知道哪一部分物理内存可用
|
| os/src/linker.ld
|
ekernel
|
| 指明内核数据终止phy address
|
| 之后的物理内存都可用
|
|
|
| os/src/config
|
| MEMORY_END: usize = 0x80800000;
|
| 硬编码整块物理内存终止物理地址0x80800000
|
| 物理起始地址0x80000000
|
| 可用内存大小8M
|
|
|
| 因而内存PFN左开右闭从ekernel上取整-MEMORY_END下取整
|
|
| FramAllocator
|
描述PFN管理器需要提供的功能
|
|
|
|
| 最简单栈式PFN管理
|
| PFN[current,end),表示从未分配的区间
|
| recycled后入先出方式保存被回收的PFN
|
|
|
|
初始化设置current和end
|
|
|
alloc
| 从recycled pop
|
有直接返回
|
|
|
不然检查current == end没内存了回None
|
|
|
不然current +=1,返回current -1
|
|
| PFN跟PPN的表示方式一样
|
| 所以这里使用的是PPN
|
|
dealloc
| 如果ppn >= current 或者recycled里找到了ppn
|
| 报错,分配过了
|
| 不然recycled push这个ppn
|
|
| 包装在全局FRAME_ALLOCATOR
|
| 装成PFN
|
|
| 包装的frame_alloc返回FrameTracker
|
| FrameTracker这里将ppn先做了清零
|
| 再保存ppn
|
| 因为ppn可能被分配干过的别的,需要先清零
|
|
|
|
| 实现了drop trait,被drop的时候自动dealloc ppn
|
|
|
|
|
| 多级page table实现
|
| sv39多级页表以节点为单位管理
|
| 每个节点在一个PFN中
|
| 位置可以用PPN来表示
|
|
| 结构
|
| init
|
先alloc一个frame里面的ppn给root_ppn
|
|
|
alloc的frame塞进frames
|
|
|
|
| 恒等映射identical Mapping
|
每个pfn用一个在多级页表中与其ppn相等的vpn映射
|
|
| impl ppn
|
| 由于恒等映射
|
| 裸指针虽然对应virtual addr
|
| 所以映射到相同的phy addr
|
| 所以可以直接使用
|
|
|
pub fn get_pte_array(&self) -> &'static mut [PageTableEntry] {
| let pa: PhysAddr = self.clone().into();
|
| 前边定义了从ppn到phy address,相当于ppn当前page的开始地址
|
| core::slice::from_raw_parts_mut(pa.0 as *mut PageTableEntry, 512)
|
| 返回512个pte
|
一个page 4k / 一个pte 8bytes
|
得到一个page table有512个pte
|
|
|
|
|
get_bytes_array(&self) -> &'static mut [u8] {
| core::slice::from_raw_parts_mut(pa.0 as *mut u8, 4096)
|
| pa同上,返回的是一个page的bytes
|
|
|
|
pub fn get_mut(&self) -> &'static mut T {
| (pa.0 as *mut T).as_mut().unwrap()
|
| pa同上,返回当前地址的任意类型
|
|
|
| impl vpn
|
pub fn indexes(&self) -> [usize; 3] {
| for i in (0..3).rev() {
|
|
| idx[i] = vpn & 511;
|
// 取当前低 9 位作为索引(511 = 0b1_1111_1111)
|
| vpn >>= 9;
|
|
| }
|
|
| idx
|
|
|
| 取出vpn的三级页索引
|
| 这个缺了图例,vpn实际上在sv39的时候会有三级索引
|
| 但这个图例是three piece里的,这里的低是12bit不是图例里的8bit
|
| 每个索引是9bit不是这里的7bit
|
|
|
|
| 按照从高到低顺序返回
|
|
| impl PageTable
|
find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
| 拿了root的ppn
|
|
| 返回当前ppn的所有的PageTableEntry
|
get_pte_array
|
| 根据idxs[i]来索引找到对应pte
|
|
| 如果i == 2是最后一级pte里面有vpn映射的真实pfn
|
这里获得最后的pte
|
不然判断如果pte不valid
| frame_alloc新的
|
| 新建一个pte
|
| frames加新
|
|
|
| 更新ppn为当前pte的ppn,继续循环
|
|
|
pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
| pte到ppn的映射
|
|
|
| 先根据vpn
|
find_pte_create(vpn)
|
找个要不创个
|
| !is_valid有问题
|
assert
|
|
| 用新的ppn和flag更新pte
|
|
|
|
| pub fn unmap(&mut self, vpn: VirtPageNum) {
|
| 不经过MMU手动查页表的方法们
|
| 说是这么说也得解析多级vpn,只是不创建新的pte
|
| 相当于只读pagetable
|
|
pub fn from_token(satp: usize) -> Self {
| 临时创建一个手动查页表的PageTable
|
| 只有一个satp token得到的多级页表root节点的ppn
|
| frames为空
|
|
fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
| 与上面的find_pte_create类似
|
| 只是不会分配ppn,遇到is_valid()直接返回None
|
|
pub fn translate(&self, vpn: VirtPageNum) -> Option {
| 调用find_pte,找到了返回一个clone()
|
|
|
|
|
| 地址空间address space
|
| address space
|
| 地址空间抽象的重要意义在于 隔离 (Isolation)
|
|
| 执行每个应用的代码的时候,
|
| 内核需要控制 MMU 使用这个app address space的多级page table进行地址转换
|
|
|
| 应用的address space只能访问自己page table映射到的pfn
|
|
|
| os也需要通过MMU,有自己的address space(MeomrySet)
|
|
|
|
|
| 保存App的KernelStack
|
| 大小KERNEL_STACK_SIZE
|
| rw,可读可写
|
|
| 中间用Guard Page隔开
|
| 这样在空间不足的时候
|
| 访问到空洞
|
| 空洞page table没有映射
|
| 会触发exception
|
| 可以交给trap handler处理错误
|
|
| 但
|
| 由于编译器会对访存顺序和局部变量在栈帧中的位置进行优化
|
| 难以确定一个已经溢出的栈帧中的哪些位置会先被访问
|
| 总的来说,空洞区域被设置的越大,我们就能越早捕获到这一错误并避免它覆盖其他重要数据
|
|
|
|
|
|
| 低256g
|
| 4个逻辑section,恒等映射
|
.text/.rodata/.data/.bss
|
| 无需调整内核内存布局 os/src/linker.ld
|
|
| 仍能和启用页表机制之前那样访问内核的各数据段
|
|
| 加入了访问限制
|
| U都没有设置
|
只能S态访问
|
| .text
|
不能w
|
| .rodata
|
不能wx
|
| .data/.bss
|
可以rw不能x
|
|
|
| os address space需要存在一个恒等映射到os 数据段之外的可用pfn的MapArea,
|
| 这个嗯
|
| TODO
|
| 点了之前也没搞清楚为啥需要
|
| 对应那个ekernel - MEMORY_END的内存
|
|
| 这样才能在启用page table机制之后,内核仍能以纯软件的方式读写这些物理页帧。
|
|
| 它们的标志位仅包含 rw ,意味着该 逻辑段只能在 S 特权级以上访问,并且只能读写。
|
|
|
|
|
|
| 逻辑段
|
| MMU 通过查多级page table 可以正确完成地址转换的连续的virtual address区间
|
| 区间内包含的所有vpn都以一种相同的方式映射到pfn,
|
| 具有可读/可写/可执行等属性
|
|
| 逻辑段MapArea
|
| 描述一段连续地址的virtual address
|
pub struct MapArea {
| vpn_range: VPNRange,
|
vpn的连续区间
|
| data_frames: BTreeMap,
|
| type是Framed
|
| 每个vpn和映射到的pfn的FrameTracker的一个键值对容器
|
| BTreeMap
|
| 这些pfn用来存放实际内存data,而不是多级页表中间节点
|
| 声明周期绑定MapAread,一种RAII
|
|
| map_type: MapType,
|
vpn映射到pfn的方式
| Identical
|
恒等映射
|
| Framed
|
每个vpn需要映射到一个新分配的pfn
|
|
| map_perm: MapPermission,
|
| 访问方式
|
PTEFlags子集
|
|
|
仅有U/R/W/X
|
|
|
|
| }
|
pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
| page_table已知
|
| 如果恒等Identical,ppn就是vpn
|
如果Framed
| frame_alloc
|
新的pfn
|
| ppn更新成frame.ppn
|
|
| data_frames插入
|
vpn映射frame
|
|
| 设置pte_flags
|
| 更新page_table里vpn , ppn映射
|
|
pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
| 如果Framed类型,从data_frames里删除vpn key
|
| page_table unmap vpn
|
|
pub fn map(&mut self, page_table: &mut PageTable) {
| 针对vpn_range里的每个vpn
|
跑一次map_one
|
|
| pub fn unmap(&mut self, page_table: &mut PageTable) {
|
pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
| 需要假设
|
| /// data: start-aligned but maybe with shorter length
|
| /// assume that all frames were cleared before
|
|
|
| vpn_range.get_start()拿到开头的vpn
|
loop
| data截取到start+PAGE_SIZE大小
|
| 通过vpn获得对应的ppn里的所有bytes的,src.len()长度bytes内容
|
| copy_from_slice过去
|
| start+PAGE_SIZE
|
| 如果start >= len 数据拷贝完成,退出
|
| 进入下个vpn
|
|
|
|
| MemorySet
|
| 地址空间,一系列有关联的逻辑段MapArea
|
|
pub struct MemorySet {
| page_table: PageTable,
|
| 多级页表
|
| 挂着所有多级页表的每个节点pte所在的pfn
|
|
| areas: Vec,
|
| MapArea vec
|
| 挂着对应逻辑段中的data所在pfn
|
|
|
| }
|
| pub fn new_bare() -> Self {
|
fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
| map_area
|
map
|
当前的page_table
|
|
|
| 如果有data
|
| 说明里说如果framed映射才插入
|
| 但没有这个判断
|
|
| map_area,copy_data
|
|
| 最后areas push(map_area)
|
|
|
| pub fn insert_framed_area(&mut self, start_va: VirtAddr, end_va: VirtAddr, permission: MapPermission) {
|
| pub fn new_kernel() -> Self;
|
pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize);
| 生成elf的address space
|
| 插入跳板
|
| xmas_elf解析elf数据
|
| 根绝elf魔数判断是否合法elf
|
直接得到program header 数目
| 遍历
|
将合适区域加入address space
|
|
|
确认program header类型LOAD
|
|
|
表明有被os加载的必要
|
|
|
通过ph.virtual_addr() ph.mem_size()计算这一区域在app address space中位置
|
|
|
通过ph.flags()确认访问限制,转换为MapPermission,默认包含U
|
|
|
创建map_area
|
|
|
push到address space
|
|
|
完成data拷贝,data存放位置通过ph.offset() 和ph.file_size()找到
|
|
|
| 注意
|
存在一部分零初始化
|
ph.file_size()小于ph.mem_size()
|
|
|
|
这些零出于缩减可执行文件大小的原因不应该实际出现在 ELF 数据中
|
|
|
| 处理app stack,之前维护的max_end_vpn记录目前涉及最大vpn
|
| 在上面接着放个保护页
|
| app stack
|
| 然后映射次高页面,存放TrapContext
|
| 返回memory_set,user_stack_top,entry_point
|
|
|
|
| os地址空间
|
pub fn new_kernel() -> Self {
| map跳板
|
|
| map各个section, .text, .rodata, .data, .bss
|
|
| 还有一块固定的ekernel-MEMORY_END这个是干啥的?来着?
|
TODO
|
|
|
| app地址空间
|
| 效仿内核地址空间的设计
|
| 借助页表page table机制
|
| 使得应用地址空间的各个MapArea也可以有不同的访问方式限制
|
| 提早检测出app的错误
|
|
| 之前每个应用app链接脚本中的起始地址base address被要求是不同的
|
| 代码和数据存放的位置才不会产生冲突
|
|
| 现在,借助地址空间的抽象
|
| 所有应用程序都使用同样的起始地址base address,只需要一个ld script
|
|
|
|
|
| 在 .text 和 .rodata 中间以及 .rodata 和 .data 中间我们进行了page对齐ALIGN(4K)
|
| 前后两个logical section的访问方式限制是不同的
|
| 鉴于只能以page为单位对这个限制进行设置
|
|
| .data 和 .bss 两个逻辑段由于限制相同,无需进行页面对齐。
|
|
| 左侧是低256g
|
| 直观
|
| 右侧高256g
|
| 最高方跳板,直接映射
|
| 所以与os address space的内容一致
|
| TrapContext也在那,但
|
|
| 这俩都不U
|
| 只在address space切换发挥作用
|
|
|
|
| os/src/build.rs
|
|
|
|
pub fn get_app_data(app_id: usize) -> &'static [u8] {
| get_app_data 则根据传入的应用编号取出对应应用的 ELF 格式可执行文件数据
|
| 基于 build.rs 生成的 link_app.S 给出的符号来确定其位置
|
| 实际放在内核的.data中
|
| loader 模块中原有的os和app stack则分别作为逻辑段放在os和app的space address中,
|
| 我们无需再去专门为其定义一种类型。
|
|
|
|
|
|
| 更新分时多任务
|
| SBI初始化完成
|
| CPU跳转到os entry并在S-Mode上执行 instruction
|
| 还没有开启分页
|
| 开启分页后,内核每次访存看的都是虚拟地址
|
|
|
| os/src/mm/memory_set.rs
|
创建内核地址空间
pub static ref KERNEL_SPACE: Arc> = Arc::new(unsafe {
| UPSafeCell::new(MemorySet::new_kernel()
|
|
| )});
|
| 编译进全局.data
|
|
|
|
os/src/mm/mod.rs
init
| 初始化heap
|
| init_frame_allocator用到了vec需要heap
|
| KERNEL_SPACE.activate
|
这里开启了mmu
|
|
|
|
|
implMemorySet
pub fn activate(&self) {
| 通过老样子token获得root的ppn
|
| 写入satp,正式开始mmu
|
需要注意satp CSR是否是一个平滑的过渡
|
|
| 两条相邻指令的virtual address相邻
|
| 由于切换satp的instauction不是一条跳转指令
|
| pc只简单自增
|
|
| 而它们所在的phy address一般情况下相邻
|
| 但经过地址转换后的virtual address却不同
|
|
| 切换satp导致mmu查多级页表
|
| 要求前后两个address space在切换satp的指令附近映射满足连续
|
| 这条写入satp的instruction和下一条instruction都在os内存布局的.text段中
|
|
| 切换后是一个恒等映射
|
|
| 切换前是物理地址直接取指-类似一个恒等映射
|
|
|
|
插入sfence.vma
| 因多级页表需要多次访问得到最后的pfn
|
| 加入TLB快表来缓存vpn到pfn的映射,加速访存
|
| 修改了satp切换了address space
|
| TLB失效,需要刷新
|
|
|
|
|
跳板
| 跳板干什么用
|
?
|
| 为什么高地址次级页面存放TrapContext
|
?
|
| 为什么不把TrapContext放入应用的内核栈
|
?
|
|
曾经
| 当app trap进os
|
| sscratch指出app stack top
|
| 用一条指令可从app stack 切换到os stack
|
| 然后直接将TrapContext压入os stack top
|
|
| Trap处理完毕,返回app
|
| TrapContext恢复到寄存器
|
| 将保存app stack top的sscratch与sp交换
|
| 从os stack切换回app stack
|
|
|
一旦分页
| 必须在这个过程同时完成address space的切换
|
| 当__alltraps保存Trap Context的时候
|
| 需要修改satp从app address space切换到os address space
|
| 因为trap handler只有在os address space才能访问
|
|
|
|
| address space切换不能影响instruction连续执行
|
| 要求app和os的address space切换address space的instruction附近平滑
|
|
|
内核与应用地址空间的隔离
| 采用xv6的os和app address space隔离的方法
|
| 不需要内核逻辑段在每个app address space映射一次
|
| 在嵌入式平台里无法忽视这种内存开销
|
| 相对每次trap进内核,TLB都会失效
|
| 但任务切换不会,因为都在os内完成
|
|
可以一定程度上应对Meltdown漏洞
| https://cacm.acm.org/magazines/2020/6/245161-meltdown/fulltext
|
|
|
app的TrapContext放入address space次高page
假如放入os stack
| 保存TrapContext之前
|
| 必须先切换到os address space
|
| 需要将os address space token写入satp
|
|
| 之后还需要一个通用reg保存os stack top
|
| 才能以它为base addres保存TrapContext
|
|
| 我们无法在不破坏任何一个通用reg的情况下做到
|
事实上需要用到os两条信息
| address space token
|
| app 内核stack top
|
|
| 硬件只提供一个sscratch周转
|
| 不得不将TrapContext保存在app address space的一个virtual page中
|
| 避免切换到os address space才能保存TrapContext
|
|
在TraopContext包含更多内容
| app初始化的时候由内核写入应用地址空间app address space中的 TrapContext 的相应位置,此后就不再被修改
|
| 不需要每次被保存/恢复
|
| pub struct TrapContext {
|
|
| pub x: [usize; 32],
|
|
| pub sstatus: Sstatus,
|
|
| pub sepc: usize,
|
|
| pub kernel_satp: usize,
|
表示内核地址空间的 token ;
|
| pub kernel_sp: usize,
|
当前应用app在内核地址空间os address space中的内核栈栈顶os stack top的虚拟地址virtual address;
|
| pub trap_handler: usize,
|
内核os中 trap handler 入口点的虚拟地址virtual address。
|
| }
|
|
|
|
|
os/src/trap/trap.S
| __alltraps和__restore
|
| 保存和恢复 TrapContext的同时也切换Address Space的:
|
__alltraps
| Trap进入,
|
|
| sp指向app stack
|
| sscratch指向app address存放TrapContext的位置,在次高Page
|
|
| 见__restore
|
| 里面会将TrapContext保存到sscratch
|
|
| 交换sp-sscratch
|
|
| sp已经变成了TrapContext
|
| 开始保存reg和csr
|
|
|
| 到28结束,全程在app address space完成保存TrapContext
|
|
|
| 然后加载os stack address space token到t0
|
| trap handler entry virtual address到t1
|
|
|
| 将sp修改为app的os stack top的地址
|
|
| satp修改为os stack address space token使用sfence.vma刷新tlb
|
完成切换
|
| jr跳转到t1保存的trap handler入口点地址
|
|
| 不能像之前直接call trap_handler
|
|
|
__restore
两个参数
| TrapContext在app address space的位置
|
对于所有应用相同
|
|
a0传递
|
| 即将回到app的address space token
|
a1传递
|
|
| 由于TrapContext保存在app address space
|
|
| 先切换回app address space
|
|
| 将传入的TrapContext保存回sscratch寄存器
|
__alltraps才能基于它将TrapContext保存到正确位置
|
| 将sp修改为TrapContext
|
|
| 基于它恢复reg和csr
|
|
| 通过sret返回U-Mode
|
|
|
|
|
os/src/linker.ld
| 考虑切换address space后instruction能否连续执行
|
| 3 stext = .;
|
| 4 .text : {
|
| 5 *(.text.entry)
|
| 6+ . = ALIGN(4K);
|
| 7+ strampoline = .;
|
| 8+ *(.text.trampoline);
|
| 9+ . = ALIGN(4K);
|
| 10 *(.text .text.*)
|
|
| 将trap.S整段汇编代码放在.text.trampoline section
|
| 调整布局对齐到.text section的一个page中
|
|
| 这样,这段汇编代码放在一个pfn中
|
| 且 __alltraps 恰好位于这个pfn的开头
|
| 物理地址被外部符号 strampoline 标记
|
|
| 开启分页模式之后
|
| 内核和应用代码都只能看到各自的虚拟地址空间
|
| 在它们的视角中,这段汇编代码被放在它们地址空间的最高虚拟页面上
|
|
| trap前后的一小段时间内会有一个比较 极端 的情况
|
| 刚产生trap时,CPU已经进入了内核态(即Supervisor Mode)
|
| 此时执行代码和访问数据还是在app所处的app address space中
|
|
| CPU指令 为什么能够被连续执行呢?
|
| 无论是os还是app的address space
|
| 跳板的虚拟页均位于同样位置
|
| 将会映射到同一个实际存放这段汇编代码的pfn
|
|
|
impl MemorySet
| 这个在new_kernel和每个elf文件映射都会调用
|
fn map_trampoline(&mut self) {
| self.page_table.map(
|
| VirtAddr::from(TRAMPOLINE).into(),
|
| PhysAddr::from(strampoline as usize).into(),
|
| PTEFlags::R | PTEFlags::X,
|
| );
|
| 直接在多级页表中插入一个从地址空间的最高virtual address
|
| 映射到 跳板汇编代码所在的phy address的键值对
|
|
|
| 解释了__alltraps使用jr而不是直接call trap_handler
|
| 内存布局中,这条 .text.trampoline section中的call指令和 trap_handler 都在.text之内
|
| 汇编器(Assembler) 和链接器(Linker)会根据 linker.ld 的地址布局描述,
|
| 计算两个instruction的地址偏移量
|
| 并让call指令的实际效果为当前 pc 自增这个偏移量。
|
|
| 实际上我们知道由于我们设计的缘故,
|
|
| 这条指令在被执行的时候,
|
|
| 它的虚拟地址被操作os设置在address space中的最高page之内
|
见上面的map_trampoline
|
|
| jr绝对跳转,直接跳转到reg中存储地址
|
| 不依赖pc偏移
|
|
|
|
|
|
加载执行app
| 扩展TCB
|
| 3pub struct TaskControlBlock {
|
|
| 4 pub task_status: TaskStatus,
|
|
| 5 pub task_cx: TaskContext,
|
|
| 6 pub memory_set: MemorySet,
|
app地址空间
|
| 7 pub trap_cx_ppn: PhysPageNum,
|
在高page的TrapContext实际存放的ppn
|
| 8 pub base_size: usize,
|
应用数据大小
| app address space从0x0到app stack结束一共多少字节
|
|
| 后续应包含用于app 动态分配的heap空间大小
|
暂不支持
|
| 9}
|
|
|
| 扩展对tcb的管理
|
impl TaskControlBlock {
pub fn new(elf_data: &[u8], app_id: usize) -> Self {
| 解析elf,构造address space: memory_set
|
| 通过memory_set查找多级页表,找到TrapContext所在的ppn
|
根据传入的app_id,调用kernel_stack_position找到app的os stack的地址range,根据TRAMPOLINE偏移得到
pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
| let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
|
| let bottom = top - KERNEL_STACK_SIZE;
|
| (bottom, top)
|
|
| }
|
|
| 通过insert_framed_area将这个stack_bottom, stack_top放入os address space
|
|
初始化tcb
| 其中task_cx设置为跳转到trap_return不是__restore
|
| 为了支持对app的启动并顺利切换到app address space执行
|
| 只是将ra寄存器设置为trap_return地址
|
|
初始化app的TrapContext
| 在app的address space而不是os的
|
| 只能手动查页表找到TrapContext实际被放在的ppn
|
| 再获得在app address space的TrapContext的可变引用,用于初始化
|
|
|
|
|
| TaskManager
|
| // os/src/task/mod.rs
|
|
|
|
| current_task设置0,从0app开始执行
|
|
| 提供current_user_token和current_trap_cx
|
| 获得当前正在执行的app address space token
|
| 和可以在os address space修改位于该app address space的TrapContext的可变引用
|
|
|
| os/build.rs
|
| 在.incbin不再插入清除全部符号的应用二进制镜像*.bin
|
| 将ELF格式文件直接链接进来
|
|
| 链接每个ELF之前.align 3确保8bytes对齐
|
如果不这样做,xmas-elf crate解析ELF进行不对齐内存读写
| 例如使用ld指令从内存的一个没有对齐到8bytes的address加载一个64bit的值到一个通用reg
|
|
|
|
| 改进Trap处理
|
| TrapContext不再os address space
|
| 通过调用current_trap_cx获得TrapContext可变引用
|
|
| 在trap_handler开头调用set_kernel_trap_entry
|
| 将stvec修改为通同模块下另一个函数trap_from_kernel地址
|
| 就是说:一旦进入os再次触发到S-Mode的Trap
|
| 会在硬件设置一些CSR之后跳过寄存器的保存过程
|
| 直接跳到trap_from_kernel函数
|
| 直接panic退出
|
|
| 因为os和app的address space分离后
|
| 从U trap到S和从S trap到S的处理方式差别大
|
| 不得不实现两遍不太可能整合两个
|
| 为了简单直接panic
|
|
trap_handler完成Trap处理,调用trap_return返回app的U-Mode
先调用set_user_trap_entry
| stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
|
|
| 更新stvec为跳板页而不是链接的__alltraps地址
|
| 因为分页后只能通过跳板页面上的虚拟地址来实际获得__alltraps和__restore
|
|
__restore需要俩参数
| TrapContext在app address space中虚拟地址
|
| 继续执行app address space的token
|
|
找到__restore在os/app address space共同的虚拟地址
| let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
|
| 计算__restore相对于__alltraps的偏移量
|
|
| 使用jr跳转
|
|
| fence.i清空指令缓存i-cache
|
|
| os中进行一些操作导致一些原先存放某个app代码的pfn如今用来存放数据或者是其他应用的代码
|
|
| i-cache可能保存着该pfn的错误快照
|
|
|
|
|
| 改进sys_write实现
|
| sys_write不再能够直接访问位于app address space中的数据
|
| 手动查询页表
|
|
| page_table提供将app address space中一个缓冲区
|
| 转化为在os space address中能够直接访问的形式的辅助函数
|
pub fn translated_byte_buffer(
| token是某个app address space的token
|
| ptr和len,该address space一段缓冲区,起始address和长度
|
| 以vec形式返回一组在os space address直接访问的bytes slice
|
|
| 将每个bytes slice转化为&str输出
|
|
|
|
|
|
|
|
|