MIT 6.S081 OS Lab3 page tables

知识点

  • RISC-V 的逻辑地址寻址是采用三级页表的形式,9 bit 一级索引找到二级页表,9 bit 二级索引找到三级页表,9 bit 三级索引找到内存页,最低 12 bit 为页内偏移(即一个页 4096 bytes)。
  • 虚拟地址连续但是物理地址不一定连续,这给了内核释放和分配页面时的灵活性。
  • 在XV6中,一个进程会包含text、data、stack、guard page、heap以及trampoline和trapframe 进程的用户地址空间及其初始堆栈
  • 内核地址空间
    内核地址空间

xv6源码解析

参考6.S081——虚拟内存部分——xv6源码完全解析系列(1)阅读源码

  1. 首先是vm.c中三个全局变量的含义
    // vm.c
    pagetable_t kernel_pagetable; 
    // 页表项pte_t是8bytes的变量(uint64)
    // 页表pagetable_t是pte_t的数组,所以是(uint64*)
    // kernel_pagetable
    extern char etext[];  
    // kernel.ld sets this to end of kernel code.
    // 在链接脚本中这是内核代码的结束位置
    // etext变量实际上定义内核代码的结束位置,是一个字节指针
    extern char trampoline[]; // trampoline.S
    // tramploine变量指向trampoline.S的开始,也是字节指针
    
  2. 寻址过程
    • 页表项和物理地址的相互转换

      // shift a physical address to the right place for a PTE.
      #define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
      #define PTE2PA(pte) (((pte) >> 10) << 12)
      

      在risc-v Sv39中使用三级页表项,一个页表包含512个PTE,每个PTE有8bytes,页面的大小是4096bytes。这里注意页表也是以页面的形式存在于内存中,所以这里512 = 4096/8。
      risc-v Sv39地址转换过程

      从一个Virtual Address到Physical Address的过程,首先只需要找到物理地址所在的页面(PPN)和相对页面起始位置的偏移(Offset)。
      risc-v Sv39中Virtual Address的 [ 0 : 11 ] [0:11] [0:11] 12bit指示了偏移量Offset,而页面起始位置则需要通过三级页表进行查找,先从satp寄存器中找出0级页表的起始位置(页表的大小等于页面的大小),先从Virtual Address [ 12 : 20 ] [12:20] [12:20] 9个bit指示了对应一级页表中PTE的下标,PTE中的 [ 53 : 10 ] [53:10] [53:10] 44bit(PPN)指示了二级页表的起始位置,再从Virtual Addres [ 21 : 29 ] [21:29] [21:29] 9bit作为二级页表的索引,查找对应的PTE,以此类推,最后从三级页表中找到对应PTE,以其PPN与Offset组合得到Physical Address。

  3. 内核页表相关函数
    • walk函数
      pte_t *	walk(pagetable_t pagetable, uint64 va, int alloc);
      // 软件模拟硬件MMU查找页表的过程
      // 返回以pagetable为根页表,经过多级索引之后va所对应的页表项
      // 如果alloc!=0,则在需要时创建新的页表页
      
      在walk函数中有
      // 这行代码从PTE中提取出物理地址,直接赋值给pagetable指针(而它是一个虚拟地址)
      // 只有在虚拟地址==物理地址时合理,即直接映射,这样做才是合理的。
      pagetable = (pagetable_t)PTE2PA(*pte);
      
      xv6 book中已经指出了,那就是以上代码可以正常工作的前提是,我们在进行内核地址空间的映射时,物理内存和虚拟内存采用的是直接映射的方法。
    • mappages函数
      mappages函数的主要功能是将虚拟地址范围 [ v a , v a + s i z e ) [va, va + size) [va,va+size) 映射到物理地址范围n [ p a , p a + s i z e ) [pa, pa + size) [pa,pa+size),通过逐页映射,mappages确保虚拟地址空间的连续性,同时将物理内存分页管理。
      int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm);
      // pagetable是当前进程的页表,va是虚拟地址起始位置
      // size是需要映射的虚拟地址范围大小(单位byte),可以不用页面对齐
      // pa是物理地址起始位置
      // perm为访问权限
      
      在其中用到了两个
      // PGROUNDUP(sz):sz大小的内存至少使用多少页才可以存下,返回的是下一个未使用页的地址
      // PGROUNDDOWN(a):地址a所在页面是多少号页面,拉回所在页面开始地址
      #define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))
      #define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
      
    • kvmmake函数
      调用kvmmap(仅是mappages函数薄薄的一层封装与调用,使用时将内核页表指针传入mappages函数的第一项即可。)为内核建立了一个直接映射的页表,除了trampoline和内核栈页面被映射到高地址空间以外(主要是为了设置守护页),其他的部分全部是直接映射关系,这样的好处是方便内核的操作,直接将返回的物理地址当虚拟地址使用,就像上面的walk函数一样。
    • kvminit函数和kvminithart函数
      kvminit函数简单调用kvmmake函数,传递给全局变量kernel_pagetable;kvminithart函数利用kernel_pagetable设置satp寄存器(打开分页),清除TLB。此后物理地址和虚拟地址本质上还是相等的(除了内核栈和trampoline页面)。

