MIT6.828-OS lab3:User Environments 记录

github:https://github.com/viktorika/mit-os-lab

Introduction

在本实验中,您将实现运行受保护的用户态环境environment(即“进程process”)所需的基本内核功能。您将增强JOS内核,以设置数据结构来跟踪用户环境,创建单个用户环境,将程序映像加载到其中并开始运行。您还将使JOS内核能够处理用户环境发出的任何系统调用,并处理它引起的任何其他异常。

Note:在本实验中,environment和process这两个术语是可以互换的-它们具有大致相同的含义。 我们引入术语“environment”,而不是传统的术语“process”,以便强调一点,即JOS的environment不能提供与UNIX的process相同的语义,即使它们具有大致可比性。

 

Getting Started

在你lab2完成后使用git commit你的修改,获取课程的最新版本。然后创建一个本地分支lab3。

lab3包含了一些你应该阅览的新文件。

MIT6.828-OS lab3:User Environments 记录_第1张图片

此外,lab2发放的一些源文件在lab3被修改了。为了看到这些区别,你可以使用git diff lab2。

你可能还想看看lab工具指引,它包含一些debug用户代码的信息,这些是跟我们这次lab相关的。

 

Lab Requirements

该实验室分为两部分,A和B。A部分应在分配该实验室后的一周内完成。 即使您的代码可能尚未通过所有的成绩脚本测试,您仍应提交更改并且make handin实验在A部分截止日期之前。 (如果完成了,那太好了!)您只需要在第二周的最后期限之前通过B部分的所有成绩脚本测试即可。

与lab2一样,您将需要完成实验中描述的所有常规练习以及至少一个challenge问题(针对整个实验,而不是针对每个部分)。 在lab目录的最顶层一个名为Answers-lab3.txt的文件中,写下对lab中提出的问题的简短答案,以及一两段描述您为解决所选challenge问题所做的工作的描述。 (如果您实现了多个challenge问题,则只需要在本文中描述其中一个问题。)不要忘记在提交中使用git add answers-lab3.txt包含答案文件。

 

Inline Assembly

在本实验中,您可能会发现GCC的内联汇编语言功能很有用,尽管也可以不使用它来完成实验。 至少,您将需要能够理解我们提供给您的源代码中已经存在的内联汇编语言(“ asm”语句)的片段。 您可以在课程参考资料页面上找到有关GCC内联汇编语言的多种信息来源。

 

Part A: User Environments and Exception Handling

新的头文件inc / env.h包含JOS中用户环境的基本定义。 立即阅读。 内核使用Env数据结构来跟踪每个用户environment。 在本实验中,您最初将仅创建一个environment,但是您将需要设计JOS内核以支持多个environment。 lab4将通过允许用户environment派生其他environment来利用此功能。

如您在kern / env.c中看到的那样,内核维护着与environment有关的三个主要全局变量:

