MIT6.828实验记录-Lab2

news2024/11/24 3:22:52

实验1完成了内核启动的相关操作,由实验1可知,首先BIOS(0xf0000~0x100000=64KB)将Boot Loader加载到0x7c00~0x7dff(512B)处,Boot Loader代码执行后将内核代码的ELF文件读取到0x100000其实的内存(1MB)

实验2为当前的操作系统编写内存管理相关代码。实验要求完成两个组件:

第一个组件是内核的物理内存分配器,使内核能够分配和释放内存。分配器将以4KB为单位分配物理内存,称为页。主要任务是维护数据结构,记录哪些物理页面是空闲的,哪些已分配,以及有多少进程正在共享每个分配的页面。

内存管理的第二个组件是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。x86硬件的内存管理单元 (MMU) 在指令使用内存时执行映射,查询一组页表。根据提供的规范修改JOS以设置MMU的页表。

进行实验前首先需要知道实验中需要参考以下几个文件:

  • inc/memlayout.h
  • kern/pmap.c
  • kern/pmap.h
  • kern/kclock.h
  • kern/kclock.c

memlayout.h描述了必须通过修改pmap.c来实现的虚拟地址空间的布局。

memlayout.h和pmap.h定义了PageInfo用于跟踪哪些物理内存页面空闲的结构。

kclock.c和kclock.h 操作PC的电池供电时钟和CMOS RAM硬件(BIOS记录PC 包含的物理内存量等)。

pmap.c中的代码需要读取这个设备硬件,以便计算出有多少物理内存。

2.1 Part 1:物理页管理

操作系统必须跟踪物理RAM的哪些部分是空闲的,哪些部分当前正在使用。JOS以4KB为单位管理PC的物理内存, 以便它可以使用MMU来映射和保护每块分配的内存。编写物理页面分配器。它通过一个struct PageInfo对象链接列表来跟踪哪些页面是空闲的(与xv6不同的是,它们没有嵌入到空闲页面本身中),每个页面对应一个物理页面。需要先编写物理页分配器,然后才能编写其余的虚拟内存实现,因为页表管理代码需要分配物理内存来存储页表。

练习1要求实现文件中相关函数的代码,下面将按照函数给出的顺序进行代码编写:

  • boot_alloc函数,仅在JOS设置物理内存时使用。直接根据函数提示进行编写,有一点就是关于页面对齐,这一功能主要借助ROUNDUP函数实现,具体如下:

static void *
boot_alloc(uint32_t n)
{
    static char *nextfree;  // virtual address of next byte of free memory
    char *result;
    
    //cprintf("The value of nextfree before init = %08x\n",nextfree);
    // Initialize nextfree if this is the first time.
    // 'end' is a magic symbol automatically generated by the linker,
    // which points to the end of the kernel's bss segment:
    // the first virtual address that the linker did *not* assign
    // to any kernel code or global variables.
    if (!nextfree) {
        extern char end[];
        nextfree = ROUNDUP((char *) end, PGSIZE);
    }
    /** 
        ROUNDUP(a, b) :inc/x86.h-->inc/x86.h-->inc/types.h
        Round up a to the nearest integral multiple of B
    */
    //cprintf("The value of nextfree after init = %08x\n",nextfree);
   // Allocate a chunk large enough to hold 'n' bytes, then update
    // nextfree.  Make sure nextfree is kept aligned
    // to a multiple of PGSIZE.
    // LAB 2: Your code here.
    if(n == 0)
        return nextfree;
    if(n > 0){
        if(PADDR(ROUNDUP((char *)((uint32_t)nextfree + n), PGSIZE)) < npages * PGSIZE){
            result = nextfree;
            nextfree = ROUNDUP((char *)((uint32_t)nextfree + n), PGSIZE);
            return result;
        }else
            panic("boot_alloc: out of memory\n");
    }
    return NULL;
}
  • mem_init() 函数,练习1只要求到check_page_free_list(1)这一句,首先调用boot_alloc分配npages个PageInfoge大小的内存,然后再将分配的内存清空。具体如下:
//
    // Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
    // The kernel uses this array to keep track of physical pages: for
    // each physical page, there is a corresponding struct PageInfo in this
    // array.  'npages' is the number of physical pages in memory.  Use memset
    // to initialize all fields of each struct PageInfo to 0.
    // Your code goes here:
    pages=(struct PageInfo *)boot_alloc(npages*sizeof(struct PageInfo));
    memset(pages,0,npages*sizeof(struct PageInfo));
    //
    // Now that we've allocated the initial kernel data structures, we set
    // up the list of free physical pages. Once we've done so, all further
    // memory management will go through the page_* functions. In
    // particular, we can now map memory using boot_map_region
    // or page_insert
    page_init();
    check_page_free_list(1);
  • page_init()函数,主要完成页面的初始化过程,设置空闲链表。结合注释和实验1中的内存物理地址空间图,如下:

  • 分析得到下列要点:
    • 物理内存中第0页,需要标记为已使用,目的是为了保留实模式IDT(中断描述表)和BIOS结构。也就是说初始化需要从第1页开始进行。
    • 从第1页到地址640KB(640KB/4KB=160,也就是代码中npages_basemem的值)之间(不含640K,也就是说到0x9FFFF)的区域是空闲的,可以进行初始化。
    • 640KB(0xA0000)到1M(0x100000)之间也不能用,这一部分是为了留给BIOS以及显存,大于1MB的部分,有一部分分配给了内核,需要计算出来,除去内核占用的部分之后的内存均可以初始化。关于内核之后的首个空闲页的计算要结合boot_alloc注解,(if n==0, returns the address of the next free page without allocating),最终代码实现如下:
