Mit6.828 Lab2

发布时间 2023-11-07 21:36:45作者: mjy66

lab2

​ 提交lab1代码的时候,出现了合并冲突的问题,使用git status,发现问题出现在init.c文件与lab分支的文件产生冲突,修改后成功提交。

​ lab2中多出来了以下几个文件

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

​ 另外lab2中还多了许多宏,在接下来的练习中需要经常使用,需要比较熟悉,将这几个宏列在下面:

名称 参数 作用
PADDR 内核虚拟地址kva 将内核虚拟地址kva转成对应的物理地址
KADDR 物理地址pa 将物理地址pa转化为内核虚拟地址
page2pa 页信息结构struct PageInfo 通过空闲页结构得到这一页起始位置的物理地址
pa2page 物理地址pa 通过物理地址pa获取这一页对应的页结构体struct PageInfo
page2kva 页信息结构struct PageInfo 通过空闲页结构得到这一页起始位置的虚拟地址
PDX 线性地址la 获得该线性地址la对应的页目录项索引
PTX 线性地址la 获得该线性地址la在二级页表中对应的页表项索引
PTE_ADDR(pte) 页表项或页目录项的值 获得对应的页表基址或物理地址基址

Part 1: Physical Page Management

​ 操作系统必须跟踪物理内存中哪些是空闲内存,哪些是当前正在使用的内存。JOS 以页面粒度管理个人电脑的物理内存,以便使用 MMU 映射和保护分配的每块内存。

​ 现在你将编写物理页面分配器。它通过 struct PageInfo 对象的链表(与 xv6 不同,PageInfo 对象本身并不嵌入在空闲页面中)来跟踪哪些页面是空闲的,每个对象对应一个物理页面。在编写虚拟内存实现的其余部分之前,你需要编写物理页面分配器,因为你的页表管理代码需要分配物理内存来存储页表。

练习1.在文件kern/pmap.c中,你必须实现下列函数的代码(可能按给出的顺序)。

boot_alloc()
mem_init() (只到调用check_page_free_list(1)为止)
page_init()
page_alloc()
page_free()

check_page_free_list()和check_page_alloc()测试你的物理页面分配器。你应该启动JOS,看看check_page_alloc()是否报告成功。修正你的代码,使其通过。你可能会发现添加你自己的assert()来验证你的假设是否正确。

  • boot_alloc()

    // This simple physical memory allocator is used only while JOS is setting
    // up its virtual memory system.  page_alloc() is the real allocator.
    //
    // If n>0, allocates enough pages of contiguous physical memory to hold 'n'
    // bytes.  Doesn't initialize the memory.  Returns a kernel virtual address.
    //
    // If n==0, returns the address of the next free page without allocating
    // anything.
    //
    // If we're out of memory, boot_alloc should panic.
    // This function may ONLY be used during initialization,
    // before the page_free_list list has been set up.
    static void *
    boot_alloc(uint32_t n)
    {
            static char *nextfree;  // virtual address of next byte of free memory
            char *result;
    
            // 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);
            }
    
            // 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;
            if(n == 0)
                    return result;
            else if(n > 0)
            {
                    nextfree += n;
                    nextfree = ROUNDUP(nextfree,PGSIZE);
            }
            return result;
    }
    
    

    ​ 首先nextfree是指向下一个空闲内存的虚拟地址,end是指向内核的bss段的末尾的指针,可以在/obj/kern目录下,输入objdump -h kernel命令,可以看到,.bss是内核的最终位置,所以end是指向内核的bss段的末尾的一个指针。ROUNDUP宏就是将地址圆整,保证地址对齐。

    ​ 函数开头函数:

    if (!nextfree) {
                    extern char end[];
                    nextfree = ROUNDUP((char *) end, PGSIZE);
            }
    

    意思就是如果nextfree是空的,就让nextfree指向内核末尾后的第一个空闲的虚拟地址,PGSIZE就是一个页的大小,就是4096。在boot_alloc要求分配n个字节,并更新nextfree地址,代码如下:

    result = nextfree;
    if(n == 0)
            return result;
    else if(n > 0)
    {
            nextfree += n;
            nextfree = ROUNDUP(nextfree,PGSIZE);
    }
    return result;
    

    如果分配的内存大小为0,则直接返回nextfree地址,若内存大于0,则更新nextfree,并把原来的地址返回。

  • mem_init

void
mem_init(void)
{
	uint32_t cr0;
	size_t n;
// Find out how much memory the machine has (npages & npages_basemem).
i386_detect_memory();//从CMOS获取内存大小,得到totalmem、basemem

// Remove this line when you're ready to test this function.
panic("mem_init: This function is not finished\n");

//////////////////////////////////////////////////////////////////////
// create initial page directory.
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);//获取页目录地址
memset(kern_pgdir, 0, PGSIZE);//将页目录起始地址开始的一页地址标志设置为0