JOS启动并运行后,envs指针指向代表系统中所有environment的Env结构数组。 在我们的设计中,尽管在任何给定时间通常运行的environment通常要少得多,但是JOS内核将最多支持NENV个同时活动的environment。 (NENV是在inc / env.h中#定义的常量。)分配它后,envs数组将为每个NENV可能的environment包含一个Env数据结构的单个实例。

JOS内核将所有不活动的Env结构保留在env_free_list上。 这种设计可以轻松分配和释放environment,因为只需将其添加到空闲列表中或从空闲链表中删除。

内核使用curenv符号在任何给定时间跟踪当前正在执行的environment。 在启动过程中,在运行第一个environment之前,curenv最初设置为NULL。

 

Environment State 

Env结构在inc / env.h中定义如下(尽管将来的实验中将添加更多字段):

MIT6.828-OS lab3:User Environments 记录_第2张图片

这是Env字段的用途:

env_tf:

在inc / trap.h中定义的该结构在该environment未运行时(即,在内核或其他environment正在运行时)保存该environment的寄存器值。 从用户态切换到内核态时,内核会保存这些内容,以便以后可以从中断的地方恢复environment。

env_link:

这是指向env_free_list上的下一个Env的指针。 env_free_list指向列表中的第一个空闲的environment。

env_id:

内核在此处存储一个值,该值唯一地标识当前使用此Env结构(即在envs数组中使用此特定slot)的environment。 用户environment终止后,内核可能会将相同的Env结构重新分配给不同的environment-但是新environment将具有与旧environment不同的env_id,即使新environment正在重新使用envs数组中的相同slot也是如此 。

env_parent_id:

内核在此处存储创建此environment的environment的env_id。 这样,环境可以形成“family tree”,这对于制定允许哪些environment对谁执行操作的安全决策很有用。

env_type:

这用于区分特殊environment。 对于大多数environment,它将是ENV_TYPE_USER。 在以后的lab中,我们将为特殊的系统服务environment引入更多类型。

env_status:

此变量保存以下值之一:

    ENV_FREE:指示Env结构处于非活动状态,因此处于env_free_list上。

    ENV_RUNNABLE:表示该Env指向的environment正在等待处理器运行。

    ENV_RUNNING:表示Env指向的environment正在运行。

    ENV_NOT_RUNNABLE:表示Env指向的environment是一个活动的状态,但是现在还没准备好运行,比如他可能正在等待其他environment的IPC通信。

    ENV_DYING:表示Env结构指向一个僵尸environment。 僵尸environment在下一次traps到内核时将被释放。 在lab4之前,我们将不使用此标志。

env_pgdir:

此变量保存此environment的页目录的内核虚拟地址。

 

像Unix的process一样,JOS的environment将“线程”和“地址空间”的概念结合在一起。 线程主要由保存的寄存器(env_tf字段)定义,地址空间由env_pgdir指向的页目录和页表定义。 要运行一个environment,内核必须使用保存的寄存器和适当的地址空间来设置CPU。

我们的struct Env类似于xv6中的proc。 这两种结构都在Trapframe结构中保存environment(即process)的用户态寄存器状态。 在JOS中,各个environment不像xv6中的process那样具有自己的内核栈。 一次在内核中只能有一个活动的JOS的environment,因此JOS只需要一个内核栈。

 

Allocating the Environments Array

在lab2中,您在mem_init()中为pages []数组分配了内存,这是内核用来跟踪哪些页面是空闲和哪些页面不是空闲的表。 现在,您将需要进一步修改mem_init(),以分配一个类似的Env结构数组,称为envs。

Exercise 1.修改kern / pmap.c中的mem_init()以分配和映射envs数组。 该数组完全由NENV个分配的Env结构的实例组成,就像分配pages数组的方式一样。 与pages数组一样,内存支持环境也应该在UENVS(在inc / memlayout.h中定义)上以只读方式映射为用户,以便用户进程可以从该数组读取。

您应该运行代码,并确保check_kern_pgdir()成功。

这个简单吧,做过lab2,这个直接给代码好了。

    //
    // Make 'envs' point to an array of size 'NENV' of 'struct Env'.
    // LAB 3: Your code here.
    envs = (struct Env *)boot_alloc(sizeof(struct Env) * NENV);
    //
    // Map the 'envs' array read-only by the user at linear address UENVS
    // (ie. perm = PTE_U | PTE_P).
    // Permissions:
    //    - the new image at UENVS  -- kernel R, user R
    //    - envs itself -- kernel RW, user NONE
    // LAB 3: Your code here.
    boot_map_region(kern_pgdir, UENVS, sizeof(struct Env) * NENV, PADDR(envs),  PTE_U | PTE_W);

 

Creating and Running Environments

您现在将在kern / env.c中编写运行用户environment所需的代码。 因为我们还没有文件系统,所以我们将设置内核以加载嵌入在内核本身中的静态二进制image。 JOS将此二进制文件作为ELF可执行image嵌入内核。

Lab 3的GNUmakefile在obj / user /目录中生成许多二进制image。 如果您查看kern / Makefrag,您会注意到一些神奇的地方,可以将这些二进制文件直接链接到内核可执行文件,就像它们是.o文件一样。 链接器命令行上的-b binary选项使这些文件链接为“raw”未解释的二进制文件,而不是作为编译器生成的常规.o文件。 (就链接程序而言,这些文件根本不必是ELF image-它们可以是任何东西,例如文本文件或图片!)如果您在构建内核之后查看obj / kern / kernel.sym, 您会注意到,链接器“神奇地”产生了许多有趣的符号,它们的名称晦涩难懂,例如_binary_obj_user_hello_start,_binary_obj_user_hello_end和_binary_obj_user_hello_size。 链接器通过处理二进制文件的文件名来生成这些符号名。 这些符号为常规内核代码提供了一种引用嵌入式二进制文件的方式。

在kern / init.c中的i386_init()中,您将看到在environment中运行这些二进制image之一的代码。 但是,设置用户environment的关键函数还不完善。 您将需要填写它们。

Exercise 2.在文件env.c中,完成对以下功能的编码:

env_init()

    初始化envs数组中的所有Env结构,并将它们添加到env_free_list。 还调用env_init_percpu,它将为特权级别0(内核)和特权级别3(用户)配置具有单独段的分段硬件。

env_setup_vm()

    为新environment分配页目录,并初始化新environment的地址空间的内核部分。

region_alloc()

    分配和映射environment的物理内存。

load_icode()

    您将需要像boot loader一样解析ELF二进制映像,并将其内容加载到新environment的用户地址空间中。

env_create()

    使用env_alloc分配环境并调用load_icode将ELF二进制文件加载到其中。

env_run()

    启动以用户态运行的给定environment。

在编写这些函数时,您可能会发现新的cprintf %e 很有用-它打印与错误代码相对应的描述。 例如,

将会panic消息“ env_alloc:out of memory”。

下面是代码的调用图,直至调用用户代码为止。 确保您了解每个步骤的目的。

MIT6.828-OS lab3:User Environments 记录_第3张图片

完成后,您应该编译内核并在QEMU下运行它。 如果一切顺利,您的系统应进入用户空间并执行hello二进制文件,直到使用int指令进行系统调用为止。 在这个时候会有麻烦,因为JOS尚未设置硬件以允许从用户空间到内核的任何类型的转换。 当CPU发现未将其设置为处理该系统调用中断时,它将生成一个常规保护异常,发现它不能处理该异常,生成一个双重故障异常,发现它也不能处理该异常, 最终放弃,也就是所谓的“三重故障”。 通常,然后您会看到CPU复位和系统重启。 尽管这对遗留应用程序很重要,但这对于内核开发是一件痛苦的事,因此,使用6.828修补的QEMU,您将看到寄存器dump和“三重错误”的信息。

我们将很快解决这个问题,但是现在我们可以使用调试器检查我们是否进入用户态。 使用make qemu gdb并在env_pop_tf处设置GDB断点,该断点应该是您在实际进入用户态之前击中的最后一个函数。 使用si单步完成此功能; 处理器应在iret指令之后进入用户态。 然后,您应该在用户环境的可执行文件中看到第一条指令,它是lib / entry.S中标签开头的cmpl指令。 现在使用b * 0x ...在hello的sys_cputs()中的int $ 0x30处设置一个断点(有关用户空间地址,请参见obj / user / hello.asm)。 此int是系统调用,用于向控制台显示字符。 如果不能尽可能的执行int,则地址空间设置或程序加载代码有问题; 返回并修复它,然后继续。

首先是env_init:

MIT6.828-OS lab3:User Environments 记录_第4张图片
标记envs里的所有environments为空闲,设置他们的env_ids为0,并且把他们插入到env_free_list里。确保environment在free list里的顺序与他们在envs数组里相同。以至于第一次调用env_alloc返回的是envs[0]。 代码如下:

void
env_init(void)
{
    // Set up envs array
    // LAB 3: Your code here.
    // 链表顺序要以数组顺序,这里我们倒着插入
    for(int i = NENV - 1; i >= 0; --i){
        envs[i].env_status = ENV_FREE;
        envs[i].env_id = 0;
        envs[i].env_link = env_free_list;
        env_free_list = &envs[i];
    }   

    // Per-CPU part of the initialization
    env_init_percpu();
}

再看env_setup_vm:

MIT6.828-OS lab3:User Environments 记录_第5张图片
对于environment e,初始化它的kernel虚拟内存布局。分配一个页目录表,相应设置e->env_pgdir,并初始化新environment的地址空间的内核部分。不要映射任何东西到新environment的用户部分。如果成功返回0,错误返回<0的error值。Errors包括:如果页目录表或者页表不能分配则是-E_NO_MEM。

static int
env_setup_vm(struct Env *e)
{
    int i;
    struct PageInfo *p = NULL;

    // Allocate a page for the page directory
    if (!(p = page_alloc(ALLOC_ZERO)))
        return -E_NO_MEM;

    // Now, set e->env_pgdir and initialize the page directory.
    //
    // Hint:
    //    - The VA space of all envs is identical above UTOP
    //  (except at UVPT, which we've set below).
    //  See inc/memlayout.h for permissions and layout.
    //  Can you use kern_pgdir as a template?  Hint: Yes.
    //  (Make sure you got the permissions right in Lab 2.)
    //    - The initial VA below UTOP is empty.
    //    - You do not need to make any more calls to page_alloc.
    //    - Note: In general, pp_ref is not maintained for
    //  physical pages mapped only above UTOP, but env_pgdir
    //  is an exception -- you need to increment env_pgdir's
    //  pp_ref for env_free to work correctly.
    //    - The functions in kern/pmap.h are handy.

    // LAB 3: Your code here.
    // 设置env_pgdir
    e->env_pgdir = (pde_t *)page2kva(p);
    p->pp_ref = 1;
    // UTOP以上照搬kern_pgdir,这个地方参考别人的
    for(int i = PDX(UTOP); i < NPDENTRIES; ++i)
        e->env_pgdir[i] = kern_pgdir[i];

    // UVPT maps the env's own page table read-only.
    // Permissions: kernel R, user R
    e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;

    return 0;
}

region_alloc:

MIT6.828-OS lab3:User Environments 记录_第6张图片
给environment env分配len字节的物理内存,并且将他映射在environment的虚拟地址空间。不要以零或以其他任何方式初始化映射的页面。页面应该可以被用户和kernel写入。如果分配企图失败请panic。

static void
region_alloc(struct Env *e, void *va, size_t len)
{
    // LAB 3: Your code here.
    // (But only if you need it for load_icode.)
    //
    // Hint: It is easier to use region_alloc if the caller can pass
    //   'va' and 'len' values that are not page-aligned.
    //   You should round va down, and round (va + len) up.
    //   (Watch out for corner-cases!)
    unsigned start = ROUNDDOWN((unsigned)va, PGSIZE);
    unsigned end = ROUNDUP((unsigned)va + len, PGSIZE);
    for(; start < end; start += PGSIZE){
        struct PageInfo *page = page_alloc(0); //不需要ALLOC_ZERO
        if(!page)
            panic("region_alloc: page_alloc error");
        int result = page_insert(e->env_pgdir, page, (void *)start, PTE_U | PTE_W);
        if(result < 0)
            panic("region_alloc: page_insert error");
    }
}

load_icode:

MIT6.828-OS lab3:User Environments 记录_第7张图片

这个函数从ELF二进制image里加载所有可加载的段到environment的用户内存,从ELF程序头中指示的适当虚拟地址开始。同时,它会将这些段的所有部分清零,这些部分在程序头中被标记为已映射但实际上未出现在ELF文件中-即程序的bss部分。
所有这些都跟我们的boot loader所做的非常相似,除了boot loader还需要从硬盘读取代码。请看boot/main.c获取灵感。最后,这个函数为程序初始的栈映射一页。如果他遭遇问题就要panic。

static void
load_icode(struct Env *e, uint8_t *binary, size_t size)
{
    // Hints:
    //  Load each program segment into virtual memory
    //  at the address specified in the ELF section header.
    //  You should only load segments with ph->p_type == ELF_PROG_LOAD.
    //  Each segment's virtual address can be found in ph->p_va
    //  and its size in memory can be found in ph->p_memsz.
    //  The ph->p_filesz bytes from the ELF binary, starting at
    //  'binary + ph->p_offset', should be copied to virtual address
    //  ph->p_va.  Any remaining memory bytes should be cleared to zero.
    //  (The ELF header should have ph->p_filesz <= ph->p_memsz.)
    //  Use functions from the previous lab to allocate and map pages.
    //
    //  All page protection bits should be user read/write for now.
    //  ELF segments are not necessarily page-aligned, but you can
    //  assume for this function that no two segments will touch
    //  the same virtual page.
    //
    //  You may find a function like region_alloc useful.
    //
    //  Loading the segments is much simpler if you can move data
    //  directly into the virtual addresses stored in the ELF binary.
    //  So which page directory should be in force during
    //  this function?
    //
    //  You must also do something with the program's entry point,
    //  to make sure that the environment starts executing there.
    //  What?  (See env_run() and env_pop_tf() below.)

    // LAB 3: Your code here.
    struct Elf *elf = (struct Elf *)binary;
    if(ELF_MAGIC != elf->e_magic)
        panic("load icode: e_magic is not equal to ELF_MAGIC");
    if(!elf->e_entry)
        panic("load icode: e_entry is NULL");
    //修改env的eip寄存器为入口地址,run的时候是根据env_tf来恢复上下文的
    e->env_tf.tf_eip = elf->e_entry;
    struct Proghdr *ph = (struct Proghdr *)(binary + elf->e_phoff);
    struct Proghdr *eph = ph + elf->e_phnum;
    //设置cr3寄存器为当前env的页目录,因为后面已经memmove和memset用的是改env的虚拟地址空间
    lcr3(PADDR(e->env_pgdir));
    //遍历program header table
    for(; ph < eph; ++ph)
        // 需要加载的段
        if(ELF_PROG_LOAD == ph->p_type){
            if(ph->p_filesz > ph->p_memsz)
                panic("load icode: ph->p_filesz > ph->p_memsz");
            //分配内存
            region_alloc(e, (void *)ph->p_va, ph->p_memsz);
            //加载到内存
            memmove((char *)ph->p_va, (void *)(binary + ph->p_offset), ph->p_filesz);
            //其余部分清0
            memset((char *)(ph->p_va + ph->p_filesz), 0, ph->p_memsz - ph->p_filesz);
        }

    // Now map one page for the program's initial stack
    // at virtual address USTACKTOP - PGSIZE.

    // LAB 3: Your code here.
    region_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
}

这个比较复杂,需要详细了解elf文件的结构,以及了解清楚elf加载到内存的过程,仔细看注释。再看env_create。

MIT6.828-OS lab3:User Environments 记录_第8张图片

用env_alloc分配一个新的env,通过load_icode加载elf二进制文件,并且设置他的env_type。这个函数仅仅在内核初始化期间在跑第一个用户态environment的时候调用,新的env的父亲id被设置为0。

void
env_create(uint8_t *binary, size_t size, enum EnvType type)
{
    // LAB 3: Your code here.
    struct Env *env;
    int result = env_alloc(&env, 0);
    if(result < 0)
        panic("env_create: env_alloc error");
    load_icode(env, binary, size);
    env->env_type = type;
}

这个肯定是最简单的一个了吧,调用直接写好的接口就行。最后一个env_run。

MIT6.828-OS lab3:User Environments 记录_第9张图片

从curenv的上下文切换到env e的上下文。注意:如果是第一次调用env_run,curenv是空。这个函数不做任何返回。

void
env_run(struct Env *e)
{
    // Step 1: If this is a context switch (a new environment is running):
    //     1. Set the current environment (if any) back to
    //        ENV_RUNNABLE if it is ENV_RUNNING (think about
    //        what other states it can be in),
    //     2. Set 'curenv' to the new environment,
    //     3. Set its status to ENV_RUNNING,
    //     4. Update its 'env_runs' counter,
    //     5. Use lcr3() to switch to its address space.
    // Step 2: Use env_pop_tf() to restore the environment's
    //     registers and drop into user mode in the
    //     environment.

    // Hint: This function loads the new environment's state from
    //  e->env_tf.  Go back through the code you wrote above
    //  and make sure you have set the relevant parts of
    //  e->env_tf to sensible values.

    // LAB 3: Your code here.
    // 修改原来的env状态
    if(curenv && ENV_RUNNING == curenv->env_status){
        curenv->env_status = ENV_RUNNABLE;
        curenv->env_runs--;
    }
    // 修改curenv为当前env并且修改状态
    curenv = e;
    curenv->env_status = ENV_RUNNING;
    curenv->env_runs++;
    // 切换地址空间,恢复寄存器
    lcr3(PADDR(curenv->env_pgdir));
    env_pop_tf(&curenv->env_tf);
}

这个不难,注释仔细看明白就好。到这就完成了。

 

Handling Interrupts and Exceptions 

这时,用户空间中的第一个int $ 0x30系统调用指令已进入死循环:一旦处理器进入用户态,就无法退出。 现在,您将需要实现基本的异常和系统调用处理,以便内核可以从用户态代码中恢复对处理器的控制。 您应该做的第一件事是完全熟悉x86中断和异常机制。

Exercise 3.如果尚未阅读《 80386程序员手册》(或《 IA-32开发人员手册》的第5章)中的第9章,异常和中断,请阅读。

在本lab中,我们通常遵循Intel的术语来表示中断,异常等。 但是,诸如异常,陷阱,中断,故障和中止之类的术语在整个体系结构或操作系统中没有标准含义,并且经常被使用而无视它们在诸如x86之类的特定体系结构上的细微差别。 当您在本练习之外看到这些术语时,其含义可能会略有不同。

异常和中断都是“受保护的控制传递”,它们导致处理器从用户态切换到内核态(CPL = 0),而没有给用户态代码任何干扰内核或其他environment功能的机会。 用intel的术语来说,中断是受保护的控制传输,它是由通常在处理器外部的异步事件引起的,例如外部设备I / O活跃的通知。 异常则相反,是由当前运行的代码同步引起的受保护的控制传输,例如由于除以0或无效的内存访问。

为了确保保护了这些受保护的控制传递,设计了处理器的中断/异常机制,以使在发生中断或异常时当前正在运行的代码不会随意选择进入内核的位置或方式。 相反,处理器确保只能在精心控制的条件下才能进入内核。 在x86上,两种机制可以共同提供这种保护:

1.中断描述符表。处理器确保中断和异常只能从确定的几个明确定义的入口点进入内核(而不是采用中断或异常时运行的代码)。 x86允许多达256个不同的中断或异常入口点进入内核,每个入口点都有不同的中断向量。向量是介于0到255之间的数字。中断的向量由中断源决定:不同的设备,错误条件以及对内核的应用程序请求会使用不同的向量生成中断。 CPU使用该向量作为处理器中断描述符表(IDT)的索引,该中断描述符表由内核在内核专用内存中设置,就像GDT一样。处理器从该表中的适当条目中加载:要加载到指令指针(EIP)寄存器中的值,该值指向为处理这种类型的异常而指定的内核代码。要加载到代码段(CS)寄存器中的值,在位0-1中包含运行异常处理程序的特权级别。 (在JOS中,所有异常都在内核模式下,特权级别0处理。)

2.任务状态段。处理器需要在中断或异常发生之前保存旧处理器状态的位置,例如在处理器调用异常处理程序之前保存EIP和CS的原始值,以便异常处理程序以后可以恢复该旧状态并恢复被中断的状态。代码从它停止的地方开始。但是,必须反过来保护旧处理器状态的此保存区域不受非特权用户态代码的影响;否则,错误或恶意的用户代码可能会损害内核。因此,当x86处理器执行中断或陷阱,从而导致特权级别从用户态切换为内核态时,它还会切换到内核内存中的栈。称为任务状态段(TSS)的结构指定了段选择器和该栈所在的地址。处理器(在这个新栈上)把SS,ESP,EFLAGS,CS,EIP和可选的错误代码压栈。然后,它从中断描述符中加载CS和EIP,并将ESP和SS设置为引用新栈。尽管TSS很大,并且可能有多种用途,但JOS仅使用它来定义处理器从用户态转换到内核态时应切换到的内核栈。由于JOS中的“内核态”在x86上的特权级别为0,因此处理器在进入内核态时使用TSS的ESP0和SS0字段来定义内核栈。 JOS不使用任何其他TSS字段。

Types of Exceptions and Interrupts

x86处理器可以在内部生成的所有同步异常都使用0到31之间的中断向量,因此映射到IDT条目0-31。 例如,页面错误总是通过向量14引起异常。大于31的中断向量仅由软件中断使用,该中断可以由int指令生成,也可以由外部设备在需要注意时引起的异步硬件中断生成。 在本节中,我们将扩展JOS以处理向量0-31中内部生成的x86异常。 在下一节中,我们将使JOS处理软件中断向量48(0x30),JOS(相当任意)将其用作系统调用中断向量。 在实验4中,我们将扩展JOS以处理外部生成的硬件中断,例如时钟中断。

An Example

让我们将这些片段放在一起,并通过示例进行追溯。 假设处理器正在用户态中执行代码,并遇到试图除以零的除法指令。

1.处理器切换到由TSS的SS0和ESP0字段定义的栈,这两个值分别是jos里的GD_KD和KSTACKTOP。

2.处理器将异常参数从地址KSTACKTOP开始压入内核栈:

3.因为我们正在处理除法错误,即x86上的中断向量0,所以处理器读取IDT条目0并将CS:EIP设置为指向该条目描述的处理函数。

4.处理程序函数控制并处理异常,例如,通过终止用户environment。

对于某些类型的x86异常,除了上面的“标准”五个word以外,处理器还将另一个包含错误代码的word压入栈。 14号页面错误异常是一个重要示例。 请参阅80386手册,以确定处理器将哪个错误号把错误代码压栈,以及在这种case下错误代码的含义。 当处理器把错误代码压栈时,从用户态进入的异常处理程序的栈的开头看起来如下:

MIT6.828-OS lab3:User Environments 记录_第10张图片

对于把错误代码压栈的异常类型,处理器会像前面一样在old EIP之后立即把错误代码压栈。

对于处理器的嵌套异常功能有一个重要警告。 如果处理器在已经处于内核态下时发生异常,并且由于缺少栈空间等任何原因而无法将其旧状态压入到内核栈,则处理器无法执行任何操作来恢复,因此只会重置自身。 不用说,应该对内核进行设计以免发生这种情况。

Setting Up the IDT

现在,您应该具有设置IDT和处理JOS中的异常所需的基本信息。 现在,您将设置IDT以处理中断向量0-31(处理器异常)。 在本lab的后面,我们将处理系统调用中断,并在以后的实验中添加中断32-47(设备IRQ)。

头文件inc / trap.h和kern / trap.h包含与中断和异常相关的重要定义,您需要熟悉这些定义。 文件kern / trap.h包含对内核严格专有的定义,而inc / trap.h包含对用户级程序和库也可能有用的定义。

注意:Intel定义0-31范围内的某些例外情况为保留。 由于它们永远不会由处理器生成,因此如何处理它们并不重要。 做您认为最简洁的事情。

您应该实现的总体控制流程如下所示:

MIT6.828-OS lab3:User Environments 记录_第11张图片

每个异常或中断都应在trapentry.S中具有自己的处理程序。trap_init()应使用这些处理程序的地址初始化IDT。 每个处理程序都应在栈上构建一个结构Trapframe(请参见inc / trap.h),并使用指向Trapframe的指针调用trap()(在trap.c中)。 然后trap()处理异常/中断或将其分派到特定的处理函数。

Exercise 4.编辑trapentry.S和trap.c并实现上述功能。 trapentry.S中的TRAPHANDLER和TRAPHANDLER_NOEC宏以及inc / trap.h中的T_ *定义都应为您提供帮助。 您需要为inc / trap.h中定义的每个trap在trapentry.S中添加一个入口点(使用这些宏),并且您必须提供TRAPHANDLER宏引用的_alltraps。 您还需要修改trap_init()来初始化idt,以指向trapentry.S中定义的每个入口点。 SETGATE宏在这里会有所帮助。

您的_alltraps应该:

1.把值压栈以使栈看起来像结构Trapframe。

2.将GD_KD加载到%ds和%es中。

3. pushl %esp传递一个指向Trapframe的指针作为trap()的参数。

4.调用trap(trap可以返回吗?)。

考虑使用pushal指令; 它与结构Trapframe的布局非常吻合。

在进行任何系统调用之前,请使用user目录中的某些导致异常的测试程序来测试陷阱处理代码,例如user / divzero。 此时,您应该能够在divzero,softint和badsegment测试中make grade成功。

调试这个好辛苦。。。首先我们用TRAPHANDLER宏定义我们的中断处理函数的入口。

TRAPHANDLER_NOEC(t_divide, T_DIVIDE)
TRAPHANDLER_NOEC(t_debug, T_DEBUG)
TRAPHANDLER_NOEC(t_nmi, T_NMI)
TRAPHANDLER_NOEC(t_brkpt, T_BRKPT)
TRAPHANDLER_NOEC(t_oflow, T_OFLOW)
TRAPHANDLER_NOEC(t_bound, T_BOUND)
TRAPHANDLER_NOEC(t_illop, T_ILLOP)
TRAPHANDLER_NOEC(t_device, T_DEVICE)
TRAPHANDLER(t_dblflt, T_DBLFLT)
TRAPHANDLER(t_tss, T_TSS)
TRAPHANDLER_NOEC(t_segnp, T_SEGNP)
TRAPHANDLER(t_stack, T_STACK)
TRAPHANDLER(t_gpflt, T_GPFLT)
TRAPHANDLER(t_pgflt, T_PGFLT)
TRAPHANDLER_NOEC(t_fperr, T_FPERR)
TRAPHANDLER(t_align, T_ALIGN)
TRAPHANDLER_NOEC(t_mchk, T_MCHK)
TRAPHANDLER_NOEC(t_simderr, T_SIMDERR)

TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)