page_init(void)
{
    size_t i;
    //npages_basemem = PGNUM(IOPHYSMEM); 
    for (i = 1; i < npages_basemem; i++) {
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
    //Count the last available/free page after kernel
    size_t kern_page = PGNUM(PADDR(boot_alloc(0)));
    //cprintf("kern_page = %08x\n",kern_page);
    for(i = kern_page; i < npages; i++){
        pages[i].pp_ref = 0;
        pages[i].pp_link = page_free_list;
        page_free_list = &pages[i];
    }
}
  • page_alloc()函数,完成的主要工作是从空闲链表(page_free_list)取第1个空闲页,并更新链表头指向下一个空闲页位置,如果指定了alloc_flag,则将PageInfo结构对应的那4KB内存区域清零,需要用page2kva(page)获取对应页面的虚拟地址。代码如下:
struct PageInfo *
page_alloc(int alloc_flags)
{
    // Fill this function 
   /**
        page2kva(page);//Turn page address to kernel virtual address.
    */
    if(!page_free_list)
        return NULL;
    struct PageInfo *newpage = page_free_list;
    page_free_list = page_free_list -> pp_link;
    if(alloc_flags & ALLOC_ZERO)
        memset(page2kva(newpage), 0, PGSIZE);//Why memset virtual memory?
    newpage->pp_link = NULL;
    newpage->pp_ref = 0;
    return newpage;
}
  • page_free()函数,实现了将空闲页插入到空闲页表头的操作。代码如下:
//
// Return a page to the free list.
// (This function should only be called when pp->pp_ref reaches 0.)
//
void
page_free(struct PageInfo *pp)
{
    // Fill this function in
    // Hint: You may want to panic if pp->pp_ref is nonzero or
    // pp->pp_link is not NULL.
    if(pp->pp_ref != 0 || pp->pp_link != NULL)
        panic("page_free: pp_ref is nonzero or pp_link is not NULL\n");
    pp->pp_link = page_free_list;
    page_free_list = pp;
}

一开始忘记对page_alloc中的分配的页pp_link域置空(newpage->pp_link=NULL; newpage->pp_ref=0;)

出现了grade_lab2测试报错:

由于没有将页的链域置空导致page_free();回收空闲页中的对页面链域断言不通过。改正后,使用grade_lab2进行检测,结果如下(仅完成了物理页分配):

2.2 Part 2: 虚拟内存管理

练习2主要是阅读Intel 80386 Reference Manual中的第五章和第六章,熟悉内存保护模式的管理架构。其中,第五章主要讲解了80386计算机的地址转换过程:

  • 段转换:将逻辑地址(段选择器和段偏移量)转换为线性地址。
  • 页转换:将线性地址转换为物理地址。

转换过程体现在下面图中:

结合上图分析:

实验中用到的C指针其实就是虚拟地址中的offset,通过描述图表(DESCRIPTOR TABLE)和段选字(SELECTOR),通过分段机制转换为线性地址,因为jos中设置段基址为0,所以线性地址就等于offset。在开启分页前线性地址就是物理地址。开启分页后,线性地址需要经过MMU部件进行翻译才能得到物理地址。开启分页后,MMU部件会把线性地址分成3部分,分别是页目录索引(Directory)、页表索引(Table)和页内偏移(offset), 这3个部分把原本32位的线性地址分成了10+10+12的3个片段。每个页表的大小为4KB(因为页内偏移为12位)。如下图所示(位于inc/mmu.h中)

实验1第3部分中指出只映射了前4MB的物理内存,因为这足以让系统启动运行。总共创建1024个目录项只用了两个,每个页目录大小为4B,总共占用4KB内存。页目录项的结构和页表项的结构基本一致,高20位为物理页索引(ppn),用于定位页表物理地址,通过页表物理地址和页内偏移就可以找到物理地址,所以页目录(页表)项的低12位可以用于一些标识和权限控制。

系统在访问一个页面时就会自动地去判断页表的这些位,如果页面不存在或者权限不符,就会产生异常。在启用分页前,页目录所在的物理页面的首地址存放到CR3寄存器中,x86处理器在进行页式地址转换时会自动地从CR3中取得页目录物理地址,然后根据线性地址的高10位取页目录项,由页目录项所存储的地址(高20位)得到页表所在物理页的首地址。然后根据中间10位取得页表项,由页表项所存储的地址(高20位)找到物理页起始地址(Page Frame),将该地址 + 12位页内偏移得到真正的物理地址。

为了帮助检测和识别错误,80386包含验证存储器访问和指令执行是否符合保护标准的机制。这一部分内容主要在第6章中展开讲解。保护机制主要体现在五个方面:类型检查、限制检查、可寻址域的限制、程序入口点的限制、指令集限制。其中,与页面级保护有关的是:可寻址域的限制类型检查

在可寻址域的限制方面,页面特权的概念是通过将每个页面分配到两个级别之一来实现的:

    • 主管级别(U/S=0)——用于操作系统和其他系统软件及相关数据。
    • 用户级别(U/S=1)——用于应用程序和数据。

当前级别(U 或 S)与 CPL(描述符中一个描述特权级别的字段) 相关。如果 CPL 为 0、1 或 2,则处理器在主管级别执行。如果 CPL 为 3,则处理器在用户级别执行。当处理器在主管级别执行时,所有页面都是可寻址的,但是,当处理器在用户级别执行时,只有属于用户级别的页面是可寻址的。

在类型检查方面,系统在页寻址级别,定义了两种类型:

    • 只读访问 (R/W=0)
    • 读/写访问(R/W=1)

当处理器在主管级别执行时,所有页面都是可读和可写的。当处理器在用户级执行时,只有属于用户级并被标记为读写访问的页面是可写的;属于主管级别的页面从用户级别既不可读也不可写。(细节详见手册)

练习3要求熟悉qemu调试指令,在进入qemu调试时,先在ubuntu的terminal中执行make qemu-gdb(方便调试)之后弹出qemu窗体,再点击进入terminal按下ctrl+a松开再按c,弹出(qemu)则正常进入调试,如下所示:

按照练习要求,使用qemu调试模式下执行xp指令,然后在gdb中执行x指令,对结果进行观察,看结果是否一致。结果如下:

由上图结果可以看出,两个命令在执行结果上是一致的。

同样,练习3还指出qemu还提供了info pg(显示当前页表,详细的表示,包括映射的内存范围、权限和标志),info mem(显示映射了哪些虚拟地址范围以及具有哪些权限的概述),结果如下:

因为在0x7c00处,刚刚进入bootloader,此时还未开启分页,所以直接跳转到0x100000处,再次执行info pg指令,结果如下:

注:

  • PDE(Page Directory Entry)、PTE(Page Table entry)
  • D —— Dirty,是否被修改;
  • A —— Accessed,最近是否被访问;
  • P —— Present,判断对应物理页面是否存在,存在为1,否则为0;
  • W —— Write,该位用来判断对所指向的物理页面是否可写,1可写,0不可写;

执行info men指令的结果如下:

注:根据实验1中的第3部分中的注解也可以理解上图的信息,在kern/entry.S设置CR0_PG标志之前(未进入保护模式),内存引用被视为物理地址(进入保护模式前线性地址与物理地址是相同的)。一旦CR0_PG设定,内存引用是得到由虚拟内存硬件到物理地址转换的虚拟地址。 entry_pgdir将 0xf0000000 到 0xf0400000 范围内的虚拟地址转换为物理地址 0x00000000 到 0x00400000,并将虚拟地址 0x00000000 到 0x00400000 转换为物理地址 0x00000000 到 0x00400000。

JOS内核通常将地址作为不透明值或整数进行操作,而不是对地址进行解引用,例如在物理内存分配器中,有时是虚拟地址,有时是物理地址。为了区分地址类型,JOS源区分了两种情况:类型uintptr_t表示虚拟地址、physaddr_t表示物理地址。这两种类型实际上只是32位整数 ( uint32_t) 的同义词,因此编译器不会阻止将一种类型分配给另一种类型。由于它们是整数类型(不是指针),如果尝试进行解引用,编译器会报错。

JOS内核可以通过将uintptr_t投影为指针类型对它进行解引用相比较而言,因为内存管理单元(MMU)会对所有地址进行解引用,所以,内核无法对一个物理地址进行解引用。如果将physaddr_t转换为指针并对它进行解引用,可能能够加载并存储到结果地址(硬件会将其解释为虚拟地址),但无法获得预期的内存位置。

Q:假设下面的 JOS 内核代码是正确的,变量x应该是什么类型,uintptr_t还是physaddr_t?

mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;

A:根据return_a_pointer()函数判断,value是一个指针类型,是一个虚拟地址,x是vlaue的一个解引用,因此可以判断x是uintptr_t类型。(上面标为蓝色的那句话)

JOS内核无法绕过虚拟地址转换,因此无法直接加载和存储到物理地址。为了读取和写入它知道物理地址的内存,内核需要重新映射从物理地址0开始到虚拟地址0xf0000000的所有物理内存。物理地址到虚拟地址的转换其实就是在物理地址上加上0xf0000000,这个加法的实现是借助kern/pmap.h中的KADDR(pa)宏实现的。

给定存储内核数据结构的内存的虚拟地址,JOS内核有时也需要能够找到物理地址。内核全局变量和boot_alloc()分配的内存位于加载内核的区域,从0xf0000000开始,也就是映射所有物理内存的区域。因此,要将这个区域中的虚拟地址转换为物理地址,内核可以简单地减去0xf0000000。这一减法是通过kern/pmap.h中的PADDR(va) 完成的。

实验中,可能需要将同一个物理页面映射到多个虚拟地址(或多个地址的环境中),物理页面被引用的次数将会记录到pp_ref字段中。

练习4要求完成相关函数代码,实现对页表的管理,包括:插入和删除线性到物理的映射,在需要页表时进行创建,并完成页面检查。

pgdir_walk():根据虚拟地址找到页表项地址。当物理页面不存在时,如果指定了create标志,分配新的页,并设置页目录项为新分配的页的地址。

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    pde_t *pde;
    pte_t *pte;
    pde = &pgdir[PDX(va)];//Get page directory entry 
    if(!(*pde & PTE_P)){    //Page directory not exists
        struct PageInfo *new_page = page_alloc(ALLOC_ZERO);
        if(!new_page || !create)
            return NULL;
        new_page->pp_ref++;
        //*pde = page2pa(new_page) | PTE_P | PTE_U;  //Omit PTE_W see below
        *pde = page2pa(new_page) | PTE_P | PTE_U | PTE_W;//Trun new_page to pde
    }
    if(PTE_ADDR(*pde) < npages * PGSIZE){
        pte = (pte_t *)KADDR(PTE_ADDR(*pde));//Page table entry
        return &pte[PTX(va)];
    }
    return NULL;
}

