MIT 6.S081 Lab Three

news2024/11/16 18:04:53

MIT 6.S081 Lab Three

  • 引言
  • page tables
    • Print a page table (easy)
      • 代码解析
    • A kernel page table per process (hard)
      • 代码解析
    • Simplify copyin/copyinstr(hard)
      • 代码解析
    • 可选的挑战练习


引言

本文为 MIT 6.S081 2020 操作系统 实验三解析。

MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列


page tables

在本实验中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。

开始编码之前,请阅读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() --> init时的页表:

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映射的内存?

代码解析

本实验主要是实现一个打印页表内容的函数, 首先根据提示在exec.c中的return argc之前插入if(p->pid==1) vmprint(p->pagetable)

然后看一下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,我们可以写下递归函数。对于每一个有效的页表项都打印其和其子项的内容。如果不是最后一层的页表就继续递归。通过level来控制前缀..的数量。

void _vmprint(pagetable_t pagetable, int level){
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = pagetable[i];
    // PTE_V is a flag for whether the page table is valid
    if(pte & PTE_V){
      // 按照层级输出.. .. .. 
      for (int j = 0; j < level; j++){
        if (j) printf(" ");
        printf("..");
      }
      // 得到pte指向的物理页起始地址
      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.
        _vmprint((pagetable_t)child, level + 1);
      }
    }
  }
}

void vmprint(pagetable_t pagetable){
  //打印根页表起始地址
  printf("page table %p\n", pagetable);
  _vmprint(pagetable, 1);
}
  • 添加vmprint函数定义到kernel/defs.h里面。
// 添加到vm.c下
void            vmprint(pagetable_t);
  • make qemu启动系统测试
    在这里插入图片描述

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.ckernel/proc.c中这样做(但不要修改kernel/vmcopyin.c, kernel/stats.c, user/usertests.c, 和user/stats.c
  • 页表映射丢失很可能导致内核遭遇页面错误。这将导致打印一段包含sepc=0x00000000XXXXXXXX的错误提示。你可以在kernel/kernel.asm通过查询XXXXXXXX来定位错误。

代码解析

注意一点: 实验二的要求是让每个进程在内核中执行时,使用自己的页表副本

本实验主要是让每个进程都有自己的内核页表,这样在内核中执行时使用它自己的内核页表的副本。

(1). 首先给kernel/proc.h里面的struct proc加上内核页表的字段。

uint64 kstack;               // Virtual address of kernel stack
uint64 sz;                   // Size of process memory (bytes)
pagetable_t pagetable;       // User page table
pagetable_t kernelpt;      // 进程的内核页表
struct trapframe *trapframe; // data page for trampoline.S

(2). 在vm.c中添加新的方法proc_kpt_init,该方法用于在allocproc 中初始化进程的内核页表。这个函数还需要一个辅助函数uvmmap,该函数和kvmmap方法几乎一致,不同的是kvmmap是对Xv6的内核页表进行映射,而uvmmap将用于进程的内核页表进行映射。


// Just follow the kvmmap on vm.c
void
uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
  //与kvmmap区别就在于 mappages传入的pagetable不是固定的kernel_pagetable
  if(mappages(pagetable, va, sz, pa, perm) != 0)
    panic("uvmmap");
}