以上是内核页表的设置,接下来是用户页表的设置,参考6.S081——虚拟内存部分——xv6源码完全解析系列(2)

  1. 用户页表相关函数

    • walkaddr函数
      walkaddr函数是walk函数的一层封装,专门用来查找用户页表中特定虚拟地址va所对应的物理地址。它只用来查找用户页表,返回的是物理地址,而非像walk函数那样只返回最终层的PTE。
      walkaddr被copyin/copyout/copystr三个函数调用,负责在内核态和用户态之间拷贝数据。
    • freewalk函数
      回收页表页的内存,从源码上的英文注释所述,在调用这个函数时应该保证叶子级别页表的映射关系全部解除并释放(这将会由后面的uvmunmap函数负责),因为此函数专门用来回收页表页。uvmunmap函数和freewalk函数结合,成功实现了页表页、物理页的全面释放。
    • uvmcreate函数
      为用户进程分配一个页表页并返回指向此页的指针。
    • uvminit函数
      将initcode加载到用户页表的0地址上,initcode是启动第一个进程时所需要的一些代码。这个函数的作用就是将initcode映射到用户地址空间中。首先给出xv6中用户地址空间的设计,可以看到从虚拟地址0开始存放的是进程的代码段,对于操作系统启动的第一个进程而言,这个位置放置的就是initcode代码。
    • uvmunmap函数
      取消用户进程页表中指定范围的映射关系,从虚拟地址va开始释放npages个页面,但是要注意va一定要是页对齐的。该函数与freewalk函数配合,该函数负责释放叶级页表中PTE记录的映射关系,也会回收标志位do_free置位的物理页面,而freewalk页面则用于释放页表页。
    • uvmdealloc函数与uvmalloc函数
      一个是回收用户页表中的页面,从oldsz修改到newsz并返回新地址空间的大小,另外一个申请更多内存。
    • uvmcopy函数
      主要是为fork调用服务,会将父进程的整个地址空间全部复制到子进程中,这包括页表本身和页表指向的物理内存中的数据。
    • uvmclear函数
      uvmclear函数专门用来清除一个PTE的用户使用权限,用于exec函数来设置守护页。
    • uvmfree函数
      封装了uvmunmap和freewalk函数,用于完全释放用户的地址空间
  2. copy函数
    参考6.S081——虚拟内存部分——xv6源码完全解析系列(3)

    • copyin函数
      从给定的用户页表pagetable的虚拟地址srcva处拷贝长度为len字节的数据到地址dst处。
    • copyinstr函数
      与copyin不同的地方在于,因为copyinstr复制的对象是字符串,所以复制结束的标志是遇到空字符(null-terminated)。
    • copyout函数
      // 译:从内核空间向用户空间拷贝数据
      // 从src拷贝len长度的字节到pagetable页表的dstva位置处
      // 成功时返回0,错误时返回-1
      int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len);
      

copyin代码运行在内核态,引用指针变量的地方如dst会通过MMU硬件查询内核页表为对应的物理地址,用户态下的虚拟地址则需要用用户页表,但内核态下CPU持有的页表是内核页表,只能用软件模拟MMU查找过程(walkaddr函数),对于copyout代码则恰好相反,需要软件模拟MMU查找内核地址

Lab

Print a page table