//////////////////////////////////////////////////////////////////////
// Recursively insert PD in itself as a page table, to form
// a virtual page table at virtual address UVPT.
// (For now, you don't have understand the greater purpose of the
// following line.)

// Permissions: kernel R, user R
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
//标记为可以进行地址转换以及用户权限为用户级
    
//////////////////////////////////////////////////////////////////////
// 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);
check_page_alloc();
check_page();

//////////////////////////////////////////////////////////////////////
// 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:

//////////////////////////////////////////////////////////////////////
// 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:

//////////////////////////////////////////////////////////////////////
// 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:

// Check that the initial page directory has been set up correctly.
check_kern_pgdir();

// Switch from the minimal entry page directory to the full kern_pgdir
// page table we just created.	Our instruction pointer should be
// somewhere between KERNBASE and KERNBASE+4MB right now, which is
// mapped the same way by both page tables.
//
// If the machine reboots at this point, you've probably set up your
// kern_pgdir wrong.
lcr3(PADDR(kern_pgdir));

check_page_free_list(0);

// entry.S set the really important flags in cr0 (including enabling
// paging).  Here we configure the rest of the flags that we care about.
cr0 = rcr0();
cr0 |= CR0_PE|CR0_PG|CR0_AM|CR0_WP|CR0_NE|CR0_MP;
cr0 &= ~(CR0_TS|CR0_EM);
lcr0(cr0);

// Some more checks, only possible after kern_pgdir is installed.
check_page_installed_pgdir();
}

这个函数需要用到PageInfo结构体,因此需要先看看PageInfo结构体的定义:

struct PageInfo {
	// Next page on the free list.
	struct PageInfo *pp_link;
	// pp_ref is the count of pointers (usually in page table entries)
	// to this page, for pages allocated using page_alloc.
	// Pages allocated at boot time using pmap.c's
	// boot_alloc do not have valid reference count fields.
	uint16_t pp_ref;
};

​ 这个函数首先在内核末尾后分配了PGSIZE大小给内核页目录,并将这个页目录的内存全部置0,然后需要分配pages数组内存大小,其内存大小为npages*sizeof(struct PageInfo),再将pages数组内存清零。

  • page_init
void
page_init(void)
{
    size_t i;
	// The example code here marks all physical pages as free.
	// However this is not truly the case.  What memory is free?
	//  1) Mark physical page 0 as in use.
	//     This way we preserve the real-mode IDT and BIOS structures
	//     in case we ever need them.  (Currently we don't, but...)
	    pages[0].pp_ref = 1;
        pages[0].pp_link = NULL;

        size_t i;

        size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;
        //  2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
        //     is free.
        for (i = 1; i < npages_basemem; i++) {
                pages[i].pp_ref = 0;
                pages[i].pp_link = page_free_list;
                page_free_list = &pages[i];
        }
        //  3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
        //     never be allocated.
        for(i = IOPHYSMEM/PGSIZE;i < EXTPHYSMEM/PGSIZE;i++)
        {
                pages[i].pp_ref = 1;
        }
        //  4) Then extended memory [EXTPHYSMEM, ...).
        //     Some of it is in use, some is free. Where is the kernel
        //     in physical memory?  Which pages are already in use for
        //     page tables and other data structures?
        for(i = EXTPHYSMEM/PGSIZE;i < kernel_end_page;i++)
        {
                pages[i].pp_ref = 1;
        }
        for(i = kernel_end_page ; i < npages;i++)
        {
                pages[i].pp_ref = 0;
                pages[i].pp_link = page_free_list;
                page_free_list = &pages[i];
        }

}

page_init函数会将page数组进行一个初始化,并建立空闲链表page_free_list。page数组并不是每页都是空的,根据提示:

(1)0页面是被使用,其包含IDT以及BIOS

(2)0页面到IO洞之间的页是空闲的

(3)IO洞不能被分配

(4)扩展内存一部分被使用,一部分空闲

  • Page_alloc
//
// Allocates a physical page.  If (alloc_flags & ALLOC_ZERO), fills the entire
// returned physical page with '\0' bytes.  Does NOT increment the reference
// count of the page - the caller must do these if necessary (either explicitly
// or via page_insert).
//
// Be sure to set the pp_link field of the allocated page to NULL so
// page_free can check for double-free bugs.
//
// Returns NULL if out of free memory.
//
// Hint: use page2kva and memset
struct PageInfo *
page_alloc(int alloc_flags)
{
        // Fill this function in
        if(page_free_list == NULL)
                return NULL;
        struct PageInfo* Pagealloc = page_free_list;
        page_free_list = page_free_list->pp_link;
        Pagealloc->pp_link = NULL;
        if(alloc_flags & ALLOC_ZERO)
                memset(page2kva(Pagealloc),'\0',PGSIZE);
        return Pagealloc;
}