然后照着上面说的写_alltraps:

_alltraps:
// 构造trapframe
    pushl %ds
    pushl %es
    pushal
    // 将GD_KD加载到ds和es寄存器
    movl %ss, %eax
    movw %ax, %ds
    movw %ax, %es
    // 传递trapframe指针给trap参数
    pushl %esp
    // 调用trap
    call trap

在trap.c里声明这几十个入口函数。

// 声明中断处理函数
void t_divide();
void t_debug();
void t_nmi();
void t_brkpt();
void t_oflow();
void t_bound();
void t_illop();
void t_device();
void t_dblflt();
void t_tss();
void t_segnp();
void t_stack();
void t_gpflt();
void t_pgflt();
void t_fperr();
void t_align();
void t_mchk();
void t_simderr();
void t_syscall();

在trap_init里初始化idt table。注意,这里的第二个参数,填1和填0是不一样的,区别是eflags的IF标记,这个会在下一章用到。

void
trap_init(void)
{
    extern struct Segdesc gdt[];

    // LAB 3: Your code here.
    // 初始化idt表
    SETGATE(idt[T_DIVIDE], 0, GD_KT, t_divide, 0); 
    SETGATE(idt[T_DEBUG], 0, GD_KT, t_debug, 0); 
    SETGATE(idt[T_NMI], 0, GD_KT, t_nmi, 0); 
    SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3); 
    SETGATE(idt[T_OFLOW], 0, GD_KT, t_oflow, 0); 
    SETGATE(idt[T_BOUND], 0, GD_KT, t_bound, 0); 
    SETGATE(idt[T_ILLOP], 0, GD_KT, t_illop, 0); 
    SETGATE(idt[T_DEVICE], 0, GD_KT, t_device, 0); 
    SETGATE(idt[T_DBLFLT], 0, GD_KT, t_dblflt, 0); 
    SETGATE(idt[T_TSS], 0, GD_KT, t_tss, 0); 
    SETGATE(idt[T_SEGNP], 0, GD_KT, t_segnp, 0); 
    SETGATE(idt[T_STACK], 0, GD_KT, t_stack, 0); 
    SETGATE(idt[T_GPFLT], 0, GD_KT, t_gpflt, 0); 
    SETGATE(idt[T_PGFLT], 0, GD_KT, t_pgflt, 0); 
    SETGATE(idt[T_FPERR], 0, GD_KT, t_fperr, 0); 
    SETGATE(idt[T_ALIGN], 0, GD_KT, t_align, 0); 
    SETGATE(idt[T_MCHK], 0, GD_KT, t_mchk, 0); 
    SETGATE(idt[T_SIMDERR], 0, GD_KT, t_simderr, 0); 
    SETGATE(idt[T_SYSCALL], 0, GD_KT, t_syscall, 3); 
    
    // Per-CPU setup 
    trap_init_percpu();
}