对于入口标志的检验采用&操作,即,将要检测的页目录入口低12位(假设二进制为000000000101)与PTE_P(inc/mmu.h中为0x001,转换为而二进制000000000001)进行与操作后,得到000000000001,仅保留要检测的第一位的信息,为1,证明标志位P为1。

对页目录入口标志进行设置时,采用|操作,即,将要检测的页目录入口低12位(假设二进制为000000000100)与PTE_P(inc/mmu.h中为0x001,转换为而二进制000000000001)进行或操作后,得到000000000101,完成对入口地址中标志位PTE_P的设置。

boot_map_region():将在[va, va+size)范围的虚拟地址,映射到在[pa, pa+size)范围的物理地址,主要操作就是找到虚拟地址对应的页表项地址,设置页表项地址为pa(对应该页的首地址)。

static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
    // Fill this function in
    int page_num=PGNUM(size);///计算实际需要分配多少页
    int i;
    for(i=0;i<page_num;i++){
        pte_t *pte=pgdir_walk(pgdir,(void *)va,1);
        ///assert(!pte);///不能用,一但出错直接终止了
        if(!pte)
            panic("boot_map_regin panic:pte error");
        *pte=pa|perm|PTE_P;
        va+=PGSIZE;
        pa+=PGSIZE;
    }
}