// Create a kernel page table for the process
//整个函数代码可以参考kvminit
pagetable_t
proc_kpt_init(){
  //为当前进程创建一个空的页表
  pagetable_t kernelpt = uvmcreate();
  if (kernelpt == 0) return 0;
  uvmmap(kernelpt, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  uvmmap(kernelpt, CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  uvmmap(kernelpt, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  uvmmap(kernelpt, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  uvmmap(kernelpt, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  uvmmap(kernelpt, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
  return kernelpt;
}

添加proc_kpt_init和uvmmap函数定义到kernel/defs.h里面。

// 添加到vm.c下
void            uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm);
pagetable_t     proc_kpt_init();

然后在kernel/proc.c里面的allocproc调用。

// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
  struct proc *p;
  //遍历进程数组,找到一个UNUSED槽
  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;

//找到了空闲的进程槽
found:
  //分配进程槽
  p->pid = allocpid();

  // Allocate a trapframe page.
  // 分配trapframe页面
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  // 为当前进程分配一个空的用户态根页表
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  
  //----------我们补充的代码-----------------
  //为当前进程分配一个空的内核态根页表
  p->kernelpt = proc_kpt_init(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }
  //-----------补充结束----------------------

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE;

  return p;
}

(3). 根据提示,为了确保每一个进程的内核页表都关于该进程的内核栈有一个映射。我们需要将procinit方法中相关的代码迁移到allocproc方法中。很明显就是下面这段代码,将其剪切到上述内核页表初始化的代码后。

  //----------我们补充的代码-----------------
  // 为当前进程分配一个空的内核态根页表
  p->kernelpt = proc_kpt_init(p);
  if (p->pagetable == 0)
  {
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // 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(p->kernelpt, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  p->kstack = va;
  //-----------补充结束----------------------

在procinit方法中,会将每个进程的内核栈映射到全局内核页表中,这部分的逻辑移除,每个进程的内核栈由自身的内核页表进行维护:

在这里插入图片描述


(4). 我们需要修改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

// Store kernel page table to SATP register
void
proc_inithart(pagetable_t kpt){
  w_satp(MAKE_SATP(kpt));
  sfence_vma();
}

添加proc_inithart函数定义到kernel/defs.h里面。

void            proc_inithart(pagetable_t kpt);

然后在scheduler()内调用即可,但在结束的时候,需要切换回原先的kernel_pagetable。直接调用调用上面的kvminithart()就能把Xv6的内核页表加载回去。

...
p->state = RUNNING;
c->proc = p;

// Store the kernal page table into the SATP
proc_inithart(p->kernelpt);

swtch(&c->context, &p->context);

// Come back to the global kernel page table
kvminithart();
...

(5). 在freeproc中释放一个进程的内核页表。首先释放页表内的内核栈,调用uvmunmap可以解除映射,最后的一个参数(do_free)为一的时候,会释放实际内存。

// free the kernel stack in the RAM
uvmunmap(p->kernelpt, p->kstack, 1, 1);
p->kstack = 0;

然后释放进程的内核页表,先在kernel/proc.c里面添加一个方法proc_freekernelpt。如下,历遍整个内核页表,然后将所有有效的页表项清空为零。如果这个页表项不在最后一层的页表上,需要继续进行递归。

void
proc_freekernelpt(pagetable_t kernelpt)
{
  // similar to the freewalk method
  // there are 2^9 = 512 PTEs in a page table.
  for(int i = 0; i < 512; i++){
    pte_t pte = kernelpt[i];
    if(pte & PTE_V){
      kernelpt[i] = 0;
      if ((pte & (PTE_R|PTE_W|PTE_X)) == 0){
        uint64 child = PTE2PA(pte);
        proc_freekernelpt((pagetable_t)child);
      }
    }
  }
  kfree((void*)kernelpt);
}

注意: 当进程销毁时,我们只会释放用户态页表映射的物理内存,而内核态页表映射的物理内存不用释放,因为内核态映射的物理内存是和内核进程,以及其他所有进程共享的。
在这里插入图片描述
(6). 修改vm.c中的kvmpa,将原先的kernel_pagetable改成myproc()->kernelpt,使用进程的内核页表。

//注意添加下面这两个头文件
#include "spinlock.h" 
#include "proc.h"

// 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(myproc()->kernelpt, va, 0); // 修改这里
  if(pte == 0)
    panic("kvmpa");
  if((*pte & PTE_V) == 0)
    panic("kvmpa");
  pa = PTE2PA(*pte);
  return pa+off;
}

之所以修改kvmpa函数。是因为kvmpa用来将当前进程内核栈虚拟地址翻译为物理地址,由于我们已经将进程内核栈映射交给了各个进程的内核页表管理,所以这里我们需要将原本的全局页表替换为各个进程自己的内核页表:
在这里插入图片描述

(8). 测试一下我们的代码,先跑起qemu,然后跑一下usertests。这部分耗时会比较长。

$ make qemu

> usertests

在这里插入图片描述


Simplify copyin/copyinstr(hard)

内核的copyin函数读取用户指针指向的内存。它通过将用户指针转换为内核可以直接解引用的物理地址来实现这一点。这个转换是通过在软件中遍历进程页表来执行的。在本部分的实验中,您的工作是将用户空间的映射添加到每个进程的内核页表(上一节中创建),以允许copyin(和相关的字符串函数copyinstr)直接解引用用户指针。

YOUR JOB

  • 将定义在kernel/vm.c中的copyin的主题内容替换为对copyin_new的调用(在kernel/vmcopyin.c中定义);
  • copyinstrcopyinstr_new执行相同的操作。
  • 为每个进程的内核页表添加用户地址映射,以便copyin_newcopyinstr_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:给出srcvalen值的例子,这样的值将使前两个测试为假(即它们不会导致返回-1),但是第三个测试为真 (导致返回-1)。

代码解析

本实验是实现将用户空间的映射添加到每个进程的内核页表,将进程的用户态页表复制一份到进程的内核态页表就好。

首先添加复制函数。需要注意的是,在内核模式下,无法访问设置了PTE_U的页面,所以我们要将其移除。

//vm.c
void
// 复制进程想用户态页表到内核态页表中
u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz){
  pte_t *pte_from, *pte_to;
  //向上对齐
  oldsz = PGROUNDUP(oldsz);
  for (uint64 i = oldsz; i < newsz; i += PGSIZE){
    //在用户态页表中定位虚地址关联的PTE
    if((pte_from = walk(pagetable, i, 0)) == 0)
      panic("u2kvmcopy: src pte does not exist");
    //在内核态页表中为当前虚地址创建好对应的映射关系---最后一个参数传入的是1
    //表示当pte映射关系没建立时,进行初始化,而不是直接返回0
    if((pte_to = walk(kernelpt, i, 1)) == 0)
      panic("u2kvmcopy: pte walk failed");
    //获取用户态页表中虚地址对应物理地址  
    uint64 pa = PTE2PA(*pte_from);
    //获取用户态PTE的权限,设置U位为0 
    uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);
    //将物理地址转换为PTE,同时设置权限信息
    *pte_to = PA2PTE(pa) | flags;
  }
}

将u2kvmcopy函数定义添加到defs.h头文件中:

void            u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz);

然后在内核更改进程用户映射的每一处 (fork(), exec(), 和sbrk()),都复制一份到进程的内核页表。

当我们更改当前进程的用户态页表映射时,我们需要将更改同步到当前进程的内核态页表中

  • exec()
int
exec(char *path, char **argv){
  ...
  sp = sz;
  stackbase = sp - PGSIZE;

  // 添加复制逻辑
  u2kvmcopy(pagetable, p->kernelpt, 0, sz);

  // Push argument strings, prepare rest of stack in ustack.
  for(argc = 0; argv[argc]; argc++) {
  ...
}
  • fork():
int
fork(void){
  ...
  // 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->kernelpt, 0, np->sz);
  ...
}
  • sbrk(), 在kernel/sysproc.c里面找到sys_sbrk(void),可以知道只有growproc是负责将用户内存增加或缩小 n 个字节。以防止用户进程增长到超过PLIC的地址,我们需要给它加个限制。
int
growproc(int n)
{
  uint sz;
  struct proc *p = myproc();

  sz = p->sz;
  if(n > 0){
    // 加上PLIC限制
    if (PGROUNDUP(sz + n) >= PLIC){
      return -1;
    }
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    // 复制一份到内核页表
    u2kvmcopy(p->pagetable, p->kernelpt, sz - n, sz);
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
  }
  p->sz = sz;
  return 0;
}

然后替换掉原有的copyin()copyinstr()

原有的实现:

// 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);
    // 通过遍历pagetable将用户态下的虚拟地址转换为物理地址返回
    pa0 = walkaddr(pagetable, va0);
    if(pa0 == 0)
      return -1;  
    n = PGSIZE - (srcva - va0);
    if(n > len)
      n = len;
    // 从物理地址  pa0 + (srcva - va0) 起始拷贝n个字节到dst
    // 对于内核态来说,由于采用的是等价映射,所以物理地址和虚拟地址没有区别
    memmove(dst, (void *)(pa0 + (srcva - va0)), n);
    // 往前推进,直到把所有数据copy完成
    len -= n;
    dst += n;
    srcva = va0 + PGSIZE;
  }
  return 0;
}