这样就可以处理这些中断和异常了。具体的哪些中断/异常怎么处理,这里先不考虑。。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

Questions

1.为每个异常/中断使用单独的处理程序功能的目的是什么? (即,如果所有异常/中断都传递给了相同的处理程序,则无法提供当前实现中存在的功能?)

answer:其实我觉得用同一个入口是可以的吧。。。实际上他们的入口最终都是_alltraps,之所以在这之前要再加个不同的入口就是为了参数统一,可以把中断向量跟error code压栈,因为不同的异常/中断不一定需要error code。

2.您是否需要做任何事情来使user/ softint程序正常运行? grade脚本期望它会产生一般的保护错误(trap13),但是softint的代码显示为int $ 14。 为什么要产生中断向量13? 如果内核实际上允许softint的int $ 14指令调用内核的页面错误处理程序(即中断向量14),会发生什么情况?

answer:14是的特权级别是0,也就是内核才能处理这个异常,所以通过系统调用产生中断来调用是不允许的,故会产生中断向量13,如果允许的话那应该就只会产生中断向量14。

到此为止,part A结束。

 

Part B: Page Faults, Breakpoints Exceptions, and System Calls

现在,您的内核具有基本的异常处理功能,您将对其进行优化,以提供依赖于异常处理的重要操作系统原语。

