Lab3: Page tables
在本实验室中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。
Attention
开始编码之前,请阅读xv6手册的第3章和相关文件:
- kernel/memlayout.h,它捕获了内存的布局。
- kernel/vm.c,其中包含大多数虚拟内存(VM)代码。
- kernel/kalloc.c,它包含分配和释放物理内存的代码。
要启动实验,请切换到pgtbl分支:
git fetch
git checkout pgtbl
make clean
Print a page table(easy)
为了帮助您了解RISC-V页表,也许为了帮助将来的调试,您的第一个任务是编写一个打印页表内容的函数。
YOUR JOB
定义一个名为
vmprint()
的函数。它应当接收一个pagetable_t
作为参数,并以下面描述的格式打印该页表。在exec.c
中的return argc
之前插入if(p->pid==1) vmprint(p->pagetable)
,以打印第一个进程的页表。如果你通过了pte printout
测试的make grade
,你将获得此作业的满分。
现在,当您启动xv6时,它应该像这样打印输出来描述第一个进程刚刚完成exec()
inginit
时的页表:
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
第一行显示vmprint
的参数。之后的每行对应一个PTE,包含树中指向页表页的PTE。每个PTE行都有一些“..
”的缩进表明它在树中的深度。每个PTE行显示其在页表页中的PTE索引、PTE比特位以及从PTE提取的物理地址。不要打印无效的PTE。在上面的示例中,顶级页表页具有条目0和255的映射。条目0的下一级只映射了索引0,该索引0的下一级映射了条目0、1和2。
您的代码可能会发出与上面显示的不同的物理地址。条目数和虚拟地址应相同。
一些提示:
- 你可以将
vmprint()
放在kernel/vm.c中 - 使用定义在kernel/riscv.h末尾处的宏
- 函数
freewalk
可能会对你有所启发 - 将
vmprint
的原型定义在kernel/defs.h中,这样你就可以在exec.c
中调用它了 - 在你的
printf
调用中使用%p
来打印像上面示例中的完成的64比特的十六进制PTE和地址
QUESTION
根据文本中的图3-4解释
vmprint
的输出。page 0包含什么?page 2中是什么?在用户模式下运行时,进程是否可以读取/写入page 1映射的内存?
A kernel page table per process (hard)
Xv6有一个单独的用于在内核中执行程序时的内核页表。内核页表直接映射(恒等映射)到物理地址,也就是说内核虚拟地址x
映射到物理地址仍然是x
。Xv6还为每个进程的用户地址空间提供了一个单独的页表,只包含该进程用户内存的映射,从虚拟地址0开始。因为内核页表不包含这些映射,所以用户地址在内核中无效。因此,当内核需要使用在系统调用中传递的用户指针(例如,传递给write()
的缓冲区指针)时,内核必须首先将指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。
YOUR JOB
你的第一项工作是修改内核来让每一个进程在内核中执行时使用它自己的内核页表的副本。修改
struct proc
来为每一个进程维护一个内核页表,修改调度程序使得切换进程时也切换内核页表。对于这个步骤,每个进程的内核页表都应当与现有的的全局内核页表完全一致。如果你的usertests
程序正确运行了,那么你就通过了这个实验。
阅读本作业开头提到的章节和代码;了解虚拟内存代码的工作原理后,正确修改虚拟内存代码将更容易。页表设置中的错误可能会由于缺少映射而导致陷阱,可能会导致加载和存储影响到意料之外的物理页存页面,并且可能会导致执行来自错误内存页的指令。
提示:
- 在
struct proc
中为进程的内核页表增加一个字段 - 为一个新进程生成一个内核页表的合理方案是实现一个修改版的
kvminit
,这个版本中应当创造一个新的页表而不是修改kernel_pagetable
。你将会考虑在allocproc
中调用这个函数 - 确保每一个进程的内核页表都关于该进程的内核栈有一个映射。在未修改的XV6中,所有的内核栈都在
procinit
中设置。你将要把这个功能部分或全部的迁移到allocproc
中 - 修改
scheduler()
来加载进程的内核页表到核心的satp
寄存器(参阅kvminithart
来获取启发)。不要忘记在调用完w_satp()
后调用sfence_vma()
- 没有进程运行时
scheduler()
应当使用kernel_pagetable
- 在
freeproc
中释放一个进程的内核页表 - 你需要一种方法来释放页表,而不必释放叶子物理内存页面。
- 调式页表时,也许
vmprint
能派上用场 - 修改XV6本来的函数或新增函数都是允许的;你或许至少需要在kernel/vm.c和kernel/proc.c中这样做(但不要修改kernel/vmcopyin.c, kernel/stats.c, user/usertests.c, 和user/stats.c)
- 页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含
sepc=0x00000000XXXXXXXX
的错误提示。你可以在kernel/kernel.asm通过查询XXXXXXXX
来定位错误。
Simplify copyin / copyinstr
(hard)
内核的copyin
函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。这个转换是通过在软件中遍历进程页表来执行的。在本部分的实验中,您的工作是将用户空间的映射添加到每个进程的内核页表(上一节中创建),以允许copyin
(和相关的字符串函数copyinstr
)直接解引用用户指针。
YOUR JOB
将定义在kernel/vm.c中的
copyin
的主题内容替换为对copyin_new
的调用(在kernel/vmcopyin.c中定义);对copyinstr
和copyinstr_new
执行相同的操作。为每个进程的内核页表添加用户地址映射,以便copyin_new
和copyinstr_new
工作。如果usertests
正确运行并且所有make grade
测试都通过,那么你就完成了此项作业。
此方案依赖于用户的虚拟地址范围不与内核用于自身指令和数据的虚拟地址范围重叠。Xv6使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,这个方案将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,在XV6中该地址是0xC000000
,即PLIC寄存器的地址;请参见kernel/vm.c中的kvminit()
、kernel/memlayout.h和文中的图3-4。您需要修改xv6,以防止用户进程增长到超过PLIC的地址。
一些提示:
- 先用对
copyin_new
的调用替换copyin()
,确保正常工作后再去修改copyinstr
- 在内核更改进程的用户映射的每一处,都以相同的方式更改进程的内核页表。包括
fork()
,exec()
, 和sbrk()
. - 不要忘记在
userinit
的内核页表中包含第一个进程的用户页表 - 用户地址的PTE在进程的内核页表中需要什么权限?(在内核模式下,无法访问设置了
PTE_U
的页面) - 别忘了上面提到的PLIC限制
Linux使用的技术与您已经实现的技术类似。直到几年前,许多内核在用户和内核空间中都为当前进程使用相同的自身进程页表,并为用户和内核地址进行映射以避免在用户和内核空间之间切换时必须切换页表。然而,这种设置允许边信道攻击,如Meltdown和Spectre。
QUESTION
解释为什么在
copyin_new()
中需要第三个测试srcva + len < srcva
:给出srcva
和len
值的例子,这样的值将使前两个测试为假(即它们不会导致返回-1),但是第三个测试为真 (导致返回-1)。
可选的挑战练习
- 使用超级页来减少页表中PTE的数量
- 扩展您的解决方案以支持尽可能大的用户程序;也就是说,消除用户程序小于PLIC的限制
- 取消映射用户进程的第一页,以便使对空指针的解引用将导致错误。用户文本段必须从非0处开始,例如4096
My record
1. Print a page table
本实验主要实现一个打印页表内容的函数,首先根据提示在exec.c
中的return agrc
前插入if (p->pid == 1) vmprint(p->pagetable);
准备事项:
阅读xv6 手册第三章
阅读源码:内存布局:kernel/memlayout.h
、虚拟内存:kernel/vm.c
、分配,释放物理内存kernel/kalloc.c
重点阅读kernel/vm.c
里面的freewalk
方法
主要内容如下
// Recursively free page-table pages.
// All leaf mappings must already have been removed.
void
freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
它首先会遍历整个页表,遇到有效的页表且不在最后一层时,它会递归调用。
PTE_V
是用来判断页表相是否有效,而(pte & (PTE_R|PTE_W|PTE_X)) == 0
则是用来判断该页表是否不在最后一层(叶子)。因为最后一层页表中页表项中的W R x
起码有一位会被设置为1。注释里点明所有最后一层的页表项已经被释放了,所以遇到不符合情况的就直接panic("freewalk: leaf")
仿照freewalk()
,我们可以写出如下递归函数_vmprintf()
。对于每一个有效的页表项都打印自己和其子项的内容。如果不是最后一层的页表就继续递归调用。通过level
来控制前缀..
的数量。
//Lab3: page tables
// pagetable:所要打印的页表,level:页表的层级
void
_vmprintf(pagetable_t pagetable, int level){
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){ //判断页表是否有效
for(int j = 0; j < level; j++){
if(j)
printf(" ");
printf("..");
}
uint64 child = PTE2PA(pte);
printf("%d: pte %p pa %p\n", i, pte, child);
if((pte & (PTE_R | PTE_W | PTE_X)) == 0){ //判断是否不在最后一层。
// this PTE points to a lower-level page table.
_vmprintf((pagetable_t)child, level+1);
}
}
}
}
void
vmprintf(pagetable_t pagetable){
printf("page table %p\n",pagetable);
_vmprintf(pagetable, 1);
}
记得在defs.h
里面声明函数
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
void vmprint(pagetable_t);
跑一下make qemu
可以看到
nice
2. A kernel page table per process
本实验主要是让每个进程都有自己的内核页表,这样在内核中执行时使用它自己的内核页表的副本。
-
首先给
kernel/proc.h
里面的struct proc
加上内核页表的字段// Per-process state struct proc { struct spinlock lock; // p->lock must be held when using these: enum procstate state; // Process state struct proc *parent; // Parent process void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent's wait int pid; // Process ID // these are private to the process, so p->lock need not be held. uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table pagetable_t kernel_pagetable;// 进程的内核页表 struct trapframe *trapframe; // data page for trampoline.S struct context context; // swtch() here to run process struct file *ofile[NOFILE]; // Open files struct inode *cwd; // Current directory char name[16]; // Process name (debugging) };
-
在
vm.c
中添加新的方法proc_kpt_init
,该方法用于在allocproc
中初始化进程的内核页表。这个函数还需要一个辅助函数uvmmap
,该函数和kvmmap
函数几乎一样,不同的是kvmmap
是对Xv6的全局内核页表进行映射(未修改的Xv6仅有一个内核页表),而uvmmap
将用于进程的内核页表进行映射。// 仿造kvmmap void uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm){ if(mappages(pagetable, va, sz, pa, perm) != 0) panic("uvmmap"); } //用于在allocproc中初始化进程的内核页表,allocproc 在 proc.c 里面 pagetable_t proc_kpt_init(){ pagetable_t kernelpt = uvmcreate(); if (kernelpt == 0) return 0; //接下来和 kvmmap是一样的 //异步寄存器 uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W); //虚拟内存磁盘接口 uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W); //CLINIT uvmmap(kernelpt,CLINT, CLINT, 0x10000, PTE_R | PTE_W); //PLIC uvmmap(kernelpt,PLIC, PLIC, 0x400000, PTE_R | PTE_W); // map kernel text executable and read-only. uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X); // map kernel data and the physical RAM we'll make use of. uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W); // map the trampoline for trap entry/exit to // the highest virtual address in the kernel. uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X); return kernelpt; }
然后再
kernel/proc.c
里面的allocproc
调用// An empty user page table. p->pagetable = proc_pagetable(p); if(p->pagetable == 0){ freeproc(p); release(&p->lock); return 0; } // 初始化内核页表 p->kernel_pagetable = proc_kpt_init(); if(p->kernel_pagetable == 0){ freeproc(p); release(&p->lock); return 0; }
-
根据提示,为了确保每一个进程的内核页表都关于该进程的内核栈有一个映射。我们需要将
procinit
方法中相关的代码迁移到allocproc
方法中。(有一点小小的不同),将一下代码放在上面代码的后面即可// 从 procinit() 中复制了一部分过来,这一部分是给内核栈分配一个页,将其映射在内存的高位,并在后面跟上一个无效的保护页。 但是有不一样的地方 // Allocate a page for the process's kernel stack. // Map it high in memory, followed by an invalid // guard page. char *pa = kalloc(); if(pa == 0) panic("kalloc"); uint64 va = KSTACK((int) (p - proc)); // 注意这里用的是uvmmap() 我们自己写的映射,kvmmap是映射的全局内核页表,这里我们要映射的是每一个进程的内核页表 uvmmap(p->kernel_pagetable,va, (uint64)pa, PGSIZE, PTE_R | PTE_W); p->kstack = va;
-
我们需要修改
scheduler()
来加载进程的内核页表到SATP寄存器中。提示指明让我们参考kvminithart()
// Switch h/w page table register to the kernel's page table, // and enable paging. void kvminithart() { w_satp(MAKE_SATP(kernel_pagetable)); sfence_vma(); }
kvminithart
适用于原先的内核页表,我们将进程的内核页表作为参数传入即可。在vm.c
中添加一个新方法proc_inithart
void proc_inithart(pagetable_t kpt){ w_satp(MAKE_SATP(kpt)); sfence_vma(); }
然后在
scheduler()
内调用即可,但在进程运行结束后(Swtch后)需要切换会原先的全局内核页表(提示中指明)。可以直接调用上面的kvminithart()
即可for(p = proc; p < &proc[NPROC]; p++) { acquire(&p->lock); if(p->state == RUNNABLE) { // Switch to chosen process. It is the process's job // to release its lock and then reacquire it // before jumping back to us. p->state = RUNNING; c->proc = p; // 加载进程的内核页表到CPU的 satp 寄存器中, proc_inithart(p->kernel_pagetable); swtch(&c->context, &p->context); //切换回原来的内核页表 kvminithart(); // Process is done running for now. // It should have changed its p->state before coming back. c->proc = 0; found = 1; } release(&p->lock); }
-
在
freeproc
中释放一个进程的内核页表。首先释放页表内的内核栈,调用uvmunmap
可以解除映射,最后一个参数为1时会释放实际内存。然后释放进程的内核页表,先在
kernel/proc.c
中添加一个方法proc_freekernelpt
。如下,遍历整个内核页表,然后将所有有效的页表项清空为0,如果整个页表项不是叶子,就继续递归。// free a proc structure and the data hanging from it, // including user pages. // p->lock must be held. static void freeproc(struct proc *p) { if(p->trapframe) kfree((void*)p->trapframe); p->trapframe = 0; if(p->pagetable) proc_freepagetable(p->pagetable, p->sz); p->pagetable = 0; p->sz = 0; p->pid = 0; p->parent = 0; p->name[0] = 0; p->chan = 0; p->killed = 0; p->xstate = 0; //释放进程的内核栈,调用uvmunmap可以解除映射,最后一个参数 do_free 为1时,会释放实际内存 if(p->kstack) uvmunmap(p->kernel_pagetable, p->kstack, 1, 1); p->kstack = 0; // 释放没有空闲物理内存的内核页面 if(p->kernel_pagetable){ proc_freekernelpt(p->kernel_pagetable); } p->kernel_pagetable = 0; p->state = UNUSED; } //释放进程的内核页表 void proc_freekernelpt(pagetable_t kernelpt){ // 和freewalk 相似 // 一个页表中有 2^9 = 512 个 PTE for(int i = 0; i < 512; i++){ pte_t pte = kernelpt[i]; if((pte & PTE_V)){ //PTE有效 kernelpt[i] = 0; if((pte & (PTE_R | PTE_W | PTE_X)) == 0){ uint64 child = PTE2PA(pte); proc_freekernelpt((pagetable_t)child); } } } kfree((void*)kernelpt); }
-
将需要的函数定义添加到头文件
defs.h
中// proc.c int cpuid(void); void exit(int); int fork(void); int growproc(int); pagetable_t proc_pagetable(struct proc *); void proc_freepagetable(pagetable_t, uint64); void proc_freekernelpt(pagetable_t); int kill(int); // vm.c void kvminit(void); void kvminithart(void); void proc_inithart(pagetable_t); // 将进程的内核页表保存到SATP寄存器 uint64 kvmpa(uint64); void kvmmap(uint64, uint64, uint64, int); void uvmmap(pagetable_t, uint64, uint64, uint64, int); pagetable_t proc_kpt_init(void); // 用于内核页表的初始化 int mappages(pagetable_t, uint64, uint64, uint64, int); pagetable_t uvmcreate(void);
-
修改
vm.c
中的kvmpa
,将原先的kernel_pagetable
改成myproc()->kernelpagetable
,使用进程的内核页表。(记得加头文件,不然myproc不好使)#include "param.h" #include "types.h" #include "memlayout.h" #include "elf.h" #include "riscv.h" #include "defs.h" #include "fs.h" #include "spinlock.h" //add #include "proc.h" //add // translate a kernel virtual address to // a physical address. only needed for // addresses on the stack. // assumes va is page aligned. uint64 kvmpa(uint64 va) { uint64 off = va % PGSIZE; pte_t *pte; uint64 pa; //修改的地方 //pte = walk(kernel_pagetable, va, 0); pte = walk(myproc()->kernel_pagetable, va, 0); if(pte == 0) panic("kvmpa"); if((*pte & PTE_V) == 0) panic("kvmpa"); pa = PTE2PA(*pte); return pa+off; }
测试代码
& make qemu
usertests
得到以下输出:(测试时间较长)
test stacktest: usertrap(): unexpected scause 0x000000000000000d pid=6269
sepc=0x0000000000002188 stval=0x000000000000fbc0
OK
test opentest: OK
test writetest: OK
test writebig: OK
test createtest: OK
test openiput: OK
test exitiput: OK
test iput: OK
test mem: OK
test pipe1: OK
test preempt: kill... wait... OK
test exitwait: OK
test rmdot: OK
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
3. Simplify copyin/coptinstr
大致思路:
通过添加用户地址空间的映射到进程的内核页表,这样在内核系统调用copyin()
和copyinstr()
就不用使用walk()
函数让操作系统将虚拟地址换为物理地址进行字符拷贝(因为全局内核页表不知道用户地址空间的映射情况);而是直接使用MMU来完成虚拟地址到物理地址的转换。这需要保证用户页表p->pagetable
发生变动的同时修改p->kernel_pagetable
,根据指导书,我们了解到,主要修改fork()
、exec()
、sbrk()
这三个函数。
步骤
-
编写函数:将用户页表拷贝到内核页表
- 参考
kernel/vm.c
中的uvmcopy()
函数,该函数是在fork()
时用于将父进程用户页拷贝到子进程。 - 此处的
uvmcopy()
在拷贝时并没有使用写时复制,而是直接分配相应的页面(物理内存)并复制字节;而将用户页表拷贝到内核页表时,物理地址空间实际上是不变的,只是多了一次映射。因此u2kvmcopy()
中无需kalloc()
来分配新页面,而是直接复用walk()
返回的物理地址 - 此外,指导书中指明,带有PTE_U标志的PTE不能被内核访问,因此在拷贝用户页表时,需要清除掉PTE中的PTE_U标志
// Lab3-3 // 将用户页表拷贝到内核页表 // copy the user page table into its kernel page table from 'begin' to 'end' - lab3-3 void u2kvmcopy(pagetable_t upagetable, pagetable_t kernelpt, uint64 begin, uint64 end){ pte_t *pte_from, *pte_to; uint64 pa, i; uint flags; uint64 begin_page = PGROUNDUP(begin); // 向上取整‘ for(i = begin_page; i < end; i+= PGSIZE){ if((pte_from = walk(upagetable, i ,0)) == 0) panic("u2kvmcopy: src pte does not exist"); if((pte_to = walk(kernelpt, i, 1)) == 0) panic("u2kvmcopy: pte walk failed"); pa = PTE2PA(*pte_from); flags = PTE_FLAGS(*pte_from) & (~PTE_U); // 直接映射物理地址给内核页表 *pte_to = PA2PTE(pa) | flags; } }
- 参考
-
然后在内核更改进程的用户映射的每一处(
fork(),exec(),sbrk()以及userint()
)都进行一次复制操作。-
修改
fork()
-
kernel/proc.c
中的fork()
函数用于创建子进程,其中在用户页表被拷贝给子进程(从父进程)后,也需要拷贝父进程的内核页表给子进程// kernel/proc.c // fork() ... // Copy user memory from parent to child. if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){ freeproc(np); release(&np->lock); return -1; } np->sz = p->sz; // 将新进程的用户页表数据拷贝给内核页表 // 仿造上面的拷贝内存的写法 u2kvmcopy(np->pagetable, np->kernel_pagetable, 0, np->sz); np->parent = p; // copy saved user registers. *(np->trapframe) = *(p->trapframe); // Cause fork to return 0 in the child. np->trapframe->a0 = 0; ...
-
-
修改
exec()
kernel/exec.c
中的函数exec()
用于替换进程镜像,在替换之后会将原用户页表释放掉替换为新的用户页表,因此需要同样更新进程的内核页表。
// kernel/exec.c // exec() ... // Commit to the user image. oldpagetable = p->pagetable; p->pagetable = pagetable; p->sz = sz; p->trapframe->epc = elf.entry; // initial program counter = main p->trapframe->sp = sp; // initial stack pointer proc_freepagetable(oldpagetable, oldsz); //复制用户页表给内核页表 u2kvmcopy(p->pagetable, p->kernel_pagetable, 0, sz); // Lab3-1: page tables // 打印页表 if(p->pid == 1) vmprintf(p->pagetable); return argc; // this ends up in a0, the first argument to main(argc, argv) ...
-
修改
growproc()
,其实对应的就是提示中的sbrk()
sbrk()
函数即系统调用sys_brk
函数,最终会调用kernel/proc.c
中的growproc()
函数。该函数用于增长或减少虚拟内存空间。根据指导书要求,要保证用户空间大小在PLIC部分之下。因此,在n>0
时要判断sz + n > PLIC
的情况。
// kernel/proc.c growproc() ... sz = p->sz; if(n > 0){ // 加上PLIC限制,防止用户增长到超过PLIC的地址 if(PGROUNDUP(sz + n) >= PLIC){ return -1; } if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) { return -1; } //复制一份用户页表到内核页表中 u2kvmcopy(p->pagetable, p->kernel_pagetable, sz - n, sz); } else if(n < 0){ sz = uvmdealloc(p->pagetable, sz, sz + n); } p->sz = sz; return 0; ...
-
修改
userinit()
kernel/proc.c
中的userinit()
函数用于初始化Xv6启动时第一个用户进程,该进程的加载是独立的,因此也需要将用户页表拷贝到内核页表。
// allocate one user page and copy init's instructions // and data into it. uvminit(p->pagetable, initcode, sizeof(initcode)); p->sz = PGSIZE; // init kernel page table u2kvmcopy(p->pagetable, p->kernel_pagetable, 0, p->sz); // prepare for the very first "return" from kernel to user. p->trapframe->epc = 0; // user program counter p->trapframe->sp = PGSIZE; // user stack pointer
-
替换
copyin() / copyinstr()
- 直接将两者的源码注释掉,分别调用以提供的
copyin_new() / copyinstr_new()
即可
// Copy from user to kernel. // Copy len bytes to dst from virtual address srcva in a given page table. // Return 0 on success, -1 on error. int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) { /*uint64 n, va0, pa0; while(len > 0){ va0 = PGROUNDDOWN(srcva); pa0 = walkaddr(pagetable, va0); if(pa0 == 0) return -1; n = PGSIZE - (srcva - va0); if(n > len) n = len; memmove(dst, (void *)(pa0 + (srcva - va0)), n); len -= n; dst += n; srcva = va0 + PGSIZE; } return 0; */ return copyin_new(pagetable, dst, srcva, len); } // Copy a null-terminated string from user to kernel. // Copy bytes to dst from virtual address srcva in a given page table, // until a '\0', or max. // Return 0 on success, -1 on error. int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max) { /* uint64 n, va0, pa0; int got_null = 0; while(got_null == 0 && max > 0){ va0 = PGROUNDDOWN(srcva); pa0 = walkaddr(pagetable, va0); if(pa0 == 0) return -1; n = PGSIZE - (srcva - va0); if(n > max) n = max; char *p = (char *) (pa0 + (srcva - va0)); while(n > 0){ if(*p == '\0'){ *dst = '\0'; got_null = 1; break; } else { *dst = *p; } --n; --max; p++; dst++; } srcva = va0 + PGSIZE; } if(got_null){ return 0; } else { return -1; } */ return copyinstr_new(pagetable, dst, srcva, max); }
- 直接将两者的源码注释掉,分别调用以提供的
-
test去吧
思考题
Q:为什么在第三个测试中,copyin_new()
中的srcva + len < srcva
这个条件是必要的?……
A:
三个return -1
的条件分别为srcva >= p->sz , srcva + len >= p->sz , srcva + len < srcva
。很显然,第三个条件是用来检测srcva溢出的,防止无符号整数上溢。由于srcva
和len
均为uint64
类型的变量,当srcva
小于p->sz
但是len
为一个极大的数时,如0xffff...fff
(即对应-1
)。由于无符号整数溢出便可以满足srcva + len < p->sz
这一条件,但实际上复制了大量内存。而通过scrva + len < srcva
这一判断条件能够检测出溢出。