page_lookup():查找虚拟地址va对应的页表项,如果找到则返回页表项对应的页信息结构(PageInfo)。

struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
    // Fill this function in
    pte_t *pte=pgdir_walk(pgdir,va,0);
    if(!pte||!(*pte&PTE_P))///如果页表项地址不存在,或者对应页表项的页不存在
        return NULL;
    if(pte_store)
        *pte_store=pte;
    return pa2page(PTE_ADDR(*pte));
}

page_remove():清除页表中虚拟地址va对应的物理页映射。将PageInfo的引用数pp_ref减1,并设置对应页表项的值为0,最后调用tlb_invalidate使tlb中该页缓存失效。

//
// Details:
//   - The ref count on the physical page should decrement.
//   - The physical page should be freed if the refcount reaches 0.
//   - The pg table entry corresponding to 'va' should be set to 0.
//     (if such a PTE exists)
//   - The TLB must be invalidated if you remove an entry from
//     the page table.
//
// Hint: The TA solution is implemented using page_lookup,
//  tlb_invalidate, and page_decref.
//
void
page_remove(pde_t *pgdir, void *va)
{
    // Fill this function in
    pte_t *pte;
    struct PageInfo *page=page_lookup(pgdir,va,&pte);///查找页表项
    if(!page)///如果页表项不存在
        return;
    page_decref(page);
    *pte=0;
    tlb_invalidate(pgdir,va);
}

page_insert():映射虚拟地址va到pp对应的物理页。如果之前该虚拟地址已经存在映射,则要先移除原来的映射。

注:boot_map_region映射的物理页不改变对应的pp_ref,一个物理页被这个函数映射与它是否被使用没有任何关系;而通过page_insert映射的物理页,同时表明该物理页被使用了一次,对应pp_ref加1。

//
// Map the physical page 'pp' at virtual address 'va'.
// The permissions (the low 12 bits) of the page table entry
// should be set to 'perm|PTE_P'.
//
// Requirements
//   - If there is already a page mapped at 'va', it should be page_remove()d.
//   - If necessary, on demand, a page table should be allocated and inserted
//     into 'pgdir'.
//   - pp->pp_ref should be incremented if the insertion succeeds.
//   - The TLB must be invalidated if a page was formerly present at 'va'.
//
// Corner-case hint: Make sure to consider what happens when the same
// pp is re-inserted at the same virtual address in the same pgdir.
// However, try not to distinguish this case in your code, as this
// frequently leads to subtle bugs; there's an elegant way to handle
// everything in one code path.
//
// RETURNS:
//   0 on success
//   -E_NO_MEM, if page table couldn't be allocated
//
// Hint: The TA solution is implemented using pgdir_walk, page_remove,
// and page2pa.
//
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
    pte_t *pte= pgdir_walk(pgdir, va, 1);
    if(!pte)
        return -E_NO_MEM;
    pp->pp_ref++;   //Must before remove
    if(*pte & PTE_P)
        page_remove(pgdir, va);
    *pte = page2pa(pp) | perm | PTE_P;
    return 0;

}

练习4测试过程出现的问题较多,具体如下:

pgdir_walk()中入口标志未设置PTE_P:

一个无法直接从报错信息直接知道的错误:

定位到第795行查看,找到具体出错原因如下:

即,page_insert()中,先移除映射关系(page_remove)导致页已经被释放了,又对引用数增加而出现的错。

pgdir_walk()中入口标志未设置PTE_U:

修改上述所有错误后,成功通过页管理测试:

2.3 Part 3:内核地址空间

JOS将线性地址分成内核部分和用户部分,正如上图中所示,左侧标注U开头的部分(低地址)是用户部分,K开头的部分(高地址)是内核部分。在访问上,用户仅能访问用户地址空间的内容(不能超过ULIM)。否则,用户代码可能会重写内核数据,导致系统崩溃等问题。实验中特别提到PTE_W访问控制位,对于内核代码和用户代码均有效。地址范围[UTOP,ULIM)的部分,是内核的只读数据结构部分,内核代码和用户代码均有只读权限。低于UTOP部分的内容,可以由用户空间设置访问权限进行访问。

练习5要求参考inc/memlayout.h补充kern/pmap.c中的mem_init未完成的代码来设置地址空间的内核部分。

