实验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;
}
测试
测试结果待验证。