page_alloc用来分配页,提示我们使用page2kvamemset函数,page2kva函数的作用是将页转为其对应的虚拟地址,首先要从page_free_list链表中取得空闲的页,然后更新page_free_list,题目中提示If (alloc_flags & ALLOC_ZERO), fills the entire returned physical page with '\0' bytes,因此还需要增加一个条件判断,并将该空闲页对应的虚拟地址用'\0'填充。

  • 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("fuck you dont free now man");
                return;
        }
        pp->pp_link = page_free_list;
        page_free_list = pp;
}

page_free的功能是把释放的页加入page_free_list中,在加入之前需要判断页的引用是否为0。

Part 2: Virtual Memory

Exercise 2. 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.

这个练习让我们熟悉一下内存中的段机制和页机制

1.1 段描述符

​ 段描述符为处理器提供所需的数据,将逻辑地址映射为线性地址,描述符由编译器、链接器、加载器或操作系统创建,不是程序员创建。以下两种描述符的形式为通用的描述符格式:

BASE:基地址

LIMIT:段的大小,一共有32位,但是在上图只能找到20位,因为另外12位由颗粒度(G)决定

Granularity bit:解释LIMIT字段的单位,当该位为0时,限制以一个字节为单位,当该位不为0,则设置以4000字节为单位

S:系统标志。s=0表示该段是一个系统段,s=1则表示是一个代码段或者数据段,当s=0,根据下表确定是哪种类型描述符。

TYPE:区分各种描述符的种类

若s=1,如果是数据段(第十一位为0):

A,表示是否访问过(访问过被置为1,否则为0)
W,表示是否可写(1表示可读可写,0表示只可读不可写)
E,拓展位(0表示向上拓展,1表示向下拓展)

如果是代码段(第十一位为1):

A,表示是否访问过(访问过被置为1,否则为0)
R,表示该段是否可读(1表示可执行可读,0表示只可执行不可读)
C,一致位(0表示非一致代码段,1表示一直代码段)

DPL:由保护机制使用

Segment-Present bit:如果为0,则这个描述符不能用来进行地址转换。出现以下两种情况,将会导

致操作系统清0。

1、当段所跨越的线性空间没有被分页机制所映射

2、当段在内存中不存在的时候

Accessed bit:当段被访问的时候,处理器会设置这个位

1.2描述符表

描述符表分为两种:

  • 全局描述符表
  • 局部描述符表

描述符表是一个包含8字节描述符的数组,如下图所示:

处理器能通过GDTR和LDTR寄存器定位内存中的GDT和当前LDT。

1.3选择符

​ 通过索引确定一个选择符表中的一个描述符,下图为选择符的格式

Index:在一个有8192个描述符的描述符表中选择一个描述符。处理器通过将index*8+描述符表的基址来选取描述符。

Table Indicator:指定选择器选择的是哪个表,0表示GDT、1表示LDT

Requested Privilege Level:由保护机制使用

1.4段寄存器

​ 处理器将描述符信息储存在段寄存器中,避免每次都要访问描述符表。

​ 每个段寄存器前16位为可见部分后面不可见的位由处理器操作。

​ 加载这些寄存器有两种方式:

1、直接加载指令:例如,MOV,POP,LDS等

2、隐含加载指令:例如CALL和JMP

​ 使用这些指令,程序用一个16位的选择器加载段寄存器的可见部分。处理器自动从描述符表中获取基址、限制、类型和其他信息,并将它们加载到段寄存器的不可见部分。
​ 因为大多数指令引用的是段中的数据,这些段的选择器已经被加载到段寄存器中,处理器可以将指令提供的段相关偏移量添加到段基址中,而没有额外的开销。

1.5 页翻译机制

​ 通过段的机制将逻辑地址转换为线性地址之后,第二阶段便是线性地址转换为物理地址。CR0寄存器的PG位被设置,页面转换生效。

​ 一个页框是一个4k字节的物理内存连续地址单位。以字节为界开始,大小固定。

1.5.1线性地址

​ 线性地址通过指定一个页表、该页表中的一个页以及该页中的一个偏移量,间接地指向了一个物理地址。

​ 下图展示了如何从线性地址转换为物理地址:

1.5.2页表

