In the file kern/pmap.c, you must implement code for the following functions (probably in the order given).
mem_init() (only up to the call to check_page_free_list(1))
page_init()
page_alloc()
page_free()
由注释我们可以了解到完成这几个函数目的是完成物理内存空间的页分配以及初始化。
static void *
boot_alloc(uint32_t n)
{
static char *nextfree;
char *result;
if (!nextfree) {
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE);
}
// 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.
result = nextfree;
nextfree = ROUNDUP((char*)(nextfree + n), PGSIZE);
return result;
}
boot_alloc函数是在物理页初始化完成前的内存分配方法。
我到最后都没有找到end的定义在哪里,但根据注释可以猜测是kernal程序空间的.bss段空闲地址开始处。这个boot_alloc函数对应的也是在.bss段上分配空间(这儿我踩了大坑)。
//mem_init()
pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * (npages));
memset(pages, 0, npages * sizeof(struct PageInfo));
分配pages的空间
void
page_init(void)
{
size_t i;
pages[0].pp_ref = 1;
pages[0].pp_link = NULL;
for (i = 1; i < npages_basemem; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
void * freepage = boot_alloc(0);
for(; i < ((uint32_t)freepage - KERNBASE) / PGSIZE; i++) {
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;;
}
for(; i < npages; i++) {
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
页初始化,到这儿根据page2kva宏的定义,可以了解到每一物理页对应的struct PageInfo
在pages中的索引乘以PGSIZE即是该物理页的起始地址。要完成pages的初始化,就需要知道那是空闲的,注释已经告诉了我们部分,但未说清EXTPHYSMEM之后是否使用。查看kernel/kernel.ld注意到kern的.text段开始于EXTPHYSMEM,推测这里就是kern的程序空间起始位置,故从这里一部分空间是被占有的。再通过objdump细究
obj/kern/kernel: file format elf32-i386
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000027c7 f0100000 00100000 00001000 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .rodata 00000b3c f01027e0 001027e0 000037e0 2**5
CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .stab 00004d1d f010331c 0010331c 0000431c 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .stabstr 00001d30 f0108039 00108039 00009039 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .data 0000a300 f010a000 0010a000 0000b000 2**12
CONTENTS, ALLOC, LOAD, DATA
5 .bss 00000674 f0114300 00114300 00015300 2**5
CONTENTS, ALLOC, LOAD, DATA
6 .comment 0000002d 00000000 00000000 00015974 2**0
CONTENTS, READONLY
已经知道boot_alloc是在.bss段分配空间,那么boot_alloc中的nextfree所对应的就是被占用页的边界,通过boot_alloc(0)就能获得nextfree.
struct PageInfo *
page_alloc(int alloc_flags)
{
struct PageInfo* ret = page_free_list;
if(ret != NULL)
{
if(alloc_flags & ALLOC_ZERO)
{
memset((void*)page2kva(ret), ‘\0‘, PGSIZE);
}
page_free_list = page_free_list->pp_link;
ret->pp_link = NULL;
}
return ret;
}
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_link != NULL||pp->pp_ref != 0) {
return ;
}
else {
pp->pp_link = page_free_list;
page_free_list = pp;
pp->pp_ref = 0;
}
}
Look at chapters 5 and 6 of the Intel 80386 Reference Manual, if you haven‘t done so already. Read the sections about page translation and page-based protection closely (5.2 and 6.4). We recommend that you also skim the sections about segmentation; while JOS uses the paging hardware for virtual memory and protection, segment translation and segment-based protection cannot be disabled on the x86, so you will need a basic understanding of it
....
** While GDB can only access QEMU‘s memory by virtual address, it‘s often useful to be able to inspect physical memory while setting up virtual memory. Review the QEMU monitor commands from the lab tools guide, especially the xp command, which lets you inspect physical memory. To access the QEMU monitor, press Ctrl-a c in the terminal (the same binding returns to the serial console)**
C type Address type
T* Virtual
uintptr_t Virtual
physaddr_t Physical
这里提到了接下来要用的几种指针类型,其中虽然physaddr_t明确标示着物理地址类型,但是解引用的话得到的依旧是对应的虚拟地址,这是因为编译器(硬件)默认这些地址为虚拟地址。且需要注意这两种指针类型都是整型,可能存在错误但编译器无法察觉。
Assuming that the following JOS kernel code is correct, what type should variable x have, uintptr_t or physaddr_t?
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;
个人认为在条件仅限这四行代码的时候两者都可以。value是虚拟地址这毫无疑问,再将其强制转换成mystery_t类型对于两者来说都不会报错,且其值相同(都是整型)。但从意义上的话应该是uintptr_t,因为这是虚拟地址。
In the file kern/pmap.c, you must implement code for the following functions.
pgdir_walk()
boot_map_region()
page_lookup()
page_remove()
page_insert()
// A linear address ‘la‘ has a three-part structure as follows:
//
// +--------10------+-------10-------+---------12----------+
// | Page Directory | Page Table | Offset within Page |
// | Index | Index | |
// +----------------+----------------+---------------------+
// \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/
// \---------- PGNUM(la) ----------/
上面则是虚拟地址的构成,通过PDX,PTX能得到对应的页表目录项和页表项索引,从而实现从虚拟地址到物理页再到虚拟地址的映射。
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
uint32_t _pdx = PDX(va);
pte_t* ret;
if((pte_t*)(pgdir[_pdx]) == NULL) {
if(create == true) {
struct PageInfo* ptp = page_alloc(1);
if(ptp == NULL)
return NULL;
else {
physaddr_t pp_ = page2pa(ptp);
ptp->pp_ref++;
pgdir[_pdx] = pp_ | PTE_P;
ret = (pte_t*)(KADDR(pp_));
ret = ret + PTX(va);
return ret;
}
}
else {
return NULL;
}
}
else {
ret = (pte_t*)KADDR(PTE_ADDR(pgdir[_pdx])) + PTX(va);
}
return ret;
}
实现过程让我极其煎熬,好在做出来了。模棱两可的注释(也与是我英语水平不行)让我纠结了半天返回的是页表还是页表项,执行时才反应过来。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
pte_t* pet_;
size_t sz;
uint32_t beg = PDX(va);
size_t dec = PDX(size);
for(sz = 0; sz < size;sz+=PGSIZE,va+=PGSIZE, pa += PGSIZE) {
pet_ = pgdir_walk(pgdir, (void*)va, 1);
*pet_ = pa | perm | PTE_P;
}
for(sz = 0;sz < dec; sz++, beg++)
pgdir[beg] |= perm | PTE_P;
}
静态映射,用于初始对连续空间的映射。
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t* dpg_ = pgdir_walk(pgdir, va, 1);
if(dpg_ == NULL)
return -E_NO_MEM;
pp->pp_ref++;
if(*dpg_ != 0) {
page_remove(pgdir, va);
}
*dpg_ = page2pa(pp) | perm | PTE_P;
pgdir[PDX(va)] = PTE_ADDR(pgdir[PDX(va)]) | perm | PTE_P;
return 0;
}
页插入,建立虚拟地址到物理空间的指定映射。值得一提的是pp->pp_ref++
这一行应该放在page_remove
之前,否则当插入页与目标页是同一物理页的情况下该页会被提前释放掉,导致下一次分配页的时候分配已被占用的页。
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t* dpg = pgdir_walk(pgdir, va, 0);
if(dpg == NULL)
return NULL;
if(*dpg == 0)
return NULL;
struct PageInfo* pp_ = pa2page(*dpg);
if(pte_store != 0)
*dpg = **pte_store;
return pp_;
}
页查询以及页表项覆盖,我不太明白这里pte_store为什么要用二级指针。
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t ptet_ = 0;
pte_t* ptet = &ptet_;
struct PageInfo* pp_ = page_lookup(pgdir, va, &ptet);
page_decref(pp_);
tlb_invalidate(pgdir, va);
}
** Fill in the missing code in mem_init() after the call to check_page().**
boot_map_region(kern_pgdir, UPAGES, ROUNDUP(npages * sizeof(struct PageInfo), PGSIZE),(physaddr_t)PADDR(pages), PTE_U | PTE_P);
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_P | PTE_W);
boot_map_region(kern_pgdir, KERNBASE, (0xffffffff - KERNBASE) + 1, 0, PTE_P | PTE_W);
Question
We have placed the kernel and user environment in the same address space. Why will user programs not be able to read or write the kernel‘s memory? What specific mechanisms protect the kernel memory?
对于用户空间所能看到的只是内核空间的映射,再通过页表项低12位来判断权限,如果没有设置PTE_U则用户无法访问该页。
What is the maximum amount of physical memory that this operating system can support? Why
256MB?4GB的虚拟内存映射到256mb的物理地址空间,0~0x100000.
How much space overhead is there for managing memory, if we actually had the maximum amount of physical memory? How is this overhead broken down?
1级页表4k空间对应1024个二级页表项,页表空间占据了1025页也就是约4MB空间。
Revisit the page table setup in kern/entry.S and kern/entrypgdir.c. Immediately after we turn on paging, EIP is still a low number (a little over 1MB). At what point do we transition to running at an EIP above KERNBASE? What makes it possible for us to continue executing at a low EIP between when we enable paging and when we begin running at an EIP above KERNBASE? Why is this transition necessary?
mov $relocated, %eax
jmp *%eax
在执行这条指令后eip就来到了KERNBASE之后。
__attribute__((__aligned__(PGSIZE)))
pde_t entry_pgdir[NPDENTRIES] = {
// Map VA‘s [0, 4MB) to PA‘s [0, 4MB)
[0]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P,
// Map VA‘s [KERNBASE, KERNBASE+4MB) to PA‘s [0, 4MB)
[KERNBASE>>PDXSHIFT]
= ((uintptr_t)entry_pgtable - KERNBASE) + PTE_P + PTE_W
};
这里在提前分配虚拟空间的时候同时把[0, 4MB)和[KERNBASE, KERNBASE+4MB)映射到了物理空间[0, 4MB)。否则处理器无法继续执行(eip指向不存在的空间)。
running JOS: (7.9s)
Physical page allocator: OK
Page management: OK
Kernel page directory: OK
Page management 2: OK
Score: 70/70
We consumed many physical pages to hold the page tables for the KERNBASE mapping. Do a more space-efficient job using the PTE_PS ("Page Size") bit in the page directory entries. This bit was not supported in the original 80386, but is supported on more recent x86 processors. You will therefore have to refer to Volume 3 of the current Intel manuals. Make sure you design the kernel to use this optimization only on processors that support it!
page size (PS) flag, bit 7 page-directory entries for 4-KByte pages
Determines the page size. When this flag is clear, the page size is 4
KBytes and the page-directory entry points to a page table. When the
flag is set, the page size is 4 MBytes for normal 32-bit addressing (and
2 MBytes if extended physical addressing is enabled) and the pagedirectory entry points to >a page. If the page-directory entry points to
a page table, all the pages associated with that page table will be
4-KByte pages.
这是文献中对PTE_PS的介绍。
我的想法是基于32位虚拟地址。在分配虚拟地址的时候我们会调用boot_map_region函数,其中参数size是要分配的空间,在boot_map_region的实现中加入当size大于PTSIZE时的处理。让本来应该指向二级页表的页表项指向映射物理空间的起始并标志 PTE_PS。那么在读页表项的判断就如文献中所说。
相应的pgdir_walk函数,page_insert等函数需要加入对一级页表项PTE_PS的判断并进行相应的处理。
例如pgdir_walk函数本需要返回对应的二级页表项,实现后若PTE_PS置位则只需要返回一级页表项即可。
页表项替换即映射更改时则需要根据需要映射的连续空间大小来选择是生成一个二级页表还是PTE_PS置位的页表项。
这个lab难倒我了,对虚拟地址的半吊子理解没少让我踩坑,之前还想着两天做完,结果磨磨唧唧加上其他的事硬是现在做完。
好在做完这个我算是彻底理解了内核空间初始化,内核空间用户空间的分布以及虚拟地址的映射。另外我这gdb用的愈发熟练。总之收获颇丰。
原文:https://www.cnblogs.com/kingchou0/p/14453437.html