定义一个vmprint()函数,传入一个 pagetable_t参数,打印出对应格式的页表,在exec.c中插入if(p->pid==1) vmprint(p->pagetable)以打印出第一个进程的页表,

freewalk函数的作用是递归释放页表

// vm.c
// 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);
}

仿照freewalk写出vmprint函数

// defs.h
void            vmprint(pagetable_t pagetable); // 添加声明
// vm.c
void _vmprint(pagetable_t pagetable, int level){
  for (int i = 0; i < 512; i++)
  {
    pte_t pte = pagetable[i];
    if (pte & PTE_V)
    {
      // 叶子结点
      uint64 child = PTE2PA(pte);    
      printf("..");
      for (int j = 0; j < level; j++)
        printf(" ..");
       
      printf("%d: pte %p pa %p\n", i, pte, child);

      if ((pte & (PTE_R | PTE_W | PTE_U)) == 0) // 非叶子节点
      {
        _vmprint((pagetable_t)child, level + 1);
      }
    }
  }
}
void vmprint(pagetable_t pagetable){
  printf("page table %p\n", pagetable);
  _vmprint(pagetable,0);
}

exec函数中添加,注意在分配页表之后打印

// exec.c
if(p->pid==1) vmprint(p->pagetable); // 打印当前进程pagetable

A kernel page table per process

更改内核使得每一个进程当运行于内核态时都有自己内核页表,更改proc结构体为每一个进程维护一个内核页表,更改调度器,当切换进程时切换内核页表。每个进程的内核页表应该与现有全局内核页表相同。
提示:

  • 在proc结构体中为内核页表添加一个字段
  • 一个方式是修改kvminit函数使得当创建进程时产生一个新的内核页表而不是修改kernel_pagetable,并且在allocproc函数中调用kvminit
  • 确保每个进程的内核页表映射对应进程的内核栈,在没有修改的XV6中,内核栈在procint中被设置,需要在allocproc中移除一些或者所有功能
  • 修改scheduler()以加载内核页表到核心的satp寄存器(参考kvminihart函数),在w_satp()函数后需要调用sfence_vma();
  • scheduler应该使用kernel_pagetable当没有进程处于RUNNING态
  • 释放进程内核页表在freeproc函数中
  • 需要一种方式在无需释放叶物理页面的情况下释放一个页表
  • vmprint可以帮助调试
  • 可以修改或添加函数到xv6,至少需要修改 kernel/vm.c 和kernel/proc.c,但是不要修改kernel/vmcopyin.c, kernel/stats.c, user/usertests.c, and user/stats.c
  • 缺少页表映射可能会导致内核遇到页面错误。它将打印包含 sepc=0x00000000XXXXXXXX 的错误。通过在 kernel/kernel.asm 中搜索 XXXXXXXX 来找出故障发生的位置。