​ 页表是一个指定32位页的数组。两个级别的表被用来寻址一个内存页。在较高的层次上是一个页目录。页目录可以寻址到第二级的1024个页表。第二级的一个页表可以寻址1024个页,因此一个页目录能够寻址1024×1024个页,每个页大小为4kb,所以一个页目录的表可以跨越1024×1024×4kb的物理内存。

​ 页目录的地址存储在CPU寄存器CR3中,内存管理软件可以让所有任务使用一个页面目录,也可以为每个任务使用一个页面或者两者的组合。

1.5.3页表条目

​ 页表条目的格式如下图所示:

Page Frame Address:页框地址指定了一个页面的物理起始地址。因为页面位于4K边界上,所以低阶12位总是零。在一个页目录中,页框地址是一个页表的地址。在一个二级页表中,页框地址是包含所需内存操作数的页框的地址。

Present Bit:0表示该页面不能用于页面转换,1则表示可以。在支持分页虚拟内存的软件系统中,若出现P=0,分页异常处理程序会在物理内存创建一个页。

Accessed and Dirty Bits:这些位提供了关于页表中页的使用情况,这些位都是由硬件设置的,当页面被读取,则处理器将相应的访问位设置为1,对页表项所覆盖的地址进行写操作时,则将二级页表中的dirty位设置为1。

Read/Write and User/Supervisor Bits:这些位不用于地址转换,用于页级保护。

1.5.4页的缓存

​ 处理器会将最近使用的页表数据存储在片上高速缓存中,当页表发生改变的时候,页表缓存必须也要改变,页表缓存能被以下两种方式改变:

1、将CR3寄存器加载在EAX中。

2、通过执行任务切换到具有与当前TSS不同的CR3图像的TSS。

1.6段和页翻译地址的结合

1.6.1 段跨度多个页

​ 80386的结构允许段大于或小于一个页面的大小(4kb),假设一个段用来寻址和保护一个跨度为132kb的大型数据结构,在一个支持分页虚拟内存的软件系统中,整个结构不一定同时出现在物理内存中,该结构被分为33个页面,应用程序员不需要知道虚拟子系统以这种方式对结构进行分页。

1.6.2 页跨度多个段

​ 段也可能小于一个页面的大小,比如信号量,一个系统需要多个信号量,此时就可以在一个页面内聚焦多个段。

1.6.3 非对齐的页和段的边界

​ 80386结构并不让执行页面和段的边界之间有任何的对应关系,一个段也可以包含一个页面的结束和另一个页面的开始。

1.6.4 对齐的页和段的边界

​ 也可以让段和页的边界之间有对应关系,让段只以一页为单位进行分配,段和页的分配逻辑就可以合并。

1.6.5 每段的页表

一种进一步简化空间管理软件的方法是在段描述符和页目录条目之间保持一对一的对应关系。

2.保护机制

在80386保护机制主要体现在5个方面:

1、类型检查
2、限制检查
3、对可寻址域的限制
4、程序入口点的限制
5、对指令集的限制

2.1 分段式保护

​ 段是保护的单位,段描述符保存着保护机制的参数。保护参数由软件在创建描述符时放在描述符中,应用程序的程序员无需关注保护参数的问题

​ 当程序将选择器加载到段寄存器中时,处理器不仅加载了段的基地址,而且还加载了保护信息,因此对同一段的后续保护检查不会消耗额外的时钟周期。

2.1.1 Type检查

​ 描述符的TYPE字段有两个功能:

​ 1、区分不同的描述符格式

​ 2、规定一个段的预期用途

除了应用程序常用的数据和可执行段的描述符外,80386还有用于操作系统和门的特殊段的描述符。数据段的描述符中的可写位制定了指令是否可以写进该段。可执行段描述符中的可读位指定指令是否允许从该段中读取。一个可读的、可执行的段可以通过两种方式读取:

1、通过CS寄存器覆盖前缀

2、通过加载描述符的选择器到数据段寄存器。

2.1.2 限制检查

​ 段描述符的Limit用来防止程序在段外寻址。处理器对Limit的解释取决于颗粒度的设置。当G=0时,LIMIT是描述符中出现的20位限制字段的值。极限值从0~0xFFFFFH(2^20-1)。当G=1时,处理器将12个低阶一比特附加到极限字段上。在这种情况下,极限值从0xFFFH到0xFFFFFFFFH。

​ 以下三种情况下,处理器会引起一个一般保护异常:

1、试图在一个地址>Limit的地方访问一个内存字节。

2、试图在一个地址>=Limit的地方访问一个内存字。

3、试图在地址>=(Limit-2)处访问一个内存双字。

