Introduction
在本实验中,您将实现运行受保护的用户态环境(即“进程”)所需的基本内核设施。您将增强JOS内核,以设置数据结构来跟踪用户环境、创建单个用户环境、将程序映像加载到环境中并启动它运行。您还将使JOS内核能够处理用户环境发出的任何系统调用,并处理它引起的任何其他异常。
注意:在本实验中,环境和进程这两个术语是可以互换的——它们都指的是允许你运行程序的抽象。我们引入“环境”这个术语,而不是传统的“进程”,是为了强调JOS环境和UNIX进程提供了不同的接口,并且没有提供相同的语义。
Getting Started
在提交lab2之后(如果有的话),使用Git提交您的更改,获取最新版本的课程存储库,然后基于我们的lab3分支origin/lab3创建一个名为lab3的本地分支:
Lab Requirements
这个实验分为两部分,A部分和B部分,A部分在这个实验分配后一周交;您应该在部分截止日期之前提交您的更改并提交您的实验室,确保您的代码通过所有的A部分测试(如果您的代码尚未通过B部分测试也没有关系)。你只需要让B部分测试在第二周结束前通过就可以了。
与实验2一样,你需要完成实验中描述的所有常规练习和至少一个挑战性问题(针对整个实验,而不是每个部分)。在实验室目录的最顶层的answers-lab3.txt文件中,写下实验中提出的问题的简要答案,以及解决所选挑战问题的一到两段描述。(如果你实现了多个挑战性问题,你只需要在文章中描述其中一个。)不要忘记在提交时使用git add answers-lab3.txt包含答案文件。
Inline Assembly
在本实验中,您可能会发现GCC的内联汇编语言特性很有用,尽管也可以不使用它来完成本实验。至少,你需要能够理解我们提供的源代码中已经存在的内联汇编语言片段(“asm”语句)。您可以在class reference materials页面上找到有关GCC内联汇编语言的几个信息来源。
Part A: User Environments and Exception Handling
新的include文件inc/env.h包含了JOS中用户环境的基本定义。现在就读吧。内核使用Env数据结构来跟踪每个用户环境。在这个实验室中,您最初只创建一个环境,但您将需要设计JOS内核以支持多个环境;实验4将通过允许用户环境fork其他环境来利用此功能。
读者可以在kern/env.c中看到,内核维护了与环境相关的3个主要全局变量。
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
一旦JOS启动并运行,envs指针将指向表示系统中所有环境的Env结构的数组。在我们的设计中,JOS内核将支持最多的同时活动的NENV环境,尽管在任何给定的时间内运行的环境通常要少得多。(NENV是一个常量#,定义在inc/env.h中。)一旦分配了它,envs数组将包含每个NENV可能环境的Env数据结构的单个实例。
JOS内核将所有不活动的Env结构都保存在env_free_list中。这种设计使得环境的分配和释放变得容易,因为只需要向未使用环境链表添加或删除环境。
内核使用curenv符号来跟踪任意给定时间的当前执行环境。在启动过程中,在第一个环境运行之前,curenv最初被设置为NULL。
Environment State
Env结构体在inc/ Env .h中定义如下(尽管在未来的实验室中还会添加更多字段):
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};
以下是Env字段的用途:
env_tf:
该结构定义在inc/trap.h中,保存了在环境不运行时为该环境保存的寄存器值:即在内核或其他环境运行时。内核在从用户态切换到核心态时保存了这些数据,以便稍后可以从中断的位置恢复环境。
env_link:
这是到env_free_list中的下一个环境的链接。Env_free_list指向链表上的第一个空闲环境。
env_id:
内核在这里存储了一个值,唯一标识了当前使用该Env结构的环境(即使用envs数组中的特定槽位)。在用户环境结束后,内核可以将相同的Env结构重新分配到不同的环境,但新环境的env_id与旧环境的env_id不同,即使新环境重用了envs数组中的相同槽位。
env_parent_id:
内核在这里存储了创建该环境的环境的env_id。通过这种方式,环境可以形成一个“族谱”,这将有助于做出关于哪些环境可以对谁执行什么操作的安全决策。
env_type:
这用于区分特殊环境。对于大多数环境,它将是ENV_TYPE_USER。我们将在后面的实验室中介绍更多用于特殊系统服务环境的类型。
env_status:
这个变量包含下列值之一。
ENV_FREE:
表示Env结构是不活跃的,因此在env_free_list中。
ENV_RUNNABLE:
表示Env结构表示等待在处理器上运行的环境。
ENV_RUNNING:
表示Env结构体表示当前运行的环境。
ENV_NOT_RUNNABLE:
表示Env结构体表示当前活动的环境,但目前还没有准备好运行:例如,因为它正在等待来自另一个环境的进程间通信(IPC)。
ENV_DYING:
表示Env结构体表示僵尸环境。僵尸环境将在下次捕获到内核时被释放。直到实验4我们才会使用这个标志。
env_pgdir:
该变量保存了该环境的页目录的内核虚拟地址。
像Unix进程一样,JOS环境耦合了“线程”和“地址空间”的概念。线程主要由保存的寄存器(env_tf字段)定义,地址空间由页目录和env_pgdir指向的页表定义。为运行环境,内核必须用保存的寄存器和适当的地址空间设置CPU。
我们的struct Env类似于xv6中的struct proc。这两个结构都将环境(即进程)的用户态寄存器状态保存在Trapframe结构中。在JOS中,各个环境不像xv6中的进程那样有自己的内核栈。同一时间内核中只能有一个活动的JOS环境,因此JOS只需要一个内核栈。
Allocating the Environments Array
在实验2中,你在mem_init()中为pages[]数组分配了内存,这是一个表,内核使用它来跟踪哪些页是空闲的,哪些是空闲的。您现在需要进一步修改mem_init(),以分配一个类似的Env结构数组,称为envs。
练习1
修改kern/pmap.c中的mem_init()来分配和映射envs数组。该数组由分配的Env结构的NENV实例组成,与分配pages数组的方式非常相似。与pages数组类似,在UENVS(定义在inc/memlayout.h中)中,内存支持环境也应该映射为只读用户,以便用户进程可以从该数组中读取。
你应该运行代码并确保check_kern_pgdir()成功。
Creating and Running Environments
现在,我们将在kern/env.c中编写运行用户环境所需的代码。因为我们还没有文件系统,我们将设置内核加载一个嵌入到内核本身中的静态二进制映像。JOS将该二进制文件作为ELF可执行映像嵌入到内核中。
实验3的GNUmakefile在obj/user/目录下生成了许多二进制图像。如果您查看kern/Makefrag,您会注意到一些魔法,它将这些二进制文件直接“链接”到内核可执行文件中,就像它们是.o文件一样。链接器命令行上的-b binary选项会将这些文件链接为“原始的”未解释的二进制文件,而不是编译器生成的普通的.o文件。(就链接器而言,这些文件根本不必是ELF图像——它们可以是任何东西,例如文本文件或图片!)如果你看obj/kern/kernel.sym在构建内核之后,读者会注意到,链接器“神奇地”产生了许多有趣的符号,它们的名字晦涩难懂,如_binary_obj_user_hello_start、_binary_obj_user_hello_end和_binary_obj_user_hello_size。链接器通过修改二进制文件的文件名来生成这些符号名。这些符号为普通的内核代码提供了一种引用嵌入二进制文件的方法。
在kern/init.c的i386_init()中,你将看到在环境中运行这些二进制映像的代码。但是,用于建立用户环境的关键功能还不完整;你需要填写它们。
练习2
在文件env.c中,完成以下函数的编码:
env_init ()
在envs数组中初始化所有的Env结构体,并将它们添加到env_free_list。还调用env_init_percpu,该函数将分段硬件配置为特权级别0(内核)和特权级别3(用户)分别对应的段。
env_setup_vm ()
为新环境分配一个页目录,并初始化新环境地址空间的内核部分。
region_alloc ()
为环境分配和映射物理内存
load_icode ()
您将需要解析ELF二进制映像,就像启动加载程序已经做的那样,并将其内容加载到新环境的用户地址空间中。
env_create ()
用env_alloc分配一个环境,并调用load_icode将ELF二进制文件加载到环境中。
env_run ()
启动在用户模式下运行的给定环境。
在编写这些函数时,您可能会发现新的cprintf verb %e很有用——它打印与错误代码相对应的描述。例如,
r = -E_NO_MEM;
panic ("env_alloc: %e", r);
会报出“env_alloc: out of memory”。
下面是直到调用用户代码为止的代码调用图。确保你理解了每个步骤的目的。
![](https://i-blog.csdnimg.cn/blog_migrate/a5d28ee461a574b2bebb05abe299e632.png)
完成后,应该编译内核并在QEMU下运行。如果一切顺利,你的系统应该进入用户空间并执行二进制文件hello,直到它用int指令发起一个系统调用。这样就会有麻烦了,因为JOS还没有设置硬件来允许从用户空间到内核的任何类型的转换。当CPU发现它没有设置好来处理这个系统调用中断时,它将产生一个通用保护异常,发现它不能处理这个异常,产生一个双重故障异常,发现它也不能处理这个异常,最后以所谓的“三重故障”放弃。通常,你会看到CPU重置和系统重启。虽然这对于遗留应用程序很重要(请参阅这篇博客文章以了解原因 this blog post ),但对于内核开发来说,这是一个痛苦的过程,因此使用6.828打补丁的QEMU,您将看到一个寄存器转储和一个“三重故障”消息。
稍后我们会解决这个问题,但现在我们可以使用调试器来检查是否进入了用户模式。使用make qemu-gdb并在env_pop_tf处设置一个GDB断点,这应该是实际进入用户模式之前遇到的最后一个函数。使用si单步执行此函数;在iret指令之后,处理器应该进入用户模式。然后,您应该在用户环境的可执行文件中看到第一条指令,即lib/entry.S中标签start处的cmpl指令。现在使用b *0x…在hello中sys_cputs()的int $0x30处设置断点(参见obj/user/hello. php)。Asm用于用户空间地址)。这个int是向控制台显示字符的系统调用。如果你不能像int那样执行,那么你的地址空间设置或程序加载代码有问题;在继续之前回去修复它。
Handling Interrupts and Exceptions
此时,用户空间中的第一个int $0x30系统调用指令是一条死胡同:一旦处理器进入用户模式,就没有办法退出。大家现在需要实现基本的异常和系统调用处理,以便内核能够从用户态代码恢复对处理器的控制。你应该做的第一件事是彻底熟悉x86中断和异常机制。
练习 3
Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer's Manual (or Chapter 5 of the IA-32 Developer's Manual), if you haven't already.
在本实验室中,我们通常遵循英特尔的中断、异常等术语。但异常、陷阱、中断、故障和abort等术语在不同的体系结构或操作系统上没有标准含义,在使用时往往忽略了它们在特定体系结构(如x86)上的细微差别。当你在实验室之外看到这些术语时,其含义可能略有不同。
Basics of Protected Control Transfer
保护控制转移的基础知识
异常和中断都是“受保护的控制传输”,这会导致处理器从用户态切换到核心态(CPL=0),而不会给用户态代码任何干扰内核或其他环境的功能的机会。按照英特尔的术语,中断是一种受保护的控制转移,由处理器外部的异步事件引起,例如外部设备I/O活动的通知。与此相反,异常是由当前运行的代码同步引起的受保护的控制传输,例如除零或无效的内存访问。
为了确保这些受保护的控制传输实际上得到保护,处理器的中断/异常机制被设计为在中断或异常发生时当前运行的代码,不能任意选择进入内核的位置或方式。相反,处理器确保只能在经过仔细控制的条件下进入内核。在x86上,有两种机制协同工作来提供这种保护:
1.中断描述符表:处理器确保中断和异常只能导致内核进入几个特定的、定义良好的入口点,这些入口点由内核自身确定,而不是由中断或异常发生时运行的代码确定。
x86允许最多256个不同的中断或异常入口点进入内核,每个入口点都有不同的中断向量。向量是一个0到255之间的数字。中断向量由中断的来源确定:不同的设备、错误条件和应用程序对内核的请求以不同的向量产生中断。CPU使用该向量作为处理器中断描述符表(interrupt descriptor table, IDT)的索引,该表由内核建立在内核私有内存中,类似于GDT。处理器从该表中适当的项加载:
加载到指令指针(EIP)寄存器中的值,指向指定处理该类型异常的内核代码。
加载到代码段(CS)寄存器中的值,在比特0-1中包含异常处理程序运行时的特权级别。(在JOS中,所有异常都在内核模式下处理,特权级别为0。)
中断描述符表:处理器确保异常或中断发生时,只会跳转到由内核定义的代码点处执行。x86允许256种不同的中断或异常进入点,每一个都有一个向量号,从0到255。CPU使用向量号作为IDT的索引,取出一个IDT描述符,根据IDT描述符可以获取中断处理函数cs和eip的值,从而进入中断处理函数执行。
2.任务状态段:处理器需要一个位置来保存中断或异常发生之前的旧处理器状态,例如处理器调用异常处理程序之前的EIP和CS的原始值,以便异常处理程序稍后可以恢复旧状态,并从中断的地方恢复中断的代码。但该保存旧处理器状态的区域必须保护,不受非特权用户态代码的影响。否则,有bug或恶意用户代码可能危及内核。
因此,当x86处理器接受中断或陷阱,导致特权级别从用户态切换到核心态时,它也会切换到内核内存中的栈。一个称为任务状态段(TSS)的结构指定了段选择器和栈所在的地址。处理器把SS、ESP、EFLAGS、CS、EIP和一个可选的错误码(error code)压入(push)到这个新栈上。然后从中断描述符加载CS和EIP,并设置ESP和SS指向新的栈。
尽管TSS很大,而且可能用于各种目的,但JOS只使用它来定义处理器从用户态传输到核心态时应该切换到的内核栈。因为JOS中的“内核模式”在x86上是特权级别0,所以处理器使用TSS的ESP0和SS0字段来定义进入内核模式时的内核栈。JOS不使用任何其他TSS字段。
任务状态段(TSS):当x86异常发生,并且发生了从用户模式到内核模式的转换时,处理器也会进行栈切换。一个叫做task state segment (TSS)的结构指定了栈的位置。TSS是一个很大的数据结构,由于JOS中内核模式就是指权限0,所以处理器只使用TSS结构的ESP0和SS0两个字段来定义内核栈,其它字段不使用。那么内核如何找到这个TSS结构的呢?JOS内核维护了一个 static struct Taskstate ts;的变量,然后在trap_init_percpu()函数中,设置TSS选择子(使用ltr指令)。
Types of Exceptions and Interrupts
x86处理器内部产生的所有同步异常都使用0到31之间的中断向量,因此映射到IDT项0到31。例如,缺页异常总是导致向量14的异常。大于31的中断向量只用于软件中断(由int指令产生)或异步硬件中断(由外部设备在需要注意时引起)。
在本节中,我们将扩展JOS以处理向量0 ~ 31中内部生成的x86异常。在下一节中,我们将让JOS处理软件中断向量48 (0x30), JOS(相当随意地)将其用作其系统调用中断向量。在实验4中,我们将扩展JOS以处理外部产生的硬件中断,如时钟中断。
0-31号中断都是同步中断,缺页中断就是14号,31号以上的中断可以由int指令,或者外部设备触发。在JOS中,将用48号中断作为系统调用中断。
An Example
让我们将这些片段放在一起,并通过一个示例进行跟踪。假设处理器在用户环境中执行代码,遇到一条试图除0的divide指令。
处理器切换到由TSS的SS0和ESP0字段定义的栈,在JOS中,它们将分别保存GD_KD和KSTACKTOP值。
处理器从地址KSTACKTOP开始,将异常参数压入内核栈
![](https://i-blog.csdnimg.cn/blog_migrate/55ec73c992ab3260c1129a387350e19f.png)
因为我们正在处理一个除法错误,它是x86上的中断向量0,处理器读取IDT条目0并设置CS:EIP指向该条目所描述的处理程序函数。
handler函数获得控制权并处理异常,例如终止用户环境。
对于某些类型的x86异常,除了上述“标准”的5个字之外,处理器还会向栈中压入另一个包含错误码的字。缺页异常(14)是一个重要的例子。请参阅80386手册,以确定处理器为哪些异常号推送错误码,以及在这种情况下错误码的含义。当处理器压入一个错误代码时,从用户模式进入异常处理器的栈将如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/7a15df3cfdd6507298fb7944757eafcc.png)
Nested Exceptions and Interrupts
处理器可以从核心态和用户态接收异常和中断。但只有在从用户态进入内核时,x86处理器才会在将其旧的寄存器状态压入栈中并通过IDT调用适当的异常处理程序之前,自动切换栈。如果在中断或异常发生时处理器已经处于核心态(CS寄存器的低2位已经为零),那么CPU只是将更多的值压入同一个内核栈。通过这种方式,内核可以优雅地处理内核内部代码引起的嵌套异常。这种能力是实现保护的一个重要工具,我们将在后面的系统调用一节中看到。
如果处理器已经处于核心态并接受一个嵌套异常,因为它不需要切换栈,所以它不保存旧的SS或ESP寄存器。对于没有推送错误代码的异常类型,内核栈在进入异常处理程序时看起来如下所示
![](https://i-blog.csdnimg.cn/blog_migrate/9f5f59e5fdb4821eb4d05ff17ec26cf1.png)
对于推送错误代码的异常类型,处理器会像以前一样,在旧的EIP之后立即推送错误代码。
对于处理器的嵌套异常能力有一个重要的警告。如果处理器在已经处于核心态时发生异常,并且由于任何原因(例如缺乏栈空间)无法将其旧状态压入内核栈,那么处理器就无法恢复,只能进行重置。不用说,内核的设计应该使这种情况不会发生。
Setting Up the IDT
您现在应该具备了设置IDT和在JOS中处理异常所需的基本信息。现在,我们将设置IDT来处理中断向量0 ~ 31(处理器异常)。我们将在稍后的实验中处理系统调用中断,并在稍后的实验中添加中断32-47(设备irq)。
头文件inc/trap.h和kern/trap.h包含了与中断和异常相关的重要定义,你需要熟悉这些定义。文件kern/trap.h包含了严格私有于内核的定义,而inc/trap.h包含的定义也可能对用户级程序和库有用。
注意:在0-31范围内的一些例外由英特尔定义保留。因为它们永远不会由处理器生成,所以如何处理它们并不重要。做你认为最干净的事。
你应该实现的总体控制流程如下所示:
![](https://i-blog.csdnimg.cn/blog_migrate/ec1ff57b329e84c2da76f8e948846db9.png)
每个异常或中断都应该在trapentry.S中有自己的处理程序,并且trap_init()应该用这些处理程序的地址初始化IDT。每个处理程序都应该在栈上构建一个struct Trapframe(参见inc/trap.h),并用一个指向该Trapframe的指针调用trap()(在trap.c中)。然后,trap()处理异常/中断或分派给特定的处理程序函数。
练习4
编辑trapentry.S和trap.c并实现上述特性。trapentry.S和inc/trap.h中的T_*定义 TRAPHANDLER和TRAPHANDLER_NOEC宏应该对你有所帮助。你需要在trapentry.S中添加一个入口点(使用这些宏)用于定义在inc/trap.h中的每个陷阱,并且您必须提供陷阱处理程序宏所引用的_alltraps。你还需要修改trap_init(),使idt指向trapentry.S中定义的每个入口点;SETGATE宏在这里很有用。
你的_alltraps应该:
push values to make the stack look like a struct Trapframe
load GD_KD into %ds and %es
pushl %esp to pass a pointer to the Trapframe as an argument to trap()
call trap (can trap ever return?)
考虑使用pushal指令;它非常适合structTrapframe的布局。
在进行任何系统调用之前,使用用户目录中的一些测试程序(例如user/divzero)来测试陷阱处理代码,这些程序会导致异常。现在,你应该能够让make grade在divzero、soft tint和badsegment测试中取得成功。
Part B: Page Faults, Breakpoints Exceptions, and System Calls
现在内核已经具备了基本的异常处理能力,我们将对其进行改进,以提供依赖于异常处理的重要操作系统原语
Handling Page Faults
缺页异常(中断向量14 (T_PGFLT))是一个特别重要的异常,我们将在本实验和下一个实验中大量使用它。当处理器发生缺页异常时,它将导致该异常的线性(即虚拟)地址存储在一个特殊的处理器控制寄存器CR2中。在trap.c中,我们已经提供了一个用于处理缺页异常的特殊函数,即page_fault_handler()。
缺页中断中断号是14,发生时引发缺页中断的线性地址将会被存储到CR2寄存器中。
练习5
修改trap_dispatch(),将页面异常分派给page_fault_handler()。现在应该能够让make grade在faultread、faultreadkernel、faultwrite和faultwritekernel测试中成功了。如果其中任何一个不起作用,找出原因并修复它们。记住,可以使用make run-x或make run-x-nox将JOS引导到特定的用户程序中。例如,make run-hello-nox运行hello用户程序。
在实现系统调用时,读者将在下文进一步完善内核的缺页异常处理。
The Breakpoint Exception
断点异常、interrupt vector 3, T_BRKPT 通常用于允许调试器在程序代码中插入断点,具体做法是将相关的程序指令临时替换为专门的1字节int3软件中断指令。在JOS中,我们将稍微滥用这个异常,将其转换为任何用户环境都可以使用的原始伪系统调用来调用JOS内核监视器。如果我们将JOS内核监视器视为基本调试器,那么这种用法实际上是适当的。例如,lib/panic.c中panic()的用户模式实现在显示紧急情况消息后执行int3。
断点异常中断号是3,调试器常常插入一字节的int3指令临时替代某条指令,从而引发断点异常。
练习6
修改trap_dispatch(),使断点异常调用内核监视器。现在你应该能够让make grade在断点测试中成功了。
System calls
用户进程通过调用系统调用,要求内核为其做一些事情。在用户进程调用系统调用时,处理器进入核心态,处理器和内核协同保存用户进程的状态,内核执行适当的代码以执行系统调用,然后恢复用户进程。用户进程如何获得内核的注意,以及内核如何指定它想执行的调用,这些细节在不同的系统中是不同的。
在JOS内核中,我们将使用int指令,它会导致处理器中断。特别地,我们将使用int $0x30作为系统调用中断。我们将常数T_SYSCALL定义为48 (0x30)。你必须设置中断描述符,以允许用户进程触发该中断。请注意,中断0x30不能由硬件生成,因此允许用户代码生成中断0x30不会导致歧义。
应用程序将在寄存器中传递系统调用编号和系统调用参数。这样,内核就不需要遍历用户环境的栈或指令流。系统调用编号将以%eax表示,参数(最多5个)将分别以%edx、%ecx、%ebx、%edi和%esi表示。内核将返回值传递回%eax。调用系统调用的汇编代码已经为你写好了,在lib/syscall.c的syscall()中。你应该通读一遍,确保你理解了发生了什么。
JOS使用int指令实现系统调用,使用0x30作为中断号。应用使用寄存器传递系统调用号和参数。系统调用号保存在%eax,五个参数依次保存在%edx, %ecx, %ebx, %edi, %esi中。返回值保存在%eax中。
练习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",然后在用户模式下导致缺页异常。如果没有发生这种情况,可能意味着你的系统调用处理程序不完全正确。您现在还应该能够获得make grade以在testbss测试中获得成功。
需要我们做如下几件事:
1.为中断号T_SYSCALL添加一个中断处理函数
2.在trap_dispatch()中判断中断号如果是T_SYSCALL,调用定义在kern/syscall.c中的syscall()函数,并将syscall()保存的返回值保存到tf->tf_regs.reg_eax等将来恢复到eax寄存器中。
3.修改kern/syscall.c中的syscall()函数,使能处理定义在inc/syscall.h中的所有系统调用。
User-mode startup
一个用户程序从lib/entry.S开头开始运行。在一些设置之后,这段代码调用lib/libmain.c中的libmain()。您应该修改libmain(),使全局指针thisenv指向envs[]数组中的此环境的结构体Env。(注意,lib/envs.S已经定义了envs来指向您在PART A设置的UENVS映射。)提示:查看inc/env.h并使用sys_getenvid。
Libmain()然后调用umain,在hello程序中,umain位于user/hello.c中。注意,在打印“hello, world”之后,它尝试访问thisenv->env_id。这就是它之前出错的原因。现在你已经正确初始化了thisenv,它应该不会出错。如果它仍然错误,您可能还没有映射UENVS区域用户可读(回到pmap.c中的A部分;这是我们第一次真正使用UENVS区域)。
练习 8
将所需的代码添加到用户库,然后启动内核。你会看到user/hello输出"hello, world",然后输出"i am environment 00001000"。user /hello试图通过调用sys_env_destroy() "退出"(参见lib/libmain.c和lib/exit.c)。由于内核目前只支持一个用户环境,它应该报告它已经销毁了唯一的一个环境,然后进入内核监视器。你应该能够在hello测试中make grade。
Page faults and memory protection
内存保护是操作系统的一个重要特性,它确保一个程序中的错误不会破坏其他程序或破坏操作系统本身。
操作系统通常依赖硬件支持来实现内存保护。操作系统通知硬件哪些虚拟地址有效,哪些无效。当程序试图访问无效地址或它没有权限访问的地址时,处理器会在导致错误的指令处停止程序,然后将有关该操作的信息捕获到内核中。如果错误是可修复的,内核可以修复它并让程序继续运行。如果错误不可修复,那么程序就无法继续,因为它永远无法通过导致错误的指令。
作为一个可修复错误的例子,考虑一个自动扩展的堆栈。在许多系统中,内核最初只分配一个栈页,如果程序在访问栈下更远的页时出错,内核将自动分配这些页并让程序继续执行。通过这样做,内核只分配程序所需的栈内存,但程序可以在一种错觉下工作,即它有一个任意大的栈。
系统调用是内存保护方面一个有趣的问题。大多数系统调用接口都允许用户程序向内核传递指针。这些指针指向要读或写的用户缓冲区。内核接下来在执行该系统调用时解引用这些指针。这样做有两个问题:
内核中的缺页异常可能比用户程序中的缺页异常严重得多。如果内核在操作自身的数据结构时发生缺页异常,那就是内核bug,错误处理程序应该扰乱内核(进而影响整个系统)。但在内核解引用户程序提供给它的指针时,它需要一种方法来记住这些解引引起的缺页异常实际上是用户程序引起的。
内核通常比用户程序拥有更多的内存权限。用户程序可以传递一个指向系统调用的指针,该指针指向内核可以读或写但程序不能读的内存。内核必须小心,不要被欺骗去解除对该指针的引用,因为那样可能会泄露私有信息或破坏内核的完整性。
出于这两个原因,内核在处理用户程序提供的指针时必须非常小心。
大家现在将通过一种机制来解决这两个问题,该机制检查从用户空间传递到内核的所有指针。在程序向内核传递一个指针时,内核将检查该地址是否在地址空间的用户部分,并且页表允许进行内存操作。
因而,内核永远不会因为解引用户提供的指针而发生缺页异常。如果内核发生缺页异常,它应该触发panic并终止。
练习 9
Change kern/trap.c to panic if a page fault happens in kernel mode.
Hint: to determine whether a fault happened in user mode or in kernel mode, check the low bits of the tf_cs.
Read user_mem_assert in kern/pmap.c and implement user_mem_check in that same file.
Change kern/syscall.c to sanity check arguments to system calls.
Boot your kernel, running user/buggyhello. The environment should be destroyed, and the kernel should not panic. You should see:
[00001000] user_mem_check assertion failure for va 00000001 [00001000] free env 00001000 Destroyed the only environment - nothing more to do!
最后,修改kern/kdebug.c中的debuginfo_eip,使其对usd、stab和stabstr调user_mem_check。如果您现在运行user/breakpoint,您应该能够从内核监视器运行回溯,并在内核出现缺页异常之前查看回溯遍历lib/libmain.c。什么原因导致缺页异常?你不需要修复它,但你应该理解它为什么会发生。
需要我们做两件事情:
1.首先如果页错误发生在内核态时应该直接panic。
2.实现kern/pmap.c中的user_mem_check()工具函数,该函数检测用户环境是否有权限访问线性地址区域[va, va+len)。然后对在kern/syscall.c中的系统调用函数使用user_mem_check()工具函数进行内存访问权限检查。
请注意,刚才实现的机制也适用于恶意用户应用程序(例如user/evilhello)。
练习 10.
Boot your kernel, running user/evilhello. The environment should be destroyed, and the kernel should not panic. You should see:
[00000000] new env 00001000 ... [00001000] user_mem_check assertion failure for va f010000c [00001000] free env 00001000