//
    // Now we set up virtual memory

    //
    // Map 'pages' read-only by the user at linear address UPAGES
    // Permissions:
    //    - the new image at UPAGES -- kernel R, user R
    //      (ie. perm = PTE_U | PTE_P)
    //    - pages itself -- kernel RW, user NONE
    // Your code goes here:
    //PGSIZE error
    boot_map_region(kern_pgdir,UPAGES,PGSIZE,PADDR(pages),PTE_U);
    boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U);

    //
    // Use the physical memory that 'bootstack' refers to as the kernel
    // stack.  The kernel stack grows down from virtual address KSTACKTOP.
    // We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
    // to be the kernel stack, but break this into two pieces:
    //     * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory
    //     * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
    //       the kernel overflows its stack, it will fault rather than
    //       overwrite memory.  Known as a "guard page".
    //     Permissions: kernel RW, user NONE
    // Your code goes here:
    ///bootstack-->kern/pmap.h
    boot_map_region(kern_pgdir,KSTACKTOP-KSTKSIZE, KSTKSIZE,PADDR(bootstack),PTE_W);


    //
    // Map all of physical memory at KERNBASE.
    // Ie.  the VA range [KERNBASE, 2^32) should map to
    //      the PA range [0, 2^32 - KERNBASE)
    // We might not have 2^32 - KERNBASE bytes of physical memory, but
    // we just set up the mapping anyway.
    // Permissions: kernel RW, user NONE
    // Your code goes here:
    boot_map_region(kern_pgdir,KERNBASE,0xffffffff-KERNBASE,0,PTE_W); 

Q1:地址映射出现问题.

A1:为了测试到底什么地方出现问题在check_kern_pgdir中将check_va2pa和PADDR的结果进行输出((在断言前添加cprintf).

再次进行测试,结果如下:

可以发现经过check_va2pa对实际分配结果进行测试得出的值为4294967295.应该是直接溢出到虚拟内存的最大地址(0xffffffff)了.所以再次添加cprintf,再次进行验证.

// check pages array
    n = ROUNDUP(npages*sizeof(struct PageInfo), PGSIZE);
    for (i = 0; i < n; i += PGSIZE){
        cprintf("max_address=%u  ",0xffffffff);
        cprintf("check_va2pa=%u  ",check_va2pa(pgdir, UPAGES + i));
        cprintf("PADDR=%u\n",PADDR(pages) + i);
        assert(check_va2pa(pgdir, UPAGES + i) == PADDR(pages) + i); 
    }  

输出结果如下:

又上图可知直接映射到了最高地址.查看inc/memlayout.c中的内存分布图,readonly(RO)区的大小为PTSIZE,而实现中代码中映射的大小被自己写成了PGSIZE.

为此,将新添加的第一部分代码修改为:

//
    // Now we set up virtual memory

    //
    // Map 'pages' read-only by the user at linear address UPAGES
    // Permissions:
    //    - the new image at UPAGES -- kernel R, user R
    //      (ie. perm = PTE_U | PTE_P)
    //    - pages itself -- kernel RW, user NONE
    // Your code goes here:
    boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U);

再次运行:

Q2:通过上面的输出可以发现,访问权限出现了问题.

A2:跳转到出现问题的地方进行查看:

可以看出,当大于KERNBASE部分的页目录的可写访问权限有误.而页目录访问权限是在pgdir_walk中进行设置的,所以修改pgdir_walk中对于页目录访问权限的设置.

pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
    // Fill this function in
    int pde_index=PDX(va);///页目录索引
    pde_t *pde=&pgdir[pde_index];///根据页目录索引获取指定的页目录入口
    if(!(*pde&PTE_P)){  /// Present位,如果页目录项不存在
        if(create==false)   ///如果页不存在并且不要求新建
            return NULL;
        else{
            struct PageInfo *new_page=page_alloc(ALLOC_ZERO);
            if(!new_page)
                return NULL;
            else{
                new_page->pp_ref++;
                ///练习5在下面一行补充|PTE_W
                *pde=page2pa(new_page)|PTE_P|PTE_U|PTE_W;///设置页目录项为新分配的页的地址>,并设置入口标志
            }   
        }   
    }   
    pte_t *pte=(pte_t *)KADDR(PTE_ADDR(*pde));///页表入口
    int pte_index=PTX(va);///页表索引
    return &pte[pte_index];
}

注:对于pgdir_walk函数的实现是在上一个练习中实现的,当时,设置页目录访问权限时,Hint2提示过让页面目录中的权限比严格必要的权限更为宽松,由于疏忽未设置PTE_W位。但当时并未对页目录的PTE_W位进行检测。最终,在练习5中check_kern_pgdir测试时,出现了上面的错误。

将上面两个问题解决后,再次进行检测,结果如下:

Question

Q2:页面目录中的哪些条目(行)此时已被填充?它们映射哪些地址以及它们指向何处?

A2:练习5中完成所有地址映射之后,check_kern_pgdir之前,添加如下代码进行测试:

///
//Test for Question after Exercise 5
    cprintf("Page directory\n");
    int index;
    for(index = 1023; index >= 0; index--){

        pte_t pte = kern_pgdir[index];
        cprintf("index = %u  ",index);
        cprintf("pte = %08x  ",pte);
        cprintf("address = %08x\n",PTE_ADDR(pte));
    }
///
    check_kern_pgdir();

运行结果部分截图如下:

根据运行结果填写表格:

Entry

Base Virtual Address

Point to(logically):

1023

0x003be000

Page table for top 4MB of phys memory

1022

0x003bf000

Page table for top 4MB of phys memory

...

...

Page table for top 4MB of phys memory

960

0x003ff000