向上向下拓展的意义:
在向上拓展段中,逻辑地址偏移的范围从0到段界限。超过段界限的偏移会导致一般保护异常。在向下扩展段,段界限的作用正好相反;偏移的范围从段界限到FFFFFFFFH或者 FFFFH,小于段界限的偏移会导致一般保护异常。

​ 描述符表的限制字段被处理器用来防止程序选择描述符表以外的表项。一个描述符表的极限标识了表中最后一个描述符的最后一个有效字节。由于每个描述符的长度为8个字节,对于一个最多包含N个描述符的表,极限值为N*8-1。

2.1.3 特权等级

​ 处理器通过给关键对象分配一个从0到3的值来实现特权的概念,0代表最大的权限,3代表最小。

描述符特权等级为DPL,选择器特权等级为RPL,当前的权限级别为CPL,一般记录在CS寄存器中。

2.1.4 限制对数据的访问

​ 当实现内存中的操作时,80386程序会将数据段的选择器加载到数据段寄存器中,处理器通过比较权限级别自动评估数据段的访问。如下图所示,三种不同的权限级别进入这种类型的权限检查中:

1、CPL(当前权限级别)

2、RPL 选择器的请求权限级别

3、DPL 目标段描述符的权限级别

​ 只有当目标段的DPL数值上大于或等于CPL或者选择器的RPL时,指令才能加载数据段的寄存器。

2.1.5 访问代码段中的数据

​ 使用代码段保存数据比较少见,用其保存常量是合法的,写到类型为代码段的描述符是不合法的。

以下是访问代码段中的数据的方法:

1、用一个不符合要求的、可读的、可执行的段的选择器来加载一个数据段寄存器。
2、用一个符合要求的、可读的、可执行的段的选择器加载一个数据段寄存器。
3、使用CS覆盖前缀来读取一个可读的、可执行的段,其选择器已被加载到CS寄存器中。

2.2 控制转移

​ 控制转移是由指令JMP、CALL、RET、INT和IRET以及异常和中断机制来完成的。JMP、CALL和RET的近距离形式转移,只会在段内传输,因此只需要检查CALL、JMP和RET指令的目的地是否超过当前可执行段的极限即可。

​ 而远距离的操作数指的是其它段,处理器会执行权限检查,有两种方法可以使JMP或CALL指向其他段:

1、操作数选择另一个可执行段的描述符

2、操作方选择一个调用门描述符

​ 通常情况下,CPL等于处理器当前执行的段的DPL。然而若当前代码段的描述符的c位被设置了,cs寄存器内的cpl的值可能会大于DPL。只有在满足当前特权规则之一的情况下,处理器才允许JMP或CALL直接到另一个段:

1、目标的DPL等于CPL

2、目标代码段描述符的comforming位被设置,而且目标的DPL小于或等于CPL。

​ 一个可执行段的描述符中的conforming bit若被设置,则这个段被称为conforming segment。对于conforming bit没有被设置的段控制权可以在没有gate的情况下转移到相同权限级别的可执行段。若出现CALL指令与调用门描述符一起使用的情况,就有必要实现将控制权转移到数字更小的权限级别上。

2.3 门描述符

​ 为了保护不同权限级别的可执行段之间的控制传输,80386使用四种门描述符:

1、调用门

2、陷阱门

3、中断门

4、任务门

​ 这章只介绍调用门,调用门描述符可以位于GDT或LDT中,但是不在IDT中,其主要有两个功能:

1、定义存储过程的入口点

2、指定入口点的权限级别

其格式如下图所示:

​ 调用门描述符在调用和跳转指令中的使用方式与代码段描述符相同。当硬件识别到目标选择器指向一个门描述符时,指令的操作将根据调用门的内容进行扩展。

​ 选择器和偏移量字段构成了指向存储过程入口点的指针,其门描述符保证了其通过一个有效的入口进入另一个段,而不是可能会进入到程序的中间或者指令的中间。控制传输指令的远指针不指向指令的段和偏移量;相反指针的选择器选择一个门,而不会选择偏移量。下图展示了这种寻址方式:

​ 门可以用于将控制权转移到数字上较小的权限级别,也可以转移到相同的权限级别。只有通过CALL指令才能使用门传递到较小的权限级别,门只能被JMP指令用于转移到具有相同权限级别的可执行代码段。

​ 对于指向nonconforming的程序段的JMP指令,必须满足以下两条特权规则:

MAX (CPL,RPL) <= gate DPL
target segment DPL = CPL

对于CALL指令(或指向conforming的段的JMP指令),必须满足以下两条权限规则:

MAX (CPL,RPL) <= gate DPL
target segment DPL <= CPL

虚拟、线性和物理地址