Handling Page Faults

页面错误异常,中断向量14(T_PGFLT),是一个特别重要的异常,我们将在本lab以及下一个lab中大量使用它。 当处理器发生页面故障时,它将导致故障的线性(即虚拟)地址存储在特殊的处理器控制寄存器CR2中。 在trap.c中,我们提供了一个特殊的函数page_fault_handler()的开头,用于处理页面错误异常。

Exercise 5. 修改trap_dispatch()以将页面错误异常调度到page_fault_handler()。 现在,您应该能够在faultread, faultreadkernel, faultwrite, and faultwritekernel tests中make grade成功。 如果其中任何一个不能工作,请找出原因并修复。 请记住,您可以使用make run-x或make run-x-nox将JOS引导到特定的用户程序中。

在实施系统调用时,您将在下面进一步完善内核的页面错误处理。

这个练习不是白给?直接给代码吧。。

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch(tf->tf_trapno){
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        default:
        // Unexpected trap: The user process or the kernel has a bug.
            print_trapframe(tf);
            if (tf->tf_cs == GD_KT)
                panic("unhandled trap in kernel");
            else {
                env_destroy(curenv);
                return;
            }
    }   
}

The Breakpoint Exception

断点异常中断向量3(T_BRKPT)通常用于允许调试器通过用特殊的1字节int3软件中断指令临时替换相关的程序指令,从而在程序代码中插入断点。 在JOS中,我们将通过将其转变为原始的伪系统调用(任何用户环境都可以用来调用JOS内核监视器)来稍微滥用此异常。 如果我们将JOS内核监视器视为原始调试器,那么这种用法实际上是适当的。 例如,lib / panic.c中panic()的用户态实现在显示其panic消息后执行int3。