Page table for top 4MB of phys memory

959

0x003fe000

Kernel Stack & Invalid Memory

958

0x00000000

NULL

957

0x0011a000

Page Table

956

0x003fd000

Read-Only PAGES

955

0x00000000

NULL

...

...

NULL

2

0x00000000

NULL

1

0x00000000

NULL

0

0x00000000

NULL

Q3:将内核和用户环境放置在相同的地址空间中。为什么用户程序不能读写内核内存?什么具体机制保护内核内存?

A3:内核空间内存的页表项(高于KERNBASE的部分)的perm没有设置PTE_U,要访问内核需要CPL为0-2。而用户程序的CPL为3,用户没有足够的权限去访问内核空间。

Q4:此操作系统可以支持的最大物理内存量是多少?为什么?

A4:2GB,因为PTSIZE(Page table size)最大为4MB,而每个PageInfo大小为8B,所以可以最多可以存储512K个PageInfo结构体,而每个PageInfo对应4KB内存,所以最多512K*4KB= 2GB内存。

//Print size of PageInfo
cprintf("PageInfo size = %x\n",sizeof(struct PageInfo));

Q5:如果我们实际上拥有最大数量的物理内存,那么管理内存有多少空间开销?这个开销是如何分解的?

A5: 如果有最大的2GB内存,则物理页有2GB/4KB=512K个,每个PageInfo结构占用8B,则一共是512K*8B=4MB。页表包括512K个页表项,每个页表项占用4B共需要512K*4B=2MB存储。另外,页目录本身占用4KB存储,所以额外消耗的内存为4MB+2MB+4KB=6MB + 4KB。

Q6:重新访问kern/entry.S和kern/entrypgdir.c中的页表设置。在打开分页后,EIP仍然是一个很小的数字(略高于 1MB)。什么时候过渡到在KERNBASE之上的EIP上运行?是什么让可以在启用分页和开始在高于KERNBASE 的EIP上运行之间继续以低EIP执行?为什么需要这种转变?

A6:kern/entry.S 中的jmp *%eax语句之后就开始跳转到高地址运行。在entry.S中cr3加载的是entry_pgdir,为保证系统正常运行,它将虚拟地址 [0, 4M)和[KERNBASE, KERNBASE+4M)都映射到了物理地址 [0, 4M)。在新的kern_pgdir加载后,并没有映射低位的虚拟地址[0, 4M),所以这一步跳转是必要的。

Challenge1

阅读《Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual》,思考如何使用页目录条目PTE_PS位代替用物理页保存KERNBASE映射的页表来节省空间。

首先,注释掉mem_init中对KERNBASE以上的空间的地址映射(boot_map_region())。未进行优化时,分配二级页表的页面大小为4KB,共需要64个PDE表项和64*1K个PTE表项,共需要64*4B+64K*4B=256KB+256B内存。为减少内存首先开启CR4的PSE位,开启后每个PDE表项对应4MB内存,仅需64个PDE表项,共需要64*4B=256B内存。

开启CR4的PSE位后,设置页目录的PTE_PS位,完成该操作的代码如下:

//
    // Map all of physical memory at KERNBASE.
    // Ie.  the VA range [KERNBASE, 2^32) should map to
    //      the PA range [0, 2^32 - KERNBASE)
    // We might not have 2^32 - KERNBASE bytes of physical memory, but
    // we just set up the mapping anyway.
    // Permissions: kernel RW, user NONE
    // Your code goes here:
    ///boot_map_region(kern_pgdir,KERNBASE,0xffffffff-KERNBASE,0,PTE_W);
    //Challenge1///
    ///turn on PSE of cr4
    uint32_t cr4 = rcr4();
    cr4 |= CR4_PSE;
    lcr4(cr4);
    //Set PDE
    uintptr_t va = KERNBASE;
    physaddr_t pa = 0;
    size_t i;
    for(i = 0; i< 64; i++){
        kern_pgdir[PDX(va)] = pa | PTE_W | PTE_P | PTE_PS;
        va += PTSIZE;
        pa += PTSIZE;
    }
    /Challenge1/
	///check_kern_pgdir();

注:因为check_kern_pgdir();会对PTE进行检查,而当前的优化省略了PTE表项,为保证系统能够继续执行,这里去掉check_kern_pgdir();检查。

进入QEMU调试模式,查看当前分页情况:

注:由上图可知,已经完成映射,S即为PS位。

Challenge2

扩展JOS命令,实现showmappings命令。

关于命令相关设置需要对kern/monitor.h和kern/monitor.c文件进行修改.首先,向command添加命令提示,修改如下:

//kern/monitor.c
static struct Command commands[] = { 
    { "help", "Display this list of commands", mon_help },
    { "kerninfo", "Display information about the kernel", mon_kerninfo },
    { "showmappings","Display information about physical page mappings",mon_showmappings},
};

添加函数声明:

//kern/monitor.h
// Functions implementing monitor commands.
int mon_help(int argc, char **argv, struct Trapframe *tf);
int mon_kerninfo(int argc, char **argv, struct Trapframe *tf);
int mon_backtrace(int argc, char **argv, struct Trapframe *tf);
int mon_showmappings(int argc, char **argv, struct Trapframe *tf);///新添加

实现函数:

int
mon_showmappings(int argc, char **argv, struct Trapframe *tf)
{
    char flag[0x100] = {
        [0]= '-',
        [PTE_W] = 'W',
        [PTE_U] = 'U',
        [PTE_A] = 'A',
        [PTE_D] = 'D',
        [PTE_PS] = 'S',
    };
    char *arg1 = argv[1];
    char *arg2 = argv[2];
    //char *arg3=argv[3];
    if(arg1 == NULL || arg2 == NULL){
        cprintf("showmappings need two arguments!\n");
        return 0;
    }
    char *str1,*str2;
    uintptr_t va_1 = strtol(arg1,&str1,16);
    uintptr_t va_2 = strtol(arg2,&str2,16);
    //判断输入格式,即判断是否全部为16进制格式字符
    if(*str1 || *str2){
        cprintf("Arguments's format error!\n");
        return 0;
    }
    if(va_1 > va_2){
        cprintf("The first argument must smaller than the second argument!\n");
        return 0;
    }
    pte_t *pgdir = (pde_t *)PGADDR(PDX(UVPT), PDX(UVPT), 0);
    cprintf("  va range  entry  flag    pa range\n");
    cprintf("---------------------------------------------------------------\n");
    while(va_1 <= va_2){
        pte_t pde = pgdir[PDX(va_1)];//根据目录索引(入口)获取页表
        if(pde&PTE_P){
            char flag_w = flag[pde & PTE_W];//判断页目录权限位,并记录权限标识
            char flag_u = flag[pde & PTE_U];
            char flag_a = flag[pde & PTE_A];
            char flag_d = flag[pde & PTE_D];
            char flag_s = flag[pde & PTE_PS];
            pde = PTE_ADDR(pde);//获取入口地址
            //打印低地址部分信息
            if(va_1 < KERNBASE){
                cprintf("[%08x - %08x]", va_1, va_1 + PTSIZE - 1);
                cprintf(" PDE[%03x] --%c%c%c--%c%cP\n", PDX(va_1), flag_s, flag_d, flag_a, flag_u, flag_w);
                pte_t *pte = (pte_t *)(pde + KERNBASE);
                size_t i;
                for(i = 0; i < 1024 && va_1 <= va_2; va_1 += PGSIZE, i++){
                    if(pte[i]&PTE_P){
                        flag_w = flag[pte[i] & PTE_W];//判断页表是否权限位,并>记录权限标识
                        flag_u = flag[ pte[i] & PTE_U];
                        flag_a = flag[ pte[i] & PTE_A];
                        flag_d = flag[ pte[i] & PTE_D];
                        flag_s = flag[pte[i] & PTE_PS];
                        cprintf(" |-[%08x - %08x]", va_1, va_1+PGSIZE-1);
                        cprintf(" PTE[%03x] --%c%c%c--%c%cP", i, flag_s, flag_d, flag_a, flag_u, flag_w);
                        cprintf(" [%08x - %08x]\n", PTE_ADDR(pte[i]), PTE_ADDR(pte[i]) + PGSIZE - 1);
                    }//if
                }//for
                continue;
            }//if
            //打印高地址部分信息
            cprintf("[%08x - %08x]", va_1, va_1 + PTSIZE-1);
            cprintf(" PDE[%03x] --%c%c%c--%c%cP", PDX(va_1), flag_s, flag_d, flag_a, flag_u, flag_w);
            cprintf(" [%08x - %08x]\n", pde, pde + PTSIZE-1);
        }//if
         va_1 += PTSIZE;
    }//while
    return 0;
}

测试

测试结果待验证。

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

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

相关文章

静态工厂模式,抽象工厂模式,建造者模式