练习 3.虽然 GDB 只能通过虚拟地址访问 QEMU 的内存,但在设置虚拟内存时检查物理内存往往很有用。查看实验工具指南中的 QEMU 监视器命令,尤其是 xp 命令,它可以让你检查物理内存。要访问 QEMU 监视器,请在终端按下 Ctrl-a c(同样的绑定返回串行控制台)。

使用 QEMU 监视器中的 xp 命令和 GDB 中的 x 命令检查相应物理地址和虚拟地址的内存,确保看到的数据相同。

我们的 QEMU 补丁版本提供了 info pg 命令,该命令可能也很有用:它显示了当前页表的紧凑而详细的信息,包括所有映射的内存范围、权限和标志。Stock QEMU 还提供了 info mem 命令,可显示虚拟地址的映射范围和权限概览。

​ 这个练习想要测试物理内存和虚拟内存中的值是否一样。

​ 首先开两个窗口,第一个窗口输入make qemu-gdb,第二个窗口输入make gdb,再在第二个窗口输入c后,ctrl+c终止程序,之后再在第一个窗口输入Ctrl+a+c进入监管模式。

可见物理内存0x100000和虚拟内存0xf0100000的值是一样的。

Question

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

答:uintptr_t

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

页表管理

练习 4.在文件kern/pMap.c中,您必须实现下列函数的代码。

 pgdir_walk()
 boot_map_region()
 page_lookup()
 page_remove()
 page_insert()

check_page()从mem_init()调用,测试你的页表管理例程。在继续执行之前,应该确保它报告成功。

  • pgdir_walk()
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns
// a pointer to the page table entry (PTE) for linear address 'va'.
// This requires walking the two-level page table structure.
//
// The relevant page table page might not exist yet.
// If this is true, and create == false, then pgdir_walk returns NULL.
// Otherwise, pgdir_walk allocates a new page table page with page_alloc.
//    - If the allocation fails, pgdir_walk returns NULL.
//    - Otherwise, the new page's reference count is incremented,
//	the page is cleared,
//	and pgdir_walk returns a pointer into the new page table page.
//
// Hint 1: you can turn a PageInfo * into the physical address of the
// page it refers to with page2pa() from kern/pmap.h.
//
// Hint 2: the x86 MMU checks permission bits in both the page directory
// and the page table, so it's safe to leave permissions in the page
// directory more permissive than strictly necessary.
//
// Hint 3: look at inc/mmu.h for useful macros that mainipulate page
// table and page directory entries.
//
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
        // Fill this function in
        pte_t* pgtab;
        pde_t pde = pgdir[PDX(va)];
    	//PDX(va)是找到虚拟地址va对应的页目录索引,再找到所对应的页目录
        if(pde & PTE_P)
        {
                pgtab = (pte_t*)KADDR(PTE_ADDR(pde));
            	//PTE_ADDR是找到pde页目录对应的二级页项的物理地址,然后将其转化为虚拟地址
        }
        else
        //如果pde不存在
        {
                if(!create)
                        return NULL;
                else
                {
                        struct PageInfo* page = page_alloc(true);
                    	//分配一个页
                        if(!page)
                                return NULL;
                        (page->pp_ref)++;
                    	//将其页面的指针数量增加1
                        pgdir[PDX(va)] = page2pa(page) | PTE_U | PTE_P | PTE_W;				   //更新页的权限和所对应的物理地址	
                        return  (pte_t*) page2kva(page) + PTX(va);
                }
        }
        return &pgtab[PTX(va)];
}

这个函数的目的是根据虚拟地址va找到其对应的二级页表的页的地址

  • boot_map_region()
//
// Map [va, va+size) of virtual address space to physical [pa, pa+size)
// in the page table rooted at pgdir.  Size is a multiple of PGSIZE, and
// va and pa are both page-aligned.
// Use permission bits perm|PTE_P for the entries.
//
// This function is only intended to set up the ``static'' mappings
// above UTOP. As such, it should *not* change the pp_ref field on the
// mapped pages.
//
// Hint: the TA solution uses pgdir_walk
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
        // Fill this function in
        uintptr_t end = va + size;
        while(1)
        {
                if(va == end)
                        break;
                pte_t *pte = pgdir_walk(pgdir,(void*)va,1);
                *pte = pa|perm|PTE_P;
                va += PGSIZE;
                pa += PGSIZE;
        }
}

这个函数的作用就是将物理地址和虚拟地址一一建立映射关系,长度为size大小

  • page_lookup()
//
// Return the page mapped at virtual address 'va'.
// If pte_store is not zero, then we store in it the address
// of the pte for this page.  This is used by page_remove and
// can be used to verify page permissions for syscall arguments,
// but should not be used by most callers.
//
// Return NULL if there is no page mapped at va.
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
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_P)
        {
                if(pte_store != 0)
                        *pte_store = pte;
                return pa2page(PTE_ADDR(*pte));
        }
        else
        {
                return NULL;
        }
}