Exercise 6.修改trap_dispatch()以使断点异常调用内核监视器。 现在,您应该能在breakpoint test中make grade成功。

也是白给。

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch(tf->tf_trapno){
        case T_BRKPT:
            monitor(tf);
            break;
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        default:
        // Unexpected trap: The user process or the kernel has a bug.
            print_trapframe(tf);
            if (tf->tf_cs == GD_KT)
                panic("unhandled trap in kernel");
            else {
                env_destroy(curenv);
                return;
            }
    }
}

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

Questions

3.break point test用例将生成断点异常或常规保护错误,具体取决于您如何初始化IDT中的断点条目(即,从trap_init调用SETGATE的方式)。 为什么? 您如何设置它才能使断点异常按上述方式工作,什么不正确的设置将导致它触发一般性保护故障?

answer:这问题不是上面才问答过嘛。。就是权限必须得3,因为是在用户态通过int指令来中断的。

4.您认为这些机制的目的是什么,尤其是考虑到user/ softint测试程序的作用?

answer:不允许用户随意的进入内核态,引发各种错误。

System calls

用户进程要求内核通过调用系统调用来为它们做事。 当用户进程调用系统调用时,处理器进入内核模式,处理器和内核协作以保存用户进程的状态,内核执行适当的代码以执行系统调用,然后恢复用户进程。 用户进程如何引起内核的注意以及如何指定要执行的调用的确切细节因系统而异。

在JOS内核中,我们将使用int指令。 特别是,我们将int $ 0x30用作系统调用中断。 我们为您定义了常数T_SYSCALL为48(0x30)。 您将必须设置中断描述符以允许用户进程引起该中断。 请注意,中断0x30不能由硬件生成,因此不会由于允许用户代码生成而引起歧义。

应用程序将在寄存器中传递系统调用号和系统调用参数。 这样,内核就无需在用户环境的栈或指令流中乱搞。 系统调用号将以%eax开头,参数(最多五个)将分别以%edx,%ecx,%ebx,%edi和%esi开头。 内核将返回值传回%eax。 在lib / syscall.c中的syscall()中已为您编写了用于调用系统调用的汇编代码。 您应该熟读它,并确保您了解他是怎么运行的。

Exercise 7. 在内核中为中断向量T_SYSCALL添加一个处理程序。 您将必须编辑kern / trapentry.S和kern / trap.c的trap_init()。 您还需要更改trap_dispatch()以处理系统调用中断,方法是使用适当的参数调用syscall()(在kern / syscall.c中定义),然后最后将返回值以%eax的形式传递回用户进程,您需要在kern / syscall.c中实现syscall()。 如果系统调用号无效,请确保syscall()返回-E_INVAL。 您应该阅读并理解lib / syscall.c(尤其是内联汇编例程),以确认您对系统调用接口的理解。 您可能还会发现阅读inc / syscall.h很有帮助。

