全部学习汇总: GreyZhang/g_unix: some basic learning about unix operating system. (github.com)
简介
在这个实验室中,您将实现运行受保护的用户模式环境(即“进程”)所需的基本内核功能。您将增强JOS内核,以设置数据结构来跟踪用户环境,创建单个用户环境,将程序映像加载到其中,并启动它运行。您还将使JOS内核能够处理用户环境进行的任何系统调用,并处理它引起的任何其他异常。
注意:在这个实验室中,术语“环境”和“过程”是可互换的——两者都指的是允许您运行程序的抽象。我们引入了术语“环境”而不是传统的术语“进程”,以强调JOS环境和UNIX进程提供不同的接口,并且不提供相同的语义。
入门
在提交Lab 2后,使用Git提交您的更改(如果有),获取最新版本的课程库,然后基于我们的lab3分支origin/lab3创建一个名为lab3的本地分支:
Lab 3包含许多新的源文件,您应该浏览这些文件:
此外,我们为lab2分发的一些源文件在lab3中进行了修改。要查看差异,您可以键入:
您可能还想再看一看实验室工具指南,因为它包含了有关调试与本实验室相关的用户代码的信息。
实验要求
这个实验分为A和B两部分。A部分将在这个实验分配后一周到期;您应该在A部分截止日期之前提交您的更改并在实验室中进行处理,确保您的代码通过了所有的A部分测试(如果您的代码还没有通过B部分测试也没关系)。你只需要在第二周结束时通过B部分考试。
和在实验室2中一样,你需要做实验室中描述的所有常规练习,以及至少一个挑战问题(针对整个实验室,而不是针对每个部分)。在实验室目录的顶层,在一个名为answers-lab3.txt的文件中,写下对实验室中提出的问题的简短回答,并用一两段话描述你为解决所选挑战问题所做的事情。(如果你实现了多个挑战问题,你只需要在写作中描述其中一个。)别忘了用git add answers-lab3.txt在提交的文件中包含答案文件。
内联汇编
在这个实验室,你可能会发现GCC的内联汇编语言功能很有用,尽管也可以在不使用它的情况下完成实验室。至少,你需要能够理解我们提供给你的源代码中已经存在的内联汇编语言(“asm”语句)片段。您可以在类参考资料页面上找到有关GCC内联汇编语言的几个信息来源。
A部分:用户环境和异常处理
新的inc/env.h包含JOS中用户环境的基本定义。现在就读。内核使用Env数据结构来跟踪每个用户环境。在这个实验室中,您最初只创建一个环境,但您需要设计JOS内核来支持多个环境;实验室4将利用这一特性,允许用户环境派生其他环境。
正如您在kern/env.c中看到的那样,内核维护与环境相关的三个主要全局变量:
JOS启动并运行后,envs指针指向一个env结构数组,该数组表示系统中的所有环境。在我们的设计中,JOS内核将支持最多NENV同时活动的环境,尽管在任何给定的时间通常会有少得多的运行环境。(NENV是在inc/env.h中定义的常量)分配后,env数组将包含每个NENV可能环境的env数据结构的单个实例。
JOS内核将所有不活动的Env结构保留在Env_free_list上。这种设计允许环境的轻松分配和解除分配,因为它们只需要添加到空闲列表中或从空闲列表中删除。
内核使用curenv符号在任何给定时间跟踪当前执行的环境。在启动过程中,在运行第一个环境之前,curenv最初设置为NULL。
环境状态
Env结构在c/Env.h中定义如下(尽管未来实验将增加更多字段):
以下是环境字段的用途:
像Unix进程一样,JOS环境将“线程”和“地址空间”的概念结合在一起。线程主要由保存的寄存器(env_tf字段)定义,地址空间由env_pgdir指向的页面目录和页面表定义。要运行环境,内核必须使用保存的寄存器和适当的地址空间来设置CPU。
我们的结构Env类似于xv6中的结构proc。两种结构都在Trapframe结构中保持环境(即进程)的用户模式寄存器状态。在JOS中,各个环境不像xv6中的进程那样拥有自己的内核堆栈。内核中一次只能有一个JOS环境处于活动状态,因此JOS只需要一个内核堆栈。
分配环境阵列
在实验2中,您在mem_init()中为pages[]数组分配了内存,内核使用该表来跟踪哪些页面是空闲的,哪些不是。现在,您需要进一步修改mem_init()来分配一个类似的env结构数组,称为envs。
练习1:在kern/pmap.c中修改mem_init()以分配和映射envs数组。此数组完全由Env结构的NENV实例组成,其分配方式与页面数组的分配方式非常相似。与页面数组一样,内存支持env也应该在UENVS(在inc/memlayout.h中定义)处以用户只读方式映射,以便用户进程可以从该数组中读取。
您应该运行代码并确保check_kern_pgdir()成功。
创建和运行环境
现在,您将在kern/env.c中编写运行用户环境所需的代码。因为我们还没有文件系统,所以我们将设置内核来加载嵌入内核本身的静态二进制映像。JOS将这个二进制文件作为ELF可执行映像嵌入到内核中。
Lab 3 GNUmake文件在obj/user/目录中生成许多二进制镜像。如果你看看kern/Makefrag,你会注意到一些神奇之处,它将这些二进制文件直接“链接”到内核可执行文件中,就像它们是.o文件一样。链接器命令行上的-b二进制选项会导致这些文件链接为“原始”未解释的二进制文件,而不是编译器生成的常规.o文件。(就链接器而言,这些文件根本不必是ELF镜像,它们可以是任何东西,比如文本文件或图片,和_ binary_obj_user_hello_size。链接器通过篡改二进制文件的文件名来生成这些符号名;这些符号为常规内核代码提供了一种引用嵌入的二进制文件的方法。
在kern/init.c中的i386_init()中,您将看到在环境中运行这些二进制镜像之一的代码。然而,建立用户环境的关键功能并不完整;你需要补充。
下面是代码的调用图,直到调用用户代码为止。确保您理解每一步的目的
完成后,您应该编译内核并在QEMU下运行它。如果一切顺利,您的系统应该进入用户空间并执行hello二进制文件,直到它使用int指令进行系统调用。在这一点上会有麻烦,因为JOS没有设置硬件来允许从用户空间到内核的任何类型的转换。当CPU发现它没有设置来处理这个系统调用中断时,它会生成一个通用保护异常,发现它无法处理,生成一个双故障异常,发现也无法处理,最后放弃所谓的“三重故障”。通常,您会看到CPU重置和系统重新启动。虽然这对遗留应用程序很重要(请参阅这篇博客文章以了解原因:Larry Osterman's WebLog | Microsoft Learn),但这对内核开发来说是一件痛苦的事情,所以使用6.828补丁的QEMU,您会看到一个寄存器转储和一条“Triple fault”消息。
我们将很快解决这个问题,但现在我们可以使用调试器来检查我们是否进入了用户模式。使用make qemu gdb并设置gdb断点atenv_pop_tf,这应该是在实际进入用户模式之前命中的最后一个函数。使用si单步执行此功能;处理器应在其et指令之后进入用户模式。然后,您应该看到用户环境的可执行文件中的第一条指令,即lib/entry.s中标签开头的cmpl指令。现在使用b*0x。。。在hello中的sys_cputs()中的int$0x30处设置断点(有关用户空间地址,请参阅obj/user/hello.asm)。此int是用于向控制台显示字符的系统调用。如果您无法执行到int,那么您的地址空间设置或程序加载代码有问题;在继续之前,请返回并修复它。
处理中断和异常
在这一点上,用户空间中的第一条int$0x30系统调用指令是一条死胡同:一旦处理器进入用户模式,就没有办法退出。您现在需要实现基本的异常和系统调用处理,这样内核就可以从用户模式代码中恢复对处理器的控制。您应该做的第一件事是彻底熟悉x86中断和异常机制。
在这个实验室里,我们通常遵循英特尔关于中断、异常等的术语。然而,exception、trap、interrupt、fault和abort在整个体系结构或操作系统中都没有标准意义,并且在诸如x86之类的特定体系结构中,它们之间的细微区别往往被忽略。当你在实验室外看到这些术语时,它们的含义可能会略有不同。
受保护控制权转让的基础知识
异常和中断都是“受保护的控制传输”,这会导致处理器从用户模式切换到内核模式(CPL=0),而不会给用户模式代码任何干扰内核或其他环境功能的机会。在英特尔的术语中,中断是一种受保护的控制传输,由通常在处理器外部的异步事件引起,例如外部设备I/O活动的通知。相反,一个例外是由当前运行的代码同步引起的受保护的控制传输,例如由于被零除或无效的内存访问。
为了确保这些受保护的控制传输得到实际保护,处理器的中断/异常机制被设计为,当中断或异常发生时,当前运行的代码不能任意选择内核的输入位置或方式。相反,处理器确保只有在精心控制的条件下才能进入内核。在x86上,两种机制协同工作以提供这种保护:
1.中断描述符表。处理器确保中断和异常只能导致内核在内核本身确定的几个特定的、定义明确的入口点进入,而不是在中断或异常发生时运行的代码。
x86允许多达256个不同的中断或异常入口点进入内核,每个入口点都有不同的中断向量。矢量是一个介于0和255之间的数字。中断的向量由中断的来源决定:不同的设备、错误条件和应用程序对内核的请求会产生具有不同向量的中断。CPU使用向量作为处理器中断描述符表(IDT)的索引,内核在内核专用内存中设置IDT,就像GDT一样。处理器从该表中的相应条目加载:
- 要加载到指令指针(EIP)寄存器中的值,指向指定用于处理该类型异常的内核代码。
- 要加载到代码段(CS)寄存器中的值,该值在位01中包括异常处理程序要运行的特权级别。(在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字段。
异常和中断的类型
x86处理器可以在内部生成的所有同步异常都使用0到31之间的中断向量,因此映射到IDT条目0-31。例如,页面错误总是通过向量14导致异常。大于31的中断向量仅由软件中断使用,软件中断可以由内部指令生成,也可以由外部设备在需要注意时引起的异步硬件中断。
在本节中,我们将扩展JOS来处理向量0-31中内部生成的x86异常。在下一节中,我们将使JOS处理软件中断向量48(0x30),JOS(相当任意)将其用作系统调用中断向量。在实验4中,我们将扩展JOS来处理外部生成的硬件中断,如时钟中断。
例子
让我们把这些部分放在一起,并通过一个例子进行追踪。假设处理器在用户环境中执行代码,并遇到一条试图除以零的除法指令。
1.处理器切换到由TSS的SS0和ESP0字段定义的堆栈,它们在JOS中将分别保存值GD_KD和KSTACKTOP。
2.处理器在内核堆栈上推送异常参数,从地址KSTACKTOP开始:
3.因为我们正在处理除法错误,即x86上的中断向量0,所以处理器读取IDT条目0,并将CS:EIP设置为指向该条目所描述的处理程序函数。
4.处理程序函数控制并处理异常,例如通过终止用户环境。
对于某些类型的x86异常,除了上面的“标准”五个字之外,处理器还会将另一个包含错误代码的字推送到堆栈上。页面错误异常(编号14)就是一个重要的例子。请参阅80386手册,以确定处理器推送错误代码的异常编号,以及在这种情况下错误代码的含义。当处理器推送错误代码时,当从用户模式进入时,堆栈在异常处理程序的开头看起来如下:
每个异常或中断都应该在trapentry.S中有自己的处理程序,trap_init()应该用这些处理程序的地址初始化IDT。每个处理程序都应该在堆栈上构建一个结构Trapframe(请参阅inc/trap.h),并使用指向Trapframe.trap()的指针调用trap。
实验室的A部分到此结束。别忘了添加answers-lab3.txt,提交您的更改,并在A部分截止日期前运行make-handin。
B部分:页面错误、断点异常和系统调用
既然您的内核具有基本的异常处理功能,那么您将对其进行改进,以提供依赖于异常处理的重要操作系统原语。
处理页面错误
页面错误异常,中断向量14(T_PGFLT),是一个特别重要的异常,我们将在本实验室和下一个实验室中大量练习。当处理器发生页面故障时,它将导致故障的线性(即虚拟)地址存储在一个特殊的处理器控制寄存器CR2中。在trap.c中,我们提供了一个特殊函数page_fault_handler()的开头,用于处理页面错误异常。
在实现系统调用时,您将在下面进一步完善内核的页面错误处理。
断点异常
断点异常,中断向量3(T_BRKPT),通常用于允许调试器通过用特殊的1字节int3软件中断指令临时替换相关程序指令,在程序代码中插入断点。在JOS中,我们将稍微滥用这个异常,将其转换为任何用户环境都可以用来调用JOS内核监视器的基本伪系统调用。如果我们将JOS内核监视器视为一个基本的调试器,那么这种用法实际上有些合适。例如,lib/paric.c中panic()的用户模式实现在显示其panic消息后执行int3。
系统调用
用户进程通过调用系统调用来要求内核为它们做一些事情。当用户进程调用系统调用时,处理器进入内核模式,处理器和内核合作保存用户进程的状态,内核执行适当的代码以执行系统调用,然后恢复用户进程。用户进程如何引起内核的注意以及它如何指定要执行的调用的确切细节因系统而异。
在JOS内核中,我们将使用int指令,这会导致处理器中断。特别是,我们将使用int$0x30作为系统调用中断。我们已经为您定义了常数T_SYSCALL为48(0x30)。您必须设置中断描述符,以允许用户进程导致该中断。请注意,中断0x30不能由硬件生成,因此,不存在由于允许用户代码生成它而引起的歧义。
应用程序将在寄存器中传递系统调用编号和系统调用参数。这样,内核就不需要在用户环境的堆栈或指令流中到处乱爬。系统调用号将进入%eax,参数(最多五个)将分别进入%edx、%ecx、%ebx、%edi和%esi。内核将返回值以%eax形式返回。调用系统调用的程序集代码已经在lib/syscall.c中的syscall()中为您编写。您应该通读它,并确保了解发生了什么。
用户模式启动
一个用户程序从lib/entry.S的顶部开始运行。经过一些设置后,此代码调用lib/libmain.c中的libmain()。您应该修改libmain()来初始化全局指针thisenv,使其指向envs[]数组中此环境的struct Env。(请注意,lib/entry.S已经定义了指向您在A部分中设置的UENVS映射的env。)提示:查看ininc/env.h并使用sys_getenvid。
libmain()然后调用umain,在hello程序的情况下,umain是inuser/hello.c。请注意,在打印“hello,world”后,它会尝试访问thisenv->env_id。这就是它早些时候出现故障的原因。既然您已经正确初始化了thisenv,那么它应该不会出错。如果它仍然存在故障,那么您可能还没有映射用户可读的UENVS区域(回到inpmap.c中的A部分;这是我们第一次实际使用UENVS地区)。
页面故障和内存保护
内存保护是操作系统的一个关键功能,可以确保一个程序中的错误不会损坏其他程序或损坏操作系统本身。
操作系统通常依靠硬件支持来实现内存保护。操作系统向硬件通知哪些虚拟地址有效,哪些无效。当程序试图访问无效地址或没有权限访问的地址时,处理器会在导致故障的指令处停止程序,然后将有关尝试操作的信息捕获到内核中。如果故障是可以修复的,内核可以修复它并让程序继续运行。如果故障无法修复,则程序无法继续,因为它永远无法通过导致故障的指令。
作为一个可修复故障的例子,考虑一个自动扩展的堆栈。在许多系统中,内核最初分配一个堆栈页面,然后如果程序在访问堆栈下一层的页面时出错,内核将自动分配这些页面,并让程序继续。通过这样做,内核只分配程序所需的堆栈内存,但程序可以在拥有任意大堆栈的错觉下工作。
系统调用为内存保护带来了一个有趣的问题。大多数系统调用接口允许用户程序将指针传递到内核。这些指针指向要读取或写入的用户缓冲区。然后内核在执行系统调用时取消引用这些指针。这有两个问题:
1.内核中的页面错误可能比用户程序中的页面故障严重得多。如果内核页面在操作自己的数据结构时出错,那就是内核错误,错误处理程序应该使内核(从而使整个系统)死机。但是,当内核取消引用用户程序给它的指针时,它需要一种方法来记住,这些取消引用导致的任何页面错误实际上都是代表用户程序的。
2.内核通常比用户程序具有更多的内存权限。用户程序可能会传递一个指向系统调用的指针,该系统调用指向内核可以读取或写入但程序无法读取或写入的内存。内核必须小心,不要被欺骗去引用这样的指针,因为这可能会泄露私有信息或破坏内核的完整性。
由于这两个原因,内核在处理用户程序提供的指针时必须非常小心。
现在,您将使用一种机制来解决这两个问题,该机制可以仔细检查从用户空间传递到内核的所有指针。当程序向内核传递指针时,内核将检查地址是否在地址空间的用户部分,以及页表是否允许内存操作。
因此,内核永远不会因为取消引用用户提供的指针而出现页面错误。如果内核出现页面错误,它应该死机并终止。
请注意,您刚刚实现的相同机制也适用于恶意用户应用程序(如user/evelhello)。
这就完成了实验。确保你通过了所有的初级测试,别忘了在answers-lab3.txt中写下你的问题答案和挑战练习解决方案的描述。提交你的更改,并在实验室目录中键入Make handin以提交你的工作。
在提交之前,请使用git status和git diff检查您的更改,不要忘记添加answers-lab3.txt。
准备好后,使用gitcommit-am“我的实验室3解决方案”提交您的更改,然后制作handin并按照指示进行操作。
标签:1789,JOS,中断,用户,6.828,调用,内核,MIT,处理器 From: https://blog.51cto.com/greyzhang/7807416