这个函数的作用是将虚拟地址va映射的物理地址的页面返回,如果第三个参数 pte_store 不是0,那么就把把它代表的地址放到这个页面中。提示使用pgdir_walk和pa2page的函数,pgdir_walk函数的作用是找到虚拟地址va相对应的页表中的项

static inline struct PageInfo*
pa2page(physaddr_t pa)
{
	if (PGNUM(pa) >= npages)
		panic("pa2page called with invalid pa");
	return &pages[PGNUM(pa)];
}
// page number field of address
#define PGNUM(la)	(((uintptr_t) (la)) >> PTXSHIFT)
  • page_remove
//
// Unmaps the physical page at virtual address 'va'.
// If there is no physical page at that address, silently does nothing.
//
// 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)
        {
                page_decref(page);
                *pte = 0;
                tlb_invalidate(pgdir,va);
        }
}

这个函数的目的是取消虚拟地址va对应的物理地址的页,注意的点是要取消tlb缓存中对应的页。

  • page_insert
//
// 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)
{
        // Fill this function in
        pte_t *pte = pgdir_walk(pgdir,va,1);
        if(!pte)
                return -E_NO_MEM;
        if(*pte & PTE_P)
        {
                if(PTE_ADDR(*pte) == page2pa(pp))
                {
                        *pte = page2pa(pp)|perm|PTE_P;
                        return 0;
                }
                page_remove(pgdir,va);
        }
        pp->pp_ref++;
        *pte = page2pa(pp)|perm|PTE_P;
        return 0;
}

函数目的是把虚拟地址的情况映射到物理页面pp中,如果虚拟地址对应的物理页面已经存在而且与pp不同,则需要把原来的页面remove掉,再建立映射。

Part 3: Kernel Address Space

前面完成一些基础函数的编写之后,这章需要将各个物理地址与虚拟地址之间完成映射,在inc/memlayout.h的文件中提供了JOS整体的虚拟内存布局:

/*
 * Virtual memory map:                                Permissions
 *                                                    kernel/user
 *
 *    4 Gig -------->  +------------------------------+
 *                     |                              | RW/--
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     :              .               :
 *                     :              .               :
 *                     :              .               :
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
 *                     |                              | RW/--
 *                     |   Remapped Physical Memory   | RW/--
 *                     |                              | RW/--
 *    KERNBASE, ---->  +------------------------------+ 0xf0000000      --+
 *    KSTACKTOP        |     CPU0's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     +------------------------------+                   |
 *                     |     CPU1's Kernel Stack      | RW/--  KSTKSIZE   |
 *                     | - - - - - - - - - - - - - - -|                 PTSIZE
 *                     |      Invalid Memory (*)      | --/--  KSTKGAP    |
 *                     :              .               :                   |
 *                     :              .               :                   |
 *    MMIOLIM ------>  +------------------------------+ 0xefc00000      --+
 *                     |       Memory-mapped I/O      | RW/--  PTSIZE
 * ULIM, MMIOBASE -->  +------------------------------+ 0xef800000
 *                     |  Cur. Page Table (User R-)   | R-/R-  PTSIZE
 *    UVPT      ---->  +------------------------------+ 0xef400000
 *                     |          RO PAGES            | R-/R-  PTSIZE
 *    UPAGES    ---->  +------------------------------+ 0xef000000
 *                     |           RO ENVS            | R-/R-  PTSIZE
 * UTOP,UENVS ------>  +------------------------------+ 0xeec00000
 * UXSTACKTOP -/       |     User Exception Stack     | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebff000
 *                     |       Empty Memory (*)       | --/--  PGSIZE
 *    USTACKTOP  --->  +------------------------------+ 0xeebfe000
 *                     |      Normal User Stack       | RW/RW  PGSIZE
 *                     +------------------------------+ 0xeebfd000
 *                     |                              |
 *                     |                              |
 *                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 *                     .                              .
 *                     .                              .
 *                     .                              .
 *                     |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
 *                     |     Program Data & Heap      |
 *    UTEXT -------->  +------------------------------+ 0x00800000
 *    PFTEMP ------->  |       Empty Memory (*)       |        PTSIZE
 *                     |                              |
 *    UTEMP -------->  +------------------------------+ 0x00400000      --+
 *                     |       Empty Memory (*)       |                   |
 *                     | - - - - - - - - - - - - - - -|                   |
 *                     |  User STAB Data (optional)   |                 PTSIZE
 *    USTABDATA ---->  +------------------------------+ 0x00200000        |
 *                     |       Empty Memory (*)       |                   |
 *    0 ------------>  +------------------------------+                 --+
 *
 * (*) Note: The kernel ensures that "Invalid Memory" is *never* mapped.
 *     "Empty Memory" is normally unmapped, but user programs may map pages
 *     there if desired.  JOS user programs map pages temporarily at UTEMP.
 */