在内核下运行user/ hello程序(make run-hello)。 它应该在控制台上打印“ hello,world”,然后在用户态下导致页面错误。 如果这没有发生,则可能意味着您的系统调用处理程序不太正确。 现在,您还应该能够在testbss test中make grade成功。

因为我之前已经加了系统调用的门了,所以只需要处理其他地方就ok了。首先是trap_dispatch。

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch(tf->tf_trapno){
        case T_BRKPT:
            monitor(tf);
            break;
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        case T_SYSCALL:{
            uint32_t eax = tf->tf_regs.reg_eax;
            uint32_t ebx = tf->tf_regs.reg_ebx;
            uint32_t ecx = tf->tf_regs.reg_ecx;
            uint32_t edx = tf->tf_regs.reg_edx;
            uint32_t edi = tf->tf_regs.reg_edi;
            uint32_t esi = tf->tf_regs.reg_esi;
            syscall(eax, edx, ecx, ebx, edi, esi);
            break;
        }
        default:
        // Unexpected trap: The user process or the kernel has a bug.
            print_trapframe(tf);
            if (tf->tf_cs == GD_KT)
                panic("unhandled trap in kernel");
            else {
                env_destroy(curenv);
                return;
            }
    }   
}

要是参数搞不明白就去lib/syscall.c里对照一下参数。接下来是kern/syscall.c文件。

// Dispatches to the correct kernel function, passing the arguments.
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) 
{
    // Call the function corresponding to the 'syscallno' parameter.
    // Return any appropriate return value.
    // LAB 3: Your code here.
    switch(syscallno){
        case SYS_cputs:
            sys_cputs((char *)a1, a2);
            return 0;
        case SYS_cgetc:
            return sys_cgetc();
        case SYS_getenvid:
            return sys_getenvid();
        case SYS_env_destroy:
            return sys_env_destroy(a1);
        default:
            return -E_INVAL;
    }   
}

再看关键的sys_cputs函数。

static void
sys_cputs(const char *s, size_t len)
{
    // Check that the user has permission to read memory [s, s+len).
    // Destroy the environment if not.

    // LAB 3: Your code here.
    unsigned cur_va = ROUNDDOWN((unsigned)s, PGSIZE);
    unsigned end_va = ROUNDUP((unsigned)s + len, PGSIZE);
    while(cur_va < end_va){
        //判页目录表项 
        pde_t *pgdir = curenv->env_pgdir;
        pde_t pgdir_entry = pgdir[PDX(cur_va)];
        if(!(pgdir_entry & PTE_U))
            goto memory_errors;
        //判页表项
        pte_t *pg_address = KADDR(PTE_ADDR(pgdir_entry));
        pte_t pg_entry = pg_address[PTX(cur_va)];
        if(!(pg_entry & PTE_U))
            goto memory_errors;
        cur_va += PGSIZE;
    }

    // Print the string supplied by the user.
    cprintf("%.*s", len, s);
    return;
memory_errors:
    env_destroy(curenv);
}

这个还是很烦的哦,搞得我又去翻了一遍lab2,看看有哪些宏能用。。。。注释应该写得蛮清楚了。然后这个练习就解决了。

--------------------------------------------------------------------------------------------------------------------------------

TODO   challenge

--------------------------------------------------------------------------------------------------------------------------------

User-mode startup

用户程序开始在lib / entry.S的顶部运行。 进行一些设置后,lib / libmain.c中的代码会调用libmain()。 您应该修改libmain(),以初始化全局指针thisenv指向envs []数组中对应的environment的结构Env。 (请注意,lib / entry.S已经定义了env,以指向您在A部分中设置的UENVS映射。)提示:查看inc / env.h并使用sys_getenvid。

然后,libmain()调用umain,在hello程序的例子里,它位于user / hello.c中。 请注意,在打印“ hello,world”之后,它将尝试访问thisenv-> env_id。 这就是为什么它较早出现故障。 既然您已经正确初始化了thisenv,它就应该不会出错。 如果仍然存在问题,则您可能尚未将UENVS区域映射为用户可读(返回pmap.c中的A部分;这里是我们第一次实际使用UENVS区域)。

Exercise 8.将所需的代码添加到用户库,然后启动内核。 您应该看到user / hello打印“ hello,world”,然后打印“ i am environment 00001000”。 然后,user/ hello尝试通过调用sys_env_destroy()来“退出”(请参阅lib / libmain.c和lib / exit.c)。 由于内核当前仅支持一个用户环境,因此应报告其已破坏了唯一的环境,然后放入内核监视器。 你现在应该能在hello test make grade成功。

就改一行,没啥好说的。

void
libmain(int argc, char **argv)
{
    // set thisenv to point at our Env structure in envs[].
    // LAB 3: Your code here.
    thisenv = envs;

    // save the name of the program so that panic() can use it
    if (argc > 0)
        binaryname = argv[0];

    // call user main routine
    umain(argc, argv);

    // exit gracefully
    exit();
}

Page faults and memory protection

内存保护是操作系统的一项重要功能,可确保一个程序中的错误不会破坏其他程序或破坏操作系统本身。

操作系统通常依靠硬件支持来实现内存保护。 操作系统使硬件知道哪些虚拟地址有效,哪些无效。 当程序尝试访问无效地址或没有权限的地址时,处理器会在导致故障的指令处停止程序,然后使用有关尝试操作的信息trap到内核。 如果该错误是可修复的,则内核可以对其进行修复,并让程序继续运行。 如果故障无法修复,则程序将无法继续,因为它将永远不会通过导致故障的指令。

一个可修复故障的示例,比如自动扩展栈。 在许多系统中,内核最初分配一个栈页面,然后,如果程序无法访问栈中更远的页面,内核将自动分配这些页面并使程序继续。 这样,内核仅分配程序需要的栈内存,但是程序可以在其拥有任意大栈的错觉下工作。

系统调用提出了一个有趣的内存保护问题。 大多数系统调用接口都允许用户程序将指针传递给内核。 这些指针指向要读取或写入的用户缓冲区。 然后,内核在执行系统调用时解除引用这些指针。 这有两个问题:

1. 内核中的页面错误可能比用户程序中的页面错误严重得多。 如果内核在处理自己的数据结构时发生页面错误,那就是内核错误,并且错误处理程序应该让内核(进而整个系统)panic。 但是,当内核解除引用用户程序给它的指针时,它需要一种方法来记住这些解除引用引起的任何页面错误实际上都代表用户程序。