有两种方式来完成以上任务

  1. 复制内核页表
    参考【MIT-6.S081-2020】Lab3 Pgbtl
    根据提示来做

    • proc结构体添加字段

      // proc.h
      struct proc{
      // ...
      pagetable_t kernel_pagetable;
      }
      
    • 仿照kvminit函数,在创建进程时,创建用户的内核页表,需要再defs.h中添加相关函数声明

      // vm.c
      // 添加映射到用户进程的kernel pagetable
      void 
      uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
      {
        if(mappages(pagetable, va, sz, pa, perm) != 0)
          panic("uvmmap");
      }
      
      // like kvminit
      pagetable_t proc_kernelpgtbl(void)
      {
        pagetable_t pagetable = uvmcreate();
        if(pagetable==0)  return 0;
      
        // uart registers
        uvmmap(pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
      
        // virtio mmio disk interface
        uvmmap(pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
      
        // CLINT
        uvmmap(pagetable, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
      
        // PLIC
        uvmmap(pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
      
        // map kernel text executable and read-only.
        uvmmap(pagetable, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);
      
        // map kernel data and the physical RAM we'll make use of.
        uvmmap(pagetable, (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(pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
      
        return pagetable;
      }
      
      // proc.c
      static struct proc*
      allocproc(void)
      {
        // 添加kernel pagetable
        p->kernel_pagetable = proc_kernelpgtbl();
        if (p->kernel_pagetable == 0){
          freeproc(p);
          release(&p->lock);
          return 0;
        }
      }
      
    • 每个kernel pagetable都添加一个对自己kernel stack的映射,未修改前,kernal stack初始化在kernel/proc.c/procinit里,将stack初始化的部分移入proc.c/allocproc函数中,完成对allocproc的修改

      // proc.c
      void
      procinit(void)
      {
        struct proc *p;
        
        initlock(&pid_lock, "nextpid");
        for(p = proc; p < &proc[NPROC]; p++) {
            initlock(&p->lock, "proc");
      
            // Allocate a page for the process's kernel stack.
            // Map it high in memory, followed by an invalid
            // guard page.
      
            // move to allocproc
            // char *pa = kalloc(); // 分配物理页面
            // if(pa == 0)
            //   panic("kalloc");
            // uint64 va = KSTACK((int) (p - proc)); // 分配guard page
            // kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
            // p->kstack = va;
            
        }
        kvminithart();
      }
      
      // proc.c
      static struct proc*
      allocproc(void)
      {
        // 添加kernel pagetables
        // ...
        // 添加kernel stack
        char *pa = kalloc(); // 分配物理页面
        if(pa == 0)
          panic("kalloc");
        uint64 va = KSTACK((int) (p - proc)); 
        // 添加kernel stack的映射到用户的kernel pagetable里
        uvmmap(p->kernel_pagetable,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
        p->kstack = va;
      }
      
    • 修改scheduler函数,在进程进行切换的时候把自己的kernel pagetable(结构体成员proc.kernel_pgtbl)放入到stap寄存器里,让内核使用该进程的pagetable转换地址,然后如果没有进程在运行,使用内核自己的kernel pagetable(全局变量kernel_pagetable)。

      void
      scheduler(void)
      {
        struct proc *p;
        struct cpu *c = mycpu();
        
        c->proc = 0;
        for(;;){
          // Avoid deadlock by ensuring that devices can interrupt.
          intr_on();
          
          int found = 0;
          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;
      
              // 将当前进程的kernel page 存入satp寄存器
              w_satp(MAKE_SATP(p->kernel_pagetable));
              sfence_vma();
      
              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);
          }
      #if !defined (LAB_FS)
          if(found == 0) {
            intr_on();
            asm volatile("wfi");
          }
      #else
          ;
      #endif
        }
      }
      
    • 进程销毁的时候释放kernel_pgtbl,由于kernel stack的初始化原来在procinit函数中,并且放在内核的kernel pagetable中,现在放在了kernel_pgtbl,所以在销毁进程时,需要销毁kernel stack(xv6源码中在内核释放时处理了kernel stack的销毁,因为所有的进程共享一个kernel stack

      static void
      freeproc(struct proc *p)
      {
        if(p->trapframe)
          kfree((void*)p->trapframe);
        p->trapframe = 0;
      
        // 删除kernel stack,在释放内核页表之前
        if (p->kstack)
        {
          pte_t* pte = walk(p->kernel_pagetable, p->kstack, 0);
          if (pte == 0)
            panic("freeproc: kstack");
          kfree((void*)PTE2PA(*pte)); // 释放物理页
        }
        p->kstack = 0;
      
        if(p->pagetable)
          proc_freepagetable(p->pagetable, p->sz);
        p->pagetable = 0;
      
        // 释放内核页表
        if(p->kernel_pagetable)
          proc_freekernelpgtbl(p->kernel_pagetable); // 需要实现
        p->sz = 0;
        p->pid = 0;
        p->parent = 0;
        p->name[0] = 0;
        p->chan = 0;
        p->killed = 0;
        p->xstate = 0;
        p->state = UNUSED;
      }
      
    • 以及在进程销毁时,销毁进程的kernel_pgtbl,仿照freewalk写一个freekernelpgtbl函数用于释放。

      // free kernel pagetable
      // 模仿vm.c中的freewalk,但注意物理地址没有释放
      // 最后一层叶节点没有释放,标志位并没有重置
      // 故需要修改一下
      void proc_freekernelpgtbl(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)){
            pagetable[i] = 0;
            if ((pte & (PTE_R|PTE_W|PTE_X)) == 0)
            {
              uint64 child = PTE2PA(pte);
              proc_freekernelpgtbl((pagetable_t)child);
            }
          } else if(pte & PTE_V){
            panic("proc free kpt: leaf");
          }
        }
        kfree((void*)pagetable);
      }
      //
      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);
      }
      
  2. 共享内核页表

Simplify copyin/copyinstr

Replace the body of copyin in kernel/vm.c with a call to copyin_new (defined in kernel/vmcopyin.c); do the same for copyinstr and copyinstr_new. Add mappings for user addresses to each process’s kernel page table so that copyin_new and copyinstr_new work. You pass this assignment if usertests runs correctly and all the make grade tests pass.
在进程的内核态页表中维护一个用户态页表映射的副本,这样使得内核态也可以对用户态传进来的指针(逻辑地址)进行解引用。

这样做相比原来copyin的实现的优势是,原来的copyin是通过软件模拟访问页表的过程获取物理地址的,而在内核页表内维护映射副本的话,可以利用 CPU 的硬件寻址功能进行寻址,效率更高并且可以受TLB加速。
和原先的区别在于copyin在使用时,va != pacopyin_new使用时,va = pa。这里的等于指MMU翻译后。

  1. 需要辅助函数将用户程序的页表项复制到用户程序的内核页表,注意用户地址的地址空间可以增加也可以缩小
    // 将 proc_pgtbl 页表的一部分页映射关系拷贝到 kernel_pgtbl 页表中。
    // 只拷贝页表项,不拷贝实际的物理页内存。
    void copy_proc_to_kernel(pagetable_t proc_pgtbl, pagetable_t kernel_pgtbl, uint64 oldsz, uint64 newsz){
      
      if(newsz >= PLIC){
        panic("user process exceed PLIC");
      }
    
      for(uint64 va = oldsz; va < newsz; va += PGSIZE){
        pte_t *pte1 = walk(proc_pgtbl, va, 0); 
        if (pte1 == 0) {
          panic("no user pte");
        }
        if ((*pte1 & PTE_V) == 0) {
          panic("no valid user pte");
        }
        // proc_pgtbl 页表中的页表项,无则alloc
        pte_t *pte2 = walk(kernel_pgtbl, va, 1);
        if (pte2 == 0) {
          panic("no kernel pte");
        }
        
        // 复制
        *pte2 = *pte1;
        // 关闭写入和执行权限, 关闭用户权限, 否则kernel无法使用
        *pte2 &= ~(PTE_U|PTE_W|PTE_X);
      }
      // 缩小地址空间的情况
      for (uint64 va = newsz; va < oldsz; va += PGSIZE)
      {
        pte_t *kpte = walk(kernel_pgtbl, va, 1);
        *kpte &= ~PTE_V;
      }
    }
    
  2. 在用户程序分配页表时(userinitexecfrok)或者改变地址空间(sys_sbrk / growproc)的时候同步用户页表到内核页表
    // proc.c
    void
    userinit(void)
    {
      struct proc *p;
    
      p = allocproc();
      initproc = p;
      
      // allocate one user page and copy init's instructions
      // and data into it.
      uvminit(p->pagetable, initcode, sizeof(initcode));
      p->sz = PGSIZE;
    
      // prepare for the very first "return" from kernel to user.
      p->trapframe->epc = 0;      // user program counter
      p->trapframe->sp = PGSIZE;  // user stack pointer
    
      safestrcpy(p->name, "initcode", sizeof(p->name));
      p->cwd = namei("/");
    
      p->state = RUNNABLE;
    
      copy_proc_to_kernel(p->pagetable, p->kernel_pagetable, 0, p->sz); // 复制页表
    
      release(&p->lock);
    }
    int
    fork(void)
    {
      // ...
      np->state = RUNNABLE;
      copy_proc_to_kernel(np->pagetable, np->kernel_pagetable, 0, np->sz);
      release(&np->lock);
    
      return pid;
    }
    // exec.c
    int
    exec(char *path, char **argv)
    {
      // ...
      if(p->pid==1)
        vmprint(p->pagetable);
      copy_proc_to_kernel(p->pagetable, p->kernel_pagetable,0,p->sz);
    
      return argc; // this ends up in a0, the first argument to main(argc, argv)
      // ...
    }
    // proc.c
    int
    growproc(int n)
    {
      uint sz;
      struct proc *p = myproc();
    
      sz = p->sz;
      if(n > 0){
        if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
          return -1;
        }
      } else if(n < 0){
        sz = uvmdealloc(p->pagetable, sz, sz + n);
      }
      copy_proc_to_kernel(p->pagetable, p->kernel_pagetable,p->sz,sz);
      p->sz = sz;
    
      return 0;
    }
    
  3. exec中需要判断sz有无超过PLIC
    // exce.c
    // Load program into memory.
      for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
        if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
          goto bad;
        if(ph.type != ELF_PROG_LOAD)
          continue;
        if(ph.memsz < ph.filesz)
          goto bad;
        if(ph.vaddr + ph.memsz < ph.vaddr)
          goto bad;
        uint64 sz1;
        if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
          goto bad;
        if(sz1 >= PLIC)
          goto bad;
        sz = sz1;
        if(ph.vaddr % PGSIZE != 0)
          goto bad;
        if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
          goto bad;
      }
    