boot_map_region函数的作用就是实现连续虚拟地址和物理地址之间的映射,首先考虑映射的第一部分,第一部分虚拟地址的起点是UPAGES,大小是PTSIZE,物理地址的起点是PADDR(pages),权限是 PTE_U|PTE_P。

//////////////////////////////////////////////////////////////////////
        // 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);

第二部分的映射起点是KSTACKTOP-KSTKSIZE,大小是KSTKSIZE,物理地址是PADDR(bootstack)

//////////////////////////////////////////////////////////////////////
        // 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:
        boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE,PADDR(bootstack),PTE_W );

第三部分虚拟地址的起点是KERNBASE,大小是2^32-KERNBASE,物理地址的起点是0。

//////////////////////////////////////////////////////////////////////
        // 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, -KERNBASE, 0, PTE_W);

Question

  1. What entries (rows) in the page directory have been filled in at this point? What addresses do they map and where do they point? In other words, fill out this table as much as possible:

    Entry Base Virtual Address Points to (logically):
    1023 0xffc00000 Page table for top 4MB of phys memory
    1022 ? ?
    . ? ?
    . ? ?
    . ? ?
    2 0x00800000 ?
    1 0x00400000 ?
    0 0x00000000 [see next question]
  2. 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?

    ans:在页表项中有相应的读写位,以及PTE_U区分内核和用户,MMU会负责实现这种保护。

  3. What is the maximum amount of physical memory that this operating system can support? Why?

    ans:4GB

  4. 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?

    ans:4GB/PGSIZE=210*210页(pgt) 2^10页(pgd)

    210×210×4+2^10×4(KB)

总结:Lab2大致围绕着mem_init这个函数进行编程,这个函数建立了一个两级的页表,mem_init函数大致分为以下几个步骤:

1、调用i386_detect_memory()获得机器的物理内存大小

2、调用boot_alloc初始化了一个页目录,其地址在内核bss段后

3、创建了一个能存储n个PageInfo的物理数组pages

4、调用page_init函数,根据物理内存进行pages数组的初始化,同时记录空闲的页

5、实现物理地址和虚拟地址之间的映射,

1689689247856

挑战!使用命令扩展 JOS 内核监视器,以便:

以有用且易于阅读的格式显示适用于当前活动地址空间中特定范围的虚拟/线性地址的所有物理页映射(或缺少物理页映射)。例如,您可以输入
“显示映射0x3000 0x5000”
显示物理页面映射和应用于页面的相应权限位
在虚拟地址 0x3000、0x4000 和 0x5000。
显式设置、清除或更改当前地址空间中任何映射的权限。
转储给定虚拟或物理地址范围的内存范围的内容。确保转储代码在范围扩展到页面边界时行为正确!
执行您认为以后可能对调试内核有用的任何其他操作。

//将十六进制的字符串转换为十进制整形
uint32_t xtoi(char* buf) {
        uint32_t res = 0;
        buf += 2; //0x...
        while (*buf) {
                if (*buf >= 'a') *buf = *buf-'a'+'0'+10;
                res = res*16 + *buf - '0';
                ++buf;
        }
        return res;
}

int mon_showmappings(int argc,char **argv,struct Trapframe *tf)
{
        if(argc == 1)
        {
                cprintf("please enter two vitrual address\n");
                return 0;
        }
        uint32_t start =xtoi(argv[1]),end = xtoi(argv[2]);
        for(;start<=end;start+=PGSIZE)
        {
                pte_t* pte = pgdir_walk(kern_pgdir,(void*)start,1);
                if(!pte)
                        panic("out of memory");
                if(*pte & PTE_P)
                {
                        cprintf("page %x with ",start);
                        cprintf("PTE_P: %x, PTE_W: %x, PTE_U: %x\n", *pte&PTE_P, *pte&PTE_W, *pte&PTE_U);
                }
                else
                        cprintf("page not exist: %x\n",start);
        }
        return 0;
}

写完这个映射的函数之后,要将这个函数加在commands结构体数组中,这样在qemu运行之后,输入对应的宏才会出现相关信息。

参考链接:https://blog.csdn.net/qq_32473685/article/details/99625128
https://www.cnblogs.com/oasisyang/p/15495908.html
https://www.cnblogs.com/sssaltyfish/p/10689405.html