// 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
// copy带有'\0'结束符号的字符串
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;
    // 整体实现思路和 copyin 一致
    //不同之处在于,由于事先不清楚copy数据的长度,只能一个个字节的copy,边copy边判断是否到达字符串末尾
    char *p = (char *) (pa0 + (srcva - va0));
    while(n > 0){
      if(*p == '\0'){
        *dst = '\0';
        got_null = 1;
        break;
      } else {
        // 由于内核态下采用的是等价映射,所以才可以直接这样玩
        //毕竟dst代表内核态的虚拟地址,而p代表物理地址
        *dst = *p;
      }
      --n;
      --max;
      p++;
      dst++;
    }
    srcva = va0 + PGSIZE;
  }
  if(got_null){
    return 0;
  } else {
    return -1;
  }
}

替换后新的copyin和copyinstr实现;

//vm.c
// 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)
{
  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)
{
  return copyinstr_new(pagetable, dst, srcva, max);
}

copyin_new和copyinstr_new函数实现在vmcopyin.c文件中:
在这里插入图片描述
在这里插入图片描述

并且添加到 kernel/defs.h

// vmcopyin.c
int             copyin_new(pagetable_t, char *, uint64, uint64);
int             copyinstr_new(pagetable_t, char *, uint64, uint64);

测试:

make qemu

在这里插入图片描述


可选的挑战练习

  1. 分析RISC-V的设备树以找到计算机拥有的物理内存量。
  2. 编写一个用户程序,通过调用sbrk(1)为其地址空间增加一个字节。运行该程序并研究调用sbrk之前和调用sbrk之后该程序的页表。内核分配了多少空间?新内存的PTE包含什么?
  3. 修改xv6来为内核使用超级页面。
  4. 修改xv6,这样当用户程序解引用空指针时会收到一个异常。也就是说,修改xv6使得虚拟地址0不被用户程序映射。
  5. 传统上,exec的Unix实现包括对shell脚本的特殊处理。如果要执行的文件以文本#!开头, 那么第一行将被视为解释此文件的程序来运行。例如,如果调用exec来运行myprog arg1,而myprog的第一行是#!/interp,那么exec将使用命令行/interp myprog arg1运行 /interp。在xv6中实现对该约定的支持。
  6. 为内核实现地址空间随机化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/656852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

shardingsphere第一课-前置课程-Mysql的集群搭建以及多数据源管理

一.Mysql的集群搭建(使用docker搭建省事) 1、关闭防火墙&#xff0c;重启docker** #关闭docker systemctl stop docker #关闭防火墙 systemctl stop firewalld #启动docker systemctl start docker2.1、准备主服务器 解释&#xff1a; 端口号是3306&#xff0c; 指定宿主机配…

FPGA基础知识-时序和延迟

目录 学习目标&#xff1a; 学习内容&#xff1a; 1.延迟模型的类型 2.路径延迟建模 3.时序检查 4.延迟反标注 学习时间&#xff1a; 学习总结 学习目标&#xff1a; 提示&#xff1a;这里可以添加学习目标 鉴别Verilog 仿真中用到的延迟模型的类型&#xff0c;分布延…

YOLOv5改进系列(10)——替换主干网络之GhostNet

【YOLOv5改进系列】前期回顾: YOLOv5改进系列(0)——重要性能指标与训练结果评价及分析 YOLOv5改进系列(1)——添加SE注意力机制

项目中还不会用SpringSecurity,看这篇文章就够了

安全管理是Java应用开发中无法避免的问题&#xff0c;随着Spring Boot和微服务的流行&#xff0c;Spring Security受到越来越多Java开发者的重视&#xff0c;究其原因,还是沾了微服务的光。作为Spring家族中的一员,其在和Spring家族中的其他产品如SpringBoot、Spring Cloud等进…

client-go的Indexer三部曲之而:性能测试

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 本文是《client-go的Indexer》系列的第二篇&#xff0c;在前文咱们通过实例掌握了client-go的Indexer的基本功能&#xff0c;本篇咱们尝试对下面这…

6.pixi.js编写的塔防游戏(类似保卫萝卜)-游戏资源打包逻辑

游戏说明 一个用pixi.js编写的h5塔防游戏&#xff0c;可以用electron打包为exe&#xff0c;支持移动端&#xff0c;也可以用webview控件打包为app在移动端使用 环境说明 cnpm6.2.0 npm6.14.13 node12.22.7 npminstall3.28.0 yarn1.22.10 npm config list electron_mirr…