总结

思考题

  1. Explain the output of vmprint in terms of Fig 3-4 from the text. What does page 0 contain? What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?
    根据文本中的图3-4解释vmprint的输出。第0页包含什么?第2页是什么?在用户模式下运行时,进程能否读取/写入第1页映射的内存?
    参考Lecture 7 Q&A for labs
    Ans:
    修改_vmrpint函数

    printf("%d: pte %p pa %p fl %p\n", i, pte, child, (pte<<54)>>54);
    

    打印出来的flag信息如下

    page table 0x0000000087f63000
    ..0: pte 0x0000000021fd7c01 pa 0x0000000087f5f000 fl 0x0000000000000001
    .. ..0: pte 0x0000000021fd7801 pa 0x0000000087f5e000 fl 0x0000000000000001
    .. .. ..0: pte 0x0000000021fd801f pa 0x0000000087f60000 fl 0x000000000000001f
    .. .. ..1: pte 0x0000000021fd740f pa 0x0000000087f5d000 fl 0x000000000000000f
    .. .. ..2: pte 0x0000000021fd701f pa 0x0000000087f5c000 fl 0x000000000000001f
    ..255: pte 0x0000000021fd8801 pa 0x0000000087f62000 fl 0x0000000000000001
    .. ..511: pte 0x0000000021fd8401 pa 0x0000000087f61000 fl 0x0000000000000001
    .. .. ..510: pte 0x0000000021fed807 pa 0x0000000087fb6000 fl 0x0000000000000007
    .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000 fl 0x000000000000000b
    init: starting sh
    

    根据标志位信息,page0具有权限UXWRV,page1具有权限XWRV,page2具有权限UXWRX
    标志位

    结合下图进程地址空间,page0 应该主要是应用程序的代码段和数据段; 而 page2 即对应着用户栈; 中间的 page1 应该是 guard page, 无物理地址实际映射, 用于溢出检测.
    进程地址空间
    除此之外,也可以知道page510具有权限WRV,对应trapframe,page511具有权限XRV,对应trampoline。

  2. Explain why the third test srcva + len < srcva is necessary in copyin_new(): give values for srcva and len for which the first two test fail (i.e., they will not cause to return -1) but for which the third one is true (resulting in returning -1).
    解释为什么在copyin_new()中需要第三个测试srcva+len<srcva:给出前两个测试失败的srcvalen的值(即,它们不会导致返回-1),但第三个为真的值(导致返回-1的结果)。

    // 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_new(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
    {
      struct proc *p = myproc();
    
      if (srcva >= p->sz || srcva+len >= p->sz || srcva+len < srcva)
        return -1;
      memmove((void *) dst, (void *)srcva, len);
      stats.ncopyin++;   // XXX lock
      return 0;
    }
    
    

    Ans: 三个 return -1 的条件分别为 srcva >= p->sz, srcva+len >= p->szsrcva+len < srcva. 很显然, 此处第三个条件主要是进行溢出检测, 防止无符号整数上溢. 由于 srcvalen 均为uint64类型的变量, 当srcva小于p->sz但是len为一个极大的数时, 如0xffff...ffff, 由于无符号整数溢出便可以满足srcva+len < p->sz

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值