静态工厂模式 ublic class FruitFactory {public static Fruit getFruit(String name) {Fruit fnull;switch (name){case "APPLE":{fnew Apple();}case "BANANA":{fnew Banana();}default :{System.out.println("Unknown Fruit");}}return f;} …

基于Fomantic UI Web构建 个人导航站点网站源码 网站技术导航源码

BYR-Navi-master好看有个性的网站技术导航源码 该网站基于Fomantic UI Web框架构建&#xff0c;整个项目的设计和构建具有高度的配置和定制灵活性。 整体风格比较适合个人导航站点使用 搜索框输入关键词后&#xff0c;点击上方搜索引擎图标可跳转打开对应搜索引擎搜索结果&am…

UG二次开发 向量叉乘 UF_VEC3_cross

文章作者:里海 来源网站:王牌飞行员_里海_里海NX二次开发3000例,里海BlockUI专栏,C\C++-CSDN博客 简介: UG二次开发 向量叉乘 UF_VEC3_cross,xyz三个向量已知2个求另外1个。 效果: 代码: #include "me.hpp"void ufusr(char* param, int* retcode, int paramLe…

通讯软件019——分分钟学会Prosys OPC UA Server配置

本文介绍如何配置Prosys OPC UA Simulation Server&#xff0c;通过本文可以对OPC UA的基本概念有所了解&#xff0c;掌握OPC UA的本质。更多通信资源请登录网信智汇(wangxinzhihui.com)。 1、启动OPC UA Server Prosys OPC UA Simulation Server启动后就处于运行状态。 2、配…

将两个Series序列的元素按指定条件整合Series.combine()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 把两个序列中各元素 将相同索引对应的值 按指定函数进行整合 得到整合后的结果序列 Series.combine() [太阳]选择题 关于以下代码的说法中错误的是? import pandas as pd a pd.Series([0,3],…

推荐云智推地推拉新系统源码/任务分销神器

源码类型&#xff1a; PHP源码 显示语言&#xff1a; 简体中文 运行环境&#xff1a;PHPMySQLTP 功能内容会员管理&#xff1a;总代&#xff0c;服务商&#xff0c;员工 三种会员&#xff1b;封号&#xff0c;编辑&#xff0c;删除&#xff1b;可开启或关闭总代权限和邀请下级…

Google Hacking搜索

Google Hacking概述 GoogleHacking基本用法逻辑或OR逻辑与And逻辑非NOT完整匹配 GoogleHacking高级用法sitefiletypeinurlintitleintext Google Hacking DataBaseGoogle Hacking概述 GoogleHacking&#xff1a;利用搜索引擎有争对性的搜索信息来对网络入侵的技术和行为。搜索引…

NLP(六十九)智能文档问答助手升级

本文在笔者之前研发的大模型智能文档问答项目中&#xff0c;开发更进一步&#xff0c;支持多种类型文档和URL链接&#xff0c;支持多种大模型接入&#xff0c;且使用更方便、高效。 项目介绍 在文章NLP&#xff08;六十一&#xff09;使用Baichuan-13B-Chat模型构建智能文档中…

APP检测认证第三方检测机构

APP检测 APP检测的必要性&#xff1a; 软件&#xff08;APP&#xff09;安全属于软件领域里一个重要的子领域。在以前的单机时代&#xff0c;安全问题主要是操作系统容易感染病毒&#xff0c;单机应用程序软件安全问题并不突出。但是自从互联网普及后&#xff0c;软件安全问题…

文件上传漏洞第十六关

文件上传漏洞 解题 解题 首先查看一下页面情况 一如既往的是上传图片文件&#xff0c;这里提示我们本题对上传的图片重新进行了渲染。也就是说&#xff0c;我们这里上传的图片将会被打乱重组&#xff0c;那么如果还是将一句话木马和图片拼接&#xff0c;然后上传的话&#xf…

2023.9.6 Redis 的基本介绍

目录 Redis 的介绍 Redis 用作缓存和存储 session 信息 Redis 用作数据库 消息队列 消息队列是什么&#xff1f; Redis 用作消息队列 Redis 的介绍 特点&#xff1a; 内存中存储数据&#xff1a;奠定了 Redis 进行访问和存储时的快可编程性&#xff1a;支持使用 Lua 编写脚…

Flink CDC 菜鸟教程 -环境篇

本教程将介绍如何使用 Flink CDC 来实现这个需求, 在 Flink SQL CLI 中进行,只涉及 SQL,无需一行 Java/Scala 代码,也无需安装 IDE。 系统的整体架构如下图所示: 环境篇 1、 准备一台Linux 2、准备教程所需要的组件 下载 flink-1.13.2 并将其解压至目录 flink-1.13.2 …

MAC安装JDK8

说明 mac版本&#xff1a;MacBook Pro Intel mac系统&#xff1a;macOS Ventura 13.5.1 开始我试着用linux版本的jdk&#xff0c;通过解压的方式安装jdk&#xff08;先解压然后配置环境变量&#xff0c;类似linux安装步骤&#xff09;&#xff0c;但是有各种问题。比如修改环境…

价值10万的最强爽文短剧合集 短剧素材300多部

这是一个包含300多部短剧视频素材的集合&#xff0c;旨在为您的视频创作项目提供尽可能丰富和多样的选项。这些素材涵盖了各种类型和风格&#xff0c;包括但不限于喜剧、悲剧、惊悚、浪漫和冒险等&#xff0c;以满足您的多样化创作需求。 每部短剧视频素材都经过精心挑选和处理…

springboot集成Actuator+Prometheus+Grafana

一、环境准备 PrometheusGrafana环境准备 请参考我的博文&#xff1a;https://blog.csdn.net/luckywuxn/article/details/129475991 二、代码准备 我在本次实践中使用的springboot版本是2.6.13,然后在pom.xml文件中增加一下配置 <dependency><groupId>org.sprin…

【实训项目】基于JavaWeb的图书销售购物系统

1.引言 随着互联网的快速发展和普及&#xff0c;电子商务已经成为了人们购物的主要方式之一。在电子商务领域中&#xff0c;图书销售一直是一个重要的组成部分。随着人们对知识和阅读的需求不断增长&#xff0c;图书销售市场也呈现出蓬勃发展的态势。 传统的图书销售模式存在…

速学Linux丨一文带你打开Linux学习之门

前言 如果你是刚开始学习Linux的小白同学&#xff0c;相信你已经体会到与学习一门编程语言相比&#xff0c;学习Linux系统的门槛相对较高&#xff0c;你会遇到一些困惑&#xff0c;比如&#xff1a; 为什么要学习Linux&#xff0c;学成之后我们可以在哪些领域大显身手&#xf…

GitHubGiteeGitlab极狐(JihuLab)同时配置SSH公私钥详细过程

GitHub-微软-github.com Gitee-开源中国- gitee.com Gitlab-乌克兰GitLab 公司-gitlab.com 极狐(JihuLab)-中国代理商运营的Gitlab -gitlab.cn或者jihulab.com 使用SSH公钥可以让你在你的电脑和GitHub等平台通讯的时候使用更安全的连接&#xff08;Git的Remote要使用SSH地址&a…

第6篇 vue的打包工具webpack

一 webpack 1.1 webpack的作用 webpack是一个打包工具&#xff0c;可以把多个静态资源文件打包成一个文件。如图所示&#xff1a; 1.2 webpack的打包案例

游戏开发入门——CocosCreator实现

课程介绍 课前介绍 为什么要开这门课&#xff1f; 其实市面上已经有很多关于游戏开发的课程了&#xff0c;而且都各有千秋。但是。 目前市面上的编程课很多标榜的零基础&#xff0c;但是很多名词都没有做相应的解释。我觉得对于初学者来说&#xff0c;可能还是有些难度。课…