配置legacyUnhandledExceptionPolicy属性防止处理异常后程序崩溃退出(C#)

这是这篇文章后面遗留的问题&#xff1a; winform中的全局异常信息_winform全局异常捕获_zxy2847225301的博客-CSDN博客 就是线程抛出异常后&#xff0c;被AppDomain.CurrentDomain.UnhandledException注册的事件捕获后&#xff0c;程序依旧崩溃退出。 解决方案&#xff1a;在…

架构-嵌入式模块

章节架构 约三分&#xff0c;主要为选择题 #mermaid-svg-z6RGCDSEQT5AhE1p {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-z6RGCDSEQT5AhE1p .error-icon{fill:#552222;}#mermaid-svg-z6RGCDSEQT5AhE1p .error-text…

【产品经理】从用户体验五要素出发,谈如何设计与体验一款产品

用户体验五要素是产品人必备的知识技能&#xff0c;由于网络碎片化的了解&#xff0c;往往容易造成其高深莫测&#xff0c;晦涩难懂的形象&#xff0c;进而对其束之高阁。但实质不过是一种产品分析与设计的方法论&#xff0c;正确姿势去了解它能帮助我们更好地理解一款产品和从…

移动端小于12px的字体处理方法

今天在按设计稿坐页面时&#xff0c;遇到了下图的情况 ​​​​​ 由于浏览器对字体最小为12px的限制&#xff0c;所以我查阅资料后尝试使用transform:scale来处理 代码如下&#xff1a; <div class"icon"><span class"iconfont icon-a-xuexi3 icon-…

ZYNQ——PL端流水灯的实现

文章目录 一、介绍二、代码编写三、引脚分配四、仿真分析五、添加 ILA IP六、板上验证 一、介绍 本文介绍的是在ZYNQ 7020黑金开发板上实现PL端流水灯的例子&#xff0c;开发板上PL端的LED灯总共有4个&#xff0c;在原理图中找到 PL LED 如下图所示&#xff0c;通过看图可知&a…

【MarkDown】CSDN Markdown之四象限图quadrantChart详解

四象限图 四象限图是一种将数据分为四个象限的可视化方法。它用于在二维网格上绘制数据点&#xff0c;其中一个变量表示x轴&#xff0c;另一个变量表示y轴。根据针对正在分析的数据集的一组标准&#xff0c;将图表分成四个相等的部分来确定四个象限。经常使用四象限图来识别数…

父亲节|祝天下所有父亲节日快乐,长寿安康!

父亲节&#xff0c;是一个感谢父亲的节日。普遍认为的日期是每年6月的第三个星期日&#xff0c;在这一天世界上有52个国家和地区在过父亲节。同时注重孝道也是我们中华民族的传统文化。 在这个感恩的节日里 把最真诚美好的祝福 送给天下所有的父亲们 祝福他们 节日快乐&…

OpenAI 大模型生态

目录标题 1. 语言类大模型2. 图像多模态大模型3. 语音识别模型4. 文本向量化模型5. 审查模型6. 编程大模型1. 语言类大模型 包括GPT-3、GPT-3.5、GPT-4系列模型。并且,OpenAl在训练GPT-3的同时,训练了参数不同、复杂度各不相同的A、B、C、D四项大模型(基座模型),用于不同场景…

mysql 集群 MGR

mysql安装&#xff08;3台服务&#xff09; 1下载 wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz 2解压mysql wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.11-linux-glibc2.12-x86_64.tar.gz tar -zxvf…

键盘按键事件 通过键盘上下左右按键移动界面上图标

#main.c文件 #include “keyevent.h” #include int main(int argc, char *argv[]) { QApplication a(argc, argv); KeyEvent w; w.show(); return a.exec();} #include “keyevent.h”//头文件 #ifndef KEYEVENT_H #define KEYEVENT_H #include #include #include cl…

Windows安装Make工具(make.exe和mingw)

1、make.exe 官网介绍&#xff1a;Make是一个工具&#xff0c;它控制从程序的源文件生成程序的可执行文件和其他非源文件。Make从一个名为Makefile的文件中获取如何构建程序的知识&#xff0c;该文件列出了每个非源文件以及如何从其他文件中计算它。当你编写一个程序时&#x…

[性能测试工具]——Loadrunner的使用及安装指南

目录 一、基本概念 1.1 什么是性能测试&#xff1f; 1.2 性能测试和功能测试的区别&#xff1f; 1.3 什么样的软件属于性能好&#xff0c;什么样的软件属于性能不好&#xff1f; 1.4 哪些因素会影响到软件性能&#xff1f; 二、为什么对软件进行性能测试&#xff1f; 三、性…

压测工具Jmeter学习

压测工具Jmeter Jmeter介绍 Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试&#xff0c;它最初被设计用于Web应用测试&#xff0c;但后来扩展到其他测试领域。 它可以用于测试静态和动态资源&#xff0c;例如静态文件、Java 小服务程序、CGI …

Django基础入门④:数据表显示和Django模板详讲

Django基础入门④&#xff1a;数据表显示和Django模板详讲 数据表显示字段显示表单显示分块内联显示列表显示搜索栏目创建筛选排序分页导入导出 Django模板什么是模板模板使用render方法详解 &#x1f3d8;️&#x1f3d8;️个人简介&#xff1a;以山河作礼。 &#x1f396;️&…