2.内核通常比用户程序具有更多的内存权限。 用户程序可能会传递一个指向系统调用的指针,该指针指向内核可以读取或写入但程序无法读取的内存。 内核必须小心,不要被欺骗去解引用这样的指针,因为这可能会泄露私有信息或破坏内核的完整性。

由于这两个原因,内核在处理用户程序提供的指针时必须格外小心。

现在,您将通过一种机制来仔细检查这两个问题,该机制将仔细检查从用户空间传递到内核的所有指针。 当程序将指针传递给内核时,内核将检查地址是否在地址空间的用户部分中,以及页表是否允许进行内存操作。

因此,内核将永远不会由于解除用户提供的指针的引用而遇到页面错误。 如果内核确实发生页面错误,则它应该崩溃并终止。

Exercise 9.更改kern/trap.c,如果内核态发生页面错误则panic。

Hint: 要确定是在用户态下还是内核态下发生故障,请检查tf_cs的低位。

查阅相关资料可以知道,cs寄存器的低两位表示特级权限,0是用户程序。

void
page_fault_handler(struct Trapframe *tf)
{
    uint32_t fault_va;

    // Read processor's CR2 register to find the faulting address
    fault_va = rcr2();

    // Handle kernel-mode page faults.

    // LAB 3: Your code here.
    if(!(tf->tf_cs & 3))
        panic("page_fault_handler: a page fault occurred in the kernel");

    // We've already handled kernel-mode exceptions, so if we get here,
    // the page fault happened in user mode.

    // Destroy the environment that caused the fault.
    cprintf("[%08x] user fault va %08x ip %08x\n",
        curenv->env_id, fault_va, tf->tf_eip);
    print_trapframe(tf);
    env_destroy(curenv);
}

 

阅读kern / pmap.c中的user_mem_assert,并在同一文件中实现user_mem_check。

将kern / syscall.c更改为对系统调用的参数检查他的安全性。

启动您的内核,运行user / buggyhello。 应该会销毁environment,并且内核不会panic。您应该看到:

最后,更改kern / kdebug.c中的debuginfo_eip以对usd,stabs和stabstr调用user_mem_check。 如果现在运行user/breakpoint,则应该能够从内核监视器运行backtrace,并在内核因页面错误而崩溃之前查看遍历lib / libmain.c的backtrace。 是什么原因导致此页面错误? 您不需要修复它,但是您应该了解它为什么会发生这种情况。

请注意,您刚刚实现的相同机制也适用于恶意用户应用程序(例如user / evilhello)。

Exercise 10.启动您的内核,运行user/ evilhello。 应该会销毁environment,并且内核不会panic。 您应该看到:

这样就完成了实验。 确保您通过了所有的make grade test。

先来看一下user_mem_assert。

MIT6.828-OS lab3:User Environments 记录_第12张图片

检查environment env是否允许以perm|PTE_T|PTE_P的权限访问在[va, va+len)的内存。如果可以,那么这个函数就直接返回。如果不能,env就会销毁,如果env是当前的environment,那么这个函数不会返回。

MIT6.828-OS lab3:User Environments 记录_第13张图片

代码也简单,就是调用了一下user_mem_check,如果无法访问就打印并且销毁。再看user_mem_check。

MIT6.828-OS lab3:User Environments 记录_第14张图片

检查environment env是否允许以perm|PTE_P的权限访问在[va, va+len)的内存。通常perm将至少包含PTE_U,但是这不是必须的。va和len不需要页面对齐,你必须测试包含该范围的每个页面。你将会测试 len/PGSIZE, len/PGESIZE + 1或者len/PGESIZE + 2个页面。

一个用户程序能够访问一个在ULIM下方并且页表给了权限的虚拟地址。这些正是你该在这里实现的。

如果有出现错误,设置user_mem_check_addr的值为第一个错误的虚拟地址。

如果可以访问则返回0,否则返回-E_FAULT。

很清楚要求了,直接把之前的代码搬过来改改就ok了。

int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
    // LAB 3: Your code here.
    unsigned cur_va = ROUNDDOWN((unsigned)va, PGSIZE);
    unsigned end_va = ROUNDUP((unsigned)va + len, PGSIZE);
    perm |= PTE_P;
    while(cur_va < end_va){
        //判页目录表项 
        pde_t *pgdir = curenv->env_pgdir;
        pde_t pgdir_entry = pgdir[PDX(cur_va)];
        if(cur_va > ULIM || (pgdir_entry & perm) != perm)
            goto memory_errors;
        //判页表项
        pte_t *pg_address = KADDR(PTE_ADDR(pgdir_entry));
        pte_t pg_entry = pg_address[PTX(cur_va)];
        if(cur_va > ULIM || (pg_entry & perm) != perm)
            goto memory_errors;
        cur_va += PGSIZE;
    }    
    return 0;
memory_errors:
    user_mem_check_addr = (cur_va > (unsigned)va ? cur_va : (unsigned)va);
    return -E_FAULT;
}

然后是另一个文件。直接给代码吧。

    } else {
        // The user-application linker script, user/user.ld,
        // puts information about the application's stabs (equivalent
        // to __STAB_BEGIN__, __STAB_END__, __STABSTR_BEGIN__, and
        // __STABSTR_END__) in a structure located at virtual address
        // USTABDATA.
        const struct UserStabData *usd = (const struct UserStabData *) USTABDATA;

        // Make sure this memory is valid.
        // Return -1 if it is not.  Hint: Call user_mem_check.
        // LAB 3: Your code here.
        if(user_mem_check(curenv, usd, sizeof(struct UserStabData), PTE_U))
            return -1; 

        stabs = usd->stabs;
        stab_end = usd->stab_end;
        stabstr = usd->stabstr;
        stabstr_end = usd->stabstr_end;

        // Make sure the STABS and string table memory is valid.
        // LAB 3: Your code here.
        if(user_mem_check(curenv, stabs, sizeof(struct Stab) * (stab_end - stabs), PTE_U))
            return -1; 
        if(user_mem_check(curenv, stabstr, stabstr_end - stabstr, PTE_U))
            return -1; 

这部分的代码我不确定他有没有机制检查,理论上来说这样写就可以了。。要是错了请大佬纠正。

最后make grade如下。

MIT6.828-OS lab3:User Environments 记录_第15张图片

 

总结

稍微提一下中断,中断分为内中断和外中断,或者叫异常和中断。

他们的区别在于IF标记,内中断不会reset IF flag,而外中断会reset。

你可能感兴趣的:(MIT6.828-OS,linux操作系统)