地址空间
branch ch4
sv39分页
内存控制相关CSR寄存器
默认MMU不使能
访问地址直接是物理地址
修改satp CSR启用分页模式
RV64 架构下 satp 的字段分布。
MODE
0 所有访存都被视为物理地址
8 SV39 分页
所有 S/U 特权级的访存被视为一个 39 位的虚拟地址
MMU 会将其转换成 56 位的物理地址;
如果转换失败,则会触发异常。
address格式组成
单个page大小4KB
每个virtual page和phy page都按 4 KB 对齐
4KB需要12 bytes的address表示
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);
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管理器需要提供的功能
StackFrameAllocator
最简单栈式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来表示
结构
root_ppn PPN
frames vec
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) {
思路类似只是pte更新成empty
不经过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)
使用最高的256GB
保存一个跳板Trampoline
保存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) {
就是push个MapArea::new
pub fn new_kernel() -> Self;
生成os address space
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地址空间
impl MemorySet
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
BASE_ADDRESS = 0x0 是虚拟地址
在 .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
不再将bin链接进内核
直接使用elf
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是否是一个平滑的过渡
切换satp的指令
和下一条指令
两条相邻指令的virtual address相邻
由于切换satp的instauction不是一条跳转指令
pc只简单自增
而它们所在的phy address一般情况下相邻
但经过地址转换后的virtual address却不同
切换satp导致mmu查多级页表
要求前后两个address space在切换satp的指令附近映射满足连续
这条写入satp的instruction和下一条instruction都在os内存布局的.text段中
TODO 这是为啥
切换后是一个恒等映射
切换前是物理地址直接取指-类似一个恒等映射
插入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才能访问
__restore同理,需要切回去
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
vec保存tcb
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如今用来存放数据或者是其他应用的代码
TODO
缺个例子
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输出

Generated 2025-05-05 04:17:35 +0000
25bcfca-dirty 2025-05-04 14:47:56 +0000