当前位置:首页 > 民俗文化

用户环境 Caffeinated 6.828:实验 3:用户环境

资料来源:https://pdos.csail.mit.edu/6.828/2018/labs/lab3/

翻译:qhwdw

简介

在本实验中,您将实现一个基本的内核函数,这是保护运行的用户模式环境(即进程)所必需的。您将增强JOS内核,配置数据结构以跟踪用户环境,创建单个用户环境,将程序映像加载到用户环境中,并启动它。您还应该编写一些JOS内核函数来处理任何用户环境生成的系统调用以及用户环境引入的各种异常。

注意:在这个实验中,“环境”和“过程”这两个术语是可以互换的——它们都表示同一个抽象概念,即允许你运行的程序。我在介绍中用“环境”这个术语代替传统术语“进程”的目的是强调JOS的环境和UNIX的进程提供的接口不同,它们的语义也不同。

基本原理

使用Git提交自实验2以来的更改(如果有),获取课程仓库的最新版本,并创建一个名为lab3的本地分支,指向我们的lab3分支原点/lab3:

雅典娜% cd ~/6.828/lab

雅典娜%添加git

Athena % git commit-am“handin后对lab2的更改”

创建提交734 fab 7:handin后对lab2的更改

4个文件已更改,42个插入(+),9个删除(-)

雅典娜% git拉

已经更新了。

雅典娜% git结账-b lab3原点/lab3

分支lab3设置为跟踪远程分支refs/remote/origin/lab 3。

切换到新的分支“lab3”

雅典娜% git合并实验室2

递归合并。

kern/pmap . c | 42++++++++++++++++++++

1个文件已更改,42个插入(+),0个删除(-)

雅典娜%

实验3包含一些您将探索的新源文件:

用户模式环境的公共定义

陷阱处理的公共定义

从用户环境到内核的系统调用的公共定义

用户模式支持库的公共定义

内核-用户模式环境的私有定义

实现用户模式环境的内核代码

内核-私有陷阱处理定义

陷阱处理代码

陷阱入口。汇编语言陷阱处理程序入口点

系统调用处理的内核私有定义

系统调用实现代码

lib/ Makefrag Makefile片段构建用户模式库,obj/lib/libjos.a

入口。面向用户环境的汇编语言入口点

从入口调用的用户模式库设置代码。S

用户模式系统调用存根函数

用户模式的putchar和getchar实现,提供控制台输入输出

退出的用户模式实现

恐慌的用户模式实现

用户/ *检查内核实验室3代码的各种测试程序

此外,实验2中的一些源文件将在实验3中进行修改。如果您想查看任何更改,您可以运行:

$ git diff实验室2

您还可以查看《实验工具指南》,其中包含与本实验相关的调试用户代码的信息。

实验要求

本实验分为甲、乙两部分,甲部分应在本实验完成后一周内提交;您即将提交您的更改并完成动手实验。在提交之前,确保您的代码已经通过了A部分的所有检查(如果您的代码没有通过B部分的检查,您也可以提交它)。只需要在第二周提交B部分截止日期前通过代码检查即可。

因为在实验2中,你需要做实验中描述的所有正则表达式练习,并且至少通过一次挑战(指的是整个实验,而不是每个部分)。写一个问题的详细答案贴在实验里,还有一两段关于如何解决你选择的挑战的详细描述,放在一个叫answers-lab3.txt的文件里,把这个文件放在你实验目标的根目录下。(如果你提出了多个挑战,只需要提交其中一个即可。)别忘了用git add answers-lab3.txt提交这个文件

内嵌汇编语言

在这个实验中,您可能会发现使用了GCC的内联汇编语言功能,尽管没有它也可以完成实验。但至少你需要理解这些内联汇编语言片段,它们已经存在于提供给你的源代码中。您可以在课程资源页面上找到关于GCC行中汇编语言的信息。

第一部分:用户环境和异常处理

新文件inc/env.h包含JOS中用户环境的基本定义。现在就去读。内核使用数据结构Env来跟踪每个用户的环境。在这个实验的开始,你只会创建一个环境,但是你需要设计JOS内核来支持多个环境;实验4将带来这个高级特性,允许用户环境分叉到其他环境。

正如您在kern/env.c中看到的,内核维护三个与环境相关的全局变量:

结构Env * envs = NULL//所有环境

struct Env * curenv = NULL//当前环境

静态结构Env * env _ free _ list//自由环境列表

一旦JOS启动并运行,envs指针指向一个数组,也就是数据结构Env,它保存了系统中的所有环境。在我们的设计中,JOS内核将同时支持最大数量的NENV活动环境,尽管一般来说,很少有环境在任何给定的时间运行。(NENV是由inc/Env.h中的#define定义的常量)一旦赋值,ENVs数组将包含NENV的每个可能环境的数据结构env的单个实例。

JOS内核用env_free_list上的数据结构env保存所有非活动环境。这种设计使得分配和回收环境变得容易,因为这只是添加或删除空空闲列表的问题。

内核使用符号curenv来跟踪任何给定时间的当前运行环境。在系统引导期间,curenv在第一个环境运行之前被初始化为NULL。

环境状态

数据结构Env在文件inc/env.h中定义,内容如下:(后面的实验会增加更多的字段):

结构Env {

struct Trapframe env _ tf//保存的寄存器

struct Env * env _ link//下一个自由环境

envid _ t env _ id//唯一的环境标识符

envid _ t env _ parent _ id//此环境的父环境的环境_id

EnvType env _ type//表示特殊的系统环境

未签名的env _ status//环境状况

uint32 _ t env _ runs//环境运行的次数

//地址空间

pde _ t * env _ pgdir//页面目录的内核虚拟地址

};

以下是对数据结构Env中的字段的介绍:

env_tf: 这个结构定义在 inc/trap.h 中,它用于在那个环境不运行时保持它保存在寄存器中的值,即:当内核或一个不同的环境在运行时。当从用户模式切换到内核模式时,内核将保存这些东西,以便于那个环境能够在稍后重新运行时回到中断运行的地方。env_link: 这是一个链接,它链接到在 env_free_list 上的下一个 Env 上。env_free_list 指向到列表上第一个空闲的环境。env_id: 内核在数据结构 Env 中保存了一个唯一标识当前环境的值(即:使用数组 envs 中的特定槽位)。在一个用户环境终止之后,内核可能给另外的环境重新分配相同的数据结构 Env —— 但是新的环境将有一个与已终止的旧的环境不同的 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。

我们的结构Env类似于xv6中的结构proc。它们都以陷帧结构保存环境(即进程)的用户模式寄存器状态。在JOS中,单个环境不能像xv6中的进程那样有自己的内核堆栈。这里,任何时候内核中只能有一个JOS环境是活动的,所以JOS只需要一个内核栈。

为环境分配一个阵列

在实验2的mem_init()中,您为数组页面[]分配了内存,数组页面[]是内核用来跟踪页面分配状态的表。您现在需要修改mem_init(),这样您就可以使用它来分配一个类似Env的数组,它被称为Env。

练习1。在kern/pmap.c中修改mem_init()来分配和映射envs数组。这个数组完全由Env结构分配的实例nEnv组成,就像您分配的pages数组一样。像pages数组一样,内存支持的数组env将把用户读取的内存映射到UENVS(在inc/memlayout.h文件中定义)中,这样用户进程就可以从这个数组中读取。

您应该运行您的代码,并确保check_kern_pgdir()是正确的。

创建和运行环境

现在,您将在kern/env.c中编写一些必要的代码来运行用户环境。因为我们没有制作文件系统,所以我们将设置内核加载一个嵌入在内核中的静态二进制映像。JOS内核将这个二进制映像作为ELF可执行映像嵌入到内核中。

在实验3中,GNUmakefile会在obj/user/目录中生成一些二进制映像。如果你看到kern/Makefrag,你会注意到一些奇怪的东西,它们“链接”这些二进制文件直接运行到内核中,就像。o文件。链接器命令行上的-b二进制选项会将它们作为“本机”未分析的二进制文件而不是普通文件进行链接。o编译器生成的文件。(就链接器而言,这些文件根本不是ELF图像文件——它们可以是任何东西,比如文本文件或者图片!如果你在内核构建后再看obj/kern/kernel.sym,你会注意到链接器奇怪地生成了一些有趣的、名称混乱的符号,比如_binary_obj_user_hello_start、_binary_obj_user_hello_end、_ binary _ obj _ user _ hello _链接器通过适配二进制文件的命令生成这些符号;这个符号是为普通内核代码引入嵌入式二进制文件的一种方法。

在kern/init.c的i386_init()中,您将编写一些代码来运行环境中的这些二进制映像之一。但是设置用户环境的关键功能还没有实现。你需要完成它们。

练习2。在env.c文件中,完成以下函数的代码编写:

env_init()初始化 envs 数组中所有的 Env 结构,然后把它们添加到 env_free_list 中。也称为 env_init_percpu,它通过配置硬件,在硬件上为 level 0(内核)权限和 level 3(用户)权限使用单独的段。env_setup_vm()为一个新环境分配一个页目录,并初始化新环境的地址空间的内核部分。region_alloc()为一个新环境分配和映射物理内存load_icode()你将需要去解析一个 ELF 二进制镜像,就像引导加载器那样,然后加载它的内容到一个新环境的用户地址空间中。env_create()使用 env_alloc 去分配一个环境,并调用 load_icode 去加载一个 ELF 二进制env_run()在用户模式中开始运行一个给定的环境

当您编写这些函数时,您可能会发现新的cprintf动词%e非常有用——它可以输出错误代码的描述。例如:

r =-E _ NO _ MEM;

死机(" env_alloc: %e ",r);

死机将输出env_alloc:内存不足的消息。

以下是与用户代码相关的调用图。确保你理解每一步的目的。

start (kern/entry.S)i386_init (kern/init.c)cons_initmem_initenv_inittrap_init(到目前为止还未完成)env_createenv_runenv_pop_tf

完成以上功能后,应该编译内核,在QEMU下运行。如果一切正常,您的系统将输入user 空并运行binary hello,直到使用int指令生成系统调用。到那时,就会出现问题,因为JOS还没有设置硬件来允许从user 空到kernel 空的各种转换。当CPU发现一个没有系统调用中断的服务程序时,会产生一个一般的保护异常,发现那个异常并处理,还会产生一个双故障异常,这个双故障异常也会被发现并处理,最后出现所谓的“三故障异常”。通常,稍后您会看到CPU重置和系统重新启动。虽然这是传统应用程序的一个主要问题(原因在这篇博文中有解释),但对于内核开发来说,这是一个痛苦的过程。因此,在用6.828修补的QEMU上,您可以看到转储的寄存器内容和“三重故障”消息。

我们将立即处理这些问题,但是现在,我们可以使用调试器来检查我们是否进入了用户模式。使用make qemu-GDB,并在env_pop_tf处设置一个GDB断点,这是进入用户模式前最后一个函数。使用si进入该功能;在iret指令后,处理器将进入用户模式。然后您将看到在用户环境中运行的第一条指令,这将是标签start在lib/entry中的第一条指令cmpl。s现在,用b *0x...(有关user空之间的地址,请参见obj/user/hello.asm)将断点设置在hello中sys_cputs()的int [91]x30处。这个int指令是一个系统调用,向控制台显示一个字符。如果直到int才运行,可能是你的地址空之间设置或加载代码有错误;回去,找到问题,解决,再运行。

处理中断和异常

到目前为止,user 空中的第一条系统调用指令int [93]x30已经正式死亡:处理器一旦进入用户模式,就无法返回。因此,现在,您需要实现基本的异常和系统调用服务程序,因为内核有可能从用户模式代码中重新获得对处理器的控制。你要做的第一件事就是彻底掌握x86中断和异常机制的使用。

练习3。如果您不熟悉中断和异常机制,请阅读80386程序员手册第9章(或IA-32开发人员手册第5章)。

在这个实验中,我们将遵循英特尔对中断、异常和其他类似事物的术语习惯。由于在不同的架构和操作系统中,异常、陷阱、中断、故障、中止等术语没有统一的标准,所以在特定的架构(如x86)中,我们往往不会考虑它们之间的细微差异。当你在这个实验之外看到这些术语时,它们的含义可能会略有不同。

受保护的控制转移基础

异常和中断是“受保护的控制转移”,这将导致处理器从用户模式切换到内核模式(CPL=0),而用户模式代码不会干扰内核的其他功能或其他环境。在英特尔的术语中,中断是一种“受保护的控制传输”,由处理器外部的外部异步事件引起,如外部设备I/O活动通知。相反,异常是由当前运行的代码引起的同步和受保护的控制传输,如零除错误或对无效内存的访问。

为了保证这些受保护的控制转移得到真正的保护,处理器的中断/异常机制设计如下:当发生中断/异常时,当前运行的代码不能随意选择进入内核的位置和模式。而是处理器只能在内核可以严格控制的情况下才能进入内核。在x86上,有两种机制共同提供这种保护:

中断描述符表 处理器确保中断和异常仅能够导致内核进入几个特定的、由内核本身定义好的、明确的入口点,而不是去运行中断或异常发生时的代码。x86 允许最多有 256 个不同的中断或异常入口点去进入内核,每个入口点都使用一个不同的中断向量。一个向量是一个介于 0 和 255 之间的数字。一个中断向量是由中断源确定的:不同的设备、错误条件、以及应用程序去请求内核使用不同的向量生成中断。CPU 使用向量作为进入处理器的中断描述符表(IDT)的索引,它是内核设置的内核私有内存,GDT 也是。从这个表中的适当的条目中,处理器将加载:将值加载到指令指针寄存器(EIP),指向内核代码设计好的,用于处理这种异常的服务程序。将值加载到代码段寄存器(CS),它包含运行权限为 0—1 级别的、要运行的异常服务程序。(在 JOS 中,所有的异常处理程序都运行在内核模式中,运行级别为 0。)任务状态描述符表 处理器在中断或异常发生时,需要一个地方去保存旧的处理器状态,比如,处理器在调用异常服务程序之前的 EIP 和 CS 的原始值,这样那个异常服务程序就能够稍后通过还原旧的状态来回到中断发生时的代码位置。但是对于已保存的处理器的旧状态必须被保护起来,不能被无权限的用户模式代码访问;否则代码中的 bug 或恶意用户代码将危及内核。基于这个原因,当一个 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的中断向量仅用于由int指令生成的软件中断,或由外部设备在需要时生成的异步硬件中断。

在这一节中,我们将扩展JOS来处理内部生成的向量在0到31之间的x86异常。在下一节中,我们将完成JOS的第48号软件中断向量(0x30),它将被JOS(随机选择)用作系统调用中断向量。在实验4中,我们将扩展JOS来处理外部产生的硬件中断,比如时钟中断。

例子

让我们把这些片段放在一起,用一个例子来巩固它们。我们假设处理器在用户环境中运行代码,遇到了零除的问题。

处理器去切换到由 TSS 中的 SS0 和 ESP0 定义的栈,在 JOS 中,它们各自保存着值 GD_KD 和 KSTACKTOP。处理器在内核栈上推入异常参数,起始地址为 KSTACKTOP:

+++KSTACKTOP

| 0x00000 |旧SS | " - 4

|旧ESP | " - 8

|旧EFLAGS | " - 12

| 0x00000 |旧CS | " - 16

|老EIP |”-20 & lt;- ESP

+ - +

由于我们要处理一个除零错误,它将在 x86 上产生一个中断向量 0,处理器读取 IDT 的条目 0,然后设置 CS:EIP 去指向由条目描述的处理函数。处理服务程序函数将接管控制权并处理异常,例如中止用户环境。

对于某些类型的x86异常,除了上述五个“标准”寄存器,处理器还会将另一个包含错误代码的寄存器值推送到堆栈上。异常页面错误,矢量编号14,就是一个重要的例子。查看80386手册,确定哪些异常会产生错误代码,以及在这种情况下错误代码的含义。当处理器推送错误代码并从用户模式进入内核模式时,异常处理服务程序开头的堆栈应该如下所示:

+++KSTACKTOP

| 0x00000 |旧SS | " - 4

|旧ESP | " - 8

|旧EFLAGS | " - 12

| 0x00000 |旧CS | " - 16

|老EIP |”-20

|错误代码| " - 24 <。- ESP

+ - +

嵌套的异常和中断

处理器可以处理来自用户和内核模式的异常和中断。当从用户模式接收异常和中断时,它将进入内核模式,在将其旧的寄存器状态推入堆栈并通过IDT调用相关的异常服务程序之前,x86处理器将自动切换堆栈。如果发生异常或中断时处理器已经处于内核模式(CS寄存器的低位两位为0),那么CPU只是将一些值推入同一个内核堆栈。这样,内核可以优雅地处理嵌套异常,这些异常通常是由内核自己的代码触发的。这个函数在实现保护时是一个非常重要的工具,我们将在后面的系统调用中看到它。

如果处理器已经处于内核模式,并且发生嵌套异常,因为它不需要切换堆栈,所以它不需要保存旧的SS或ESP寄存器。对于不推送错误代码的异常类型,在进入异常服务程序时,其内核堆栈应该如下图所示:

++ & lt;-旧ESP

|旧EFLAGS | " - 4

| 0x00000 |旧CS | " - 8

|老EIP |”-12

+ - +

对于需要推送错误代码的异常类型,处理器会像以前一样,在旧EIP之后立即推送错误代码。

关于处理器的异常嵌套功能有一个重要的警告。如果在处理器处于内核模式的时候出现异常,并且不管是什么原因,比如栈间泄漏空,都不会推送其旧状态,那么此时处理器将无法做任何恢复,只是简单的复位。毫无疑问,内核的设计应该是为了防止这种情况发生。

设定IDT

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

头文件inc/trap.h和kern/trap.h包含与中断和异常相关的重要定义,因此您需要熟悉如何使用它们。kern/trap.h文件包含对内核的严格和秘密的定义,但是inc/trap.h中包含的定义也可以用于用户级程序和库。

注意:0-31范围内的一些例外被定义为由英特尔保留。因为它们从来没有在他们的处理器上生成过,所以你如何处理它们不会有大问题。你想怎么做都行。

下图描述了您将实现的完整控制流程:

IDT陷人。s陷阱. c

+ - +

| & amphandler1 | ->handler 1:trap(struct Trap frame * TF)

| | //做事{

| |调用trap //处理异常/中断

| | // ...}

+ - +

| & amphandler2 | ->手柄2:

| | //做事

| |呼叫陷阱

| | // ...

+ - +

+ - +

| & amphandlerX | ->handlerX:

| | //做事

| |呼叫陷阱

| | // ...

+ - +

每个异常或中断在trapentry中都应该有自己的处理程序。和trap_init()应该用这些处理程序的地址初始化IDT。每个处理程序都应该在堆栈上构建一个结构trapframe(参见inc/trap.h),然后使用一个指针来调用Trapframe的Trap()(在trap.c中)。Trap()然后处理异常/中断或将它们分派给特定的处理程序。

练习4。编辑trapentry。然后实现上述功能。宏TRAPHANDLER和TRAPHANDLER_NOEC在trapentry中。你需要在陷阱入口中为inc/trap.h中定义的每个陷阱添加一个入口点。s(使用这些宏),您将拥有由t和o提供的_alltraps,它由宏TRAPHANDLER指向。您还需要修改trap_init()来初始化idt,以便它指向trapentry中定义的每个入口点。s;宏SETGATE会帮你实现。

您的_alltraps应该:

推送值以使栈看上去像一个结构 Trapframe加载 GD_KD 到 %ds 和 %espushl %esp 去传递一个指针到 Trapframe 以作为一个 trap() 的参数call trap (trap 能够返回吗?)

考虑使用pushal指令;它非常适合结构陷框的布局。

使用用户目录中的一些测试程序来测试您的陷阱处理代码。这些测试程序可以在生成任何系统调用之前抛出异常,比如user/divzero。此时,您应该能够成功完成divzero、softint和badsegment测试。

小挑战!目前,TRAPHANDLER列在trapentry中。s可能有许多与trap.c中安装的代码非常相似的代码..清除它们。在trapentry中修改宏。请注意,您可以直接使用。文本和。数据来切换汇编程序中的代码和数据。

问题

在你的答案-lab3.txt中回答以下问题:

为每个异常/中断设置一个独立的服务程序函数的目的是什么?(即:如果所有的异常/中断都传递给同一个服务程序,在我们的当前实现中能否提供这样的特性?)你需要做什么事情才能让 user/softint 程序正常运行?评级脚本预计将会产生一个一般保护故障(trap 13),但是 softint 的代码显示为 int $14。为什么它产生的中断向量是 13?如果内核允许 softint 的 int $14 指令去调用内核页故障的服务程序(它的中断向量是 14)会发生什么事情? “`

这个实验的A部分结束了。不要忘记添加答案-lab3.txt文件,提交您的更改,然后在A部分作业的提交截止日期之前运行make handin。

第二部分:页面错误、断点异常和系统调用

现在您的内核已经有了最基本的异常处理能力,您将继续改进它,以提供依赖于异常服务程序的操作系统原语。

处理页面错误

页面故障不正常,中断向量是14(T_PGFLT),这是很重要的事情。我们会通过这个实验和后面的实验大量练习。当处理器发生页面错误时,处理器会将导致错误的线性地址(即虚拟地址)保存在特定的控制寄存器(CR2)中。在trap.c中,我们提供了一个处理它的特殊函数的雏形,即page_fault_handler(),我们将使用它来处理页面错误异常。

练习5。修改trap_dispatch()将页面错误异常分派给page_fault_handler()。现在您应该能够成功测试faultread、faultreadkernel、faultwrite和faultwritekernel了。如果其中任何一个不能正常工作,找出问题并解决它。请记住,您可以使用make run-x或make run-x-nox将JOS重定向到特定的用户程序中。例如,您可以运行make run-hello-nox来运行hello用户程序。

接下来,您将进一步细化内核页面错误服务程序,因为您想要实现系统调用。

断点异常

断点异常,中断向量为3(T_BRKPT)。一般用于调试。它在程序代码中插入断点,然后使用特定的1字节int3软件中断指令临时替换相应的程序指令。在JOS中,我们会稍微“滥用”一下这个异常,让它成为一个伪系统调用原语,这样任何用户环境都可以用它来调用JOS内核监控器。如果我们把JOS内核监控看作最初的调试器,那么这个用法是合适的。例如,在lib/死机. c中实现的用户模式下的死机()在显示其死机消息后会运行int3中断。

练习6。修改trap_dispatch()以在调用内核监视器时生成断点异常。您现在应该能够成功完成断点测试。

小挑战!修改JOS内核监视器,以便您可以从当前位置“继续”异常(即在int3之后,断点异常调用内核监视器),这样您就可以一次运行一个单步指令。要实现单步操作,需要了解EFLAGS寄存器中某些位的含义。

可选:如果你喜欢冒险,可以找一些x86反汇编代码——也就是把它从QEMU、GNU二进制工具中分离出来,或者自己编写——然后扩展JOS内核监控器,让它可以反汇编并显示你的每一步指令。结合实验1中的符号表,这将是您编写的一个真正的内核调试器。

问题

在断点测试案例中,根据你在 IDT 中如何初始化断点条目的不同情况(即:你的从 trap_init 到 SETGATE 的调用),既有可能产生一个断点异常,也有可能产生一个一般保护故障。为什么?为了能够像上面的案例那样工作,你需要如何去设置它,什么样的不正确设置才会触发一个一般保护故障?你认为这些机制的意义是什么?尤其是要考虑 user/softint 测试程序的工作原理。 系统调用

用户进程通过系统调用请求内核为它做一些事情。当用户进程请求系统调用时,处理器首先进入内核模式,处理器和内核协作保存用户进程的状态。内核会运行相关代码以完成系统调用,然后返回用户进程。在不同的系统上,用户进程如何引起内核的注意以及它如何指定它需要的系统调用的细节是不同的。

在JOS内核中,我们使用int指令,这将导致处理器中断。特别是,我们使用int [180]x30作为系统调用中断。我们将常数T_SYSCALL定义为48(0x30)。您需要设置中断描述符表,以允许用户进程触发该中断。请注意,中断0x30不是由硬件生成的,因此允许用户代码生成它不会导致歧义。

应用程序将在寄存器中传递系统调用号和系统调用参数。这样,内核不需要遍历用户环境的堆栈或指令流。系统调用号将放在%eax中,参数(最多五个)将分别放在%edx、%ecx、%ebx、%edi和%esi中。内核将在%eax中传递返回值。使用系统调用的汇编代码是在lib/syscall.c中的syscall()中为您编写的。您可以通过阅读它来确保您理解他们的工作。

练习7。向内核中的中断向量T_SYSCALL添加一个服务程序。您需要编辑kern/trapentry的trap_init()。s和kern/trap.c Trap_dispatch()需要修改,通过调用syscall()(在kern/syscall.c中定义)和适当的参数来处理系统调用中断,然后系统调用的返回值被调度在%eax中传输给用户进程。最后,您需要在kern/syscall.c中实现syscall(),如果系统调用号是无效值,请确保syscall()的返回值必须是-e _ invalid。为了确保您理解系统调用的接口,您应该阅读并掌握lib/syscall.c文件(尤其是内联程序集的动作)。对于inc/syscall.h中列出的每个系统调用,您需要通过调用相关的内核函数来处理A。

在你的内核中运行用户/hello程序(做run-hello)。它应该在控制台上输出hello,world,然后在用户模式下生成一个页面错误。如果没有页面错误,这可能意味着您的系统调用服务程序不正确。现在,您应该能够成功通过testbss测试。

小挑战!使用sysenter和sysexit指令而不是int 0x30和iret来实现系统调用。

sysenter/sysexit命令是英特尔设计的,运行速度比int/iret命令快。它通过使用寄存器而不是堆栈,并假设如何使用分段寄存器来实现这一点。这些说明的详细信息可以在英特尔参考手册的2B卷中找到。

在JOS中添加对这些指令的支持的最简单的方法是在kern/trapentry中添加一个sysenter_handler。s,它存储了足够多的关于用户环境返回、设置内核环境、将参数推送到syscall()和直接调用syscall()的信息。一旦syscall()返回,它将设置运行sysexit指令所需的一切。您还需要在kern/init.c中添加一些代码来设置特殊的模块寄存器(MSr)。《AMD架构程序员手册》第2卷第6.1.2节和《英特尔参考手册》第2B卷的SYSENTER中详细描述了MSRs。关于如何编写msr,这里可以找到一个添加到inc/x86.h的wrmsr的实现

最后,必须修改lib/syscall.c以支持sysenter生成系统调用。以下是sysenter指令的可能寄存器布局:

eax - syscall号码

edx,ecx,ebx,edi - arg1,arg2,arg3,arg4

esi -返回pc

ebp -返回esp

esp -被sysenter丢弃

GCC的内联汇编器会自动保存您告诉它直接加载到寄存器中的值。别忘了同时推送和弹出你使用的其他寄存器,或者告诉内联汇编器你正在使用它们。内联汇编程序不支持保存%ebp,所以您需要添加一些代码来保存和恢复它们。通过使用像leal after_sysenter_label、%esi这样的指令,可以将返回地址放入%esi。

请注意,它只支持4个参数,所以您需要保留支持5个参数的系统调用的旧方法。而且因为这个快速路径不更新当前环境的陷阱帧,所以不适合我们在后续实验中加入的一些系统调用。

在下一个实验中,我们启用了异步中断,所以您需要再次评估您的代码。特别是,当返回到用户进程时,您需要启用中断,sysexit指令不会为您这样做。

启动用户模式

用户程序从库/入口的顶部运行。在一些配置之后,代码在lib/libmain中调用libmain()。您应该修改libmain()来初始化全局指针thisenv,以便它指向数组env[]中该环境的结构Env。(注意envs已经在lib/entry中定义了。提示:检查inc/env.h并使用sys_getenvid。

Libmain()然后调用umain,在hello program的情况下是user/hello.c。注意,在它输出“hello,world”之后,它试图访问thisenv->: env_id .这就是为什么错误发生在我面前。既然已经正确初始化了thisenv,它应该不会再次失败。如果故障仍然出现,可能是因为你没有映射好UENVS区域让用户可读(回到前面的A部分查看pmap . c);这是我们第一次真正使用UENVS区域。

练习8。将所需代码添加到用户库中,然后引导您的内核。你可以看到用户/hello程序会输出hello,world,然后我是environment 00001000。然后,用户/hello将通过调用sys_env_destroy()(参见lib/libmain.c和lib/exit.c)来尝试“退出”。由于内核目前只支持一个用户环境,它应该报告它破坏了唯一的环境,然后进入内核监视器。现在你应该可以成功通过hello测试了。

页故障和内存保护

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

操作系统一般依靠硬件支持来实现内存保护。操作系统告诉硬件哪些虚拟地址有效,哪些无效。当一个程序试图访问一个无效地址或一个它没有访问权限的地址时,处理器将停止该程序在导致错误的位置运行,然后在内核中捕获关于所尝试的操作的相关信息。如果故障是可修复的,内核可以修复它,让程序继续运行。如果故障无法修复,程序将无法继续运行,因为它永远不会跳过导致故障的指令。

作为可修复故障的一个例子,假设自动扩展堆栈。在很多系统上,内核初始化分配单个堆栈页,然后如果程序无法访问堆栈页下面的页,内核会自动分配这些页,让程序继续运行。这样内核只分配程序需要的内存栈,程序却可以在任意大小的栈的假象中运行。

对于内存保护,系统调用中有一个非常有趣的问题。许多系统调用接口允许用户程序向内核传递指针。这些指针指向用户想要读取或写入的缓冲区。然后内核在执行系统调用时丢弃这些指针。所以有两个问题:

内核中的页故障可能比用户程序中的页故障多的多。如果内核在维护它自己的数据结构时发生页故障,那就是一个内核 bug,而故障服务程序将使整个内核(和整个系统)崩溃。但是当内核废弃了由用户程序传递给它的指针后,它就需要一种方式去记住那些废弃指针所导致的页故障其实是代表用户程序的。一般情况下内核拥有比用户程序更多的权限。用户程序可以传递一个指针到系统调用,而指针指向的区域有可能是内核可以读取或写入而用户程序不可访问的区域。内核必须要非常小心,不能被废弃的这种指针欺骗,因为这可能导致泄露私有信息或破坏内核的完整性。

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

现在,您可以通过使用一个简单的机制来仔细检查从user 空传递到内核的所有指针来解决这个问题。当一个程序传递一个指向内核的指针时,内核会检查它的地址是否在address 空之间的用户部分,然后页表会允许对内存的操作。

这样,当内核丢弃用户提供的指针时,就永远不会出现页面错误。如果内核有这样的页面错误,应该会崩溃终止。

练习9。如果在内核模式下出现页面错误,请将kern/trap.c修改为崩溃。

提示:要判断页面错误发生在用户模式还是内核模式,只需检查tf_cs的低位即可。

在kern/pmap.c中读取user_mem_assert,并在该文件中实现user_mem_check。

修改kern/syscall.c以检查传递给系统调用的参数。

启动你的内核并运行user/bugghello。环境会被破坏,内核不会崩溃。您将看到:

[00001000]va 00000001的user_mem_check断言失败

[00001000]自由环境00001000

破坏了唯一的环境——无事可做!

最后在kern/kdebug.c中修改debuginfo_eip,调用usd、stabs、stabstr上的user_mem_check。如果现在运行user/breakpoint,应该可以从内核监控器运行回溯,然后在内核因页面故障崩溃之前看到回溯进入lib/libmain.c。是什么导致这个页面失败?不需要修,但是要明白是怎么发生的。

请注意,刚刚实现的这些机制也适用于恶意用户程序(如user/evilhello)。

练习10。启动你的内核,运行user/EVI hello。环境要破坏,内核不崩溃。您应该能够看到:

[0000000]新环境00001000

...

[00001000]va f 010000 c的user_mem_check断言失败

[00001000]自由环境00001000

这个实验到此结束。确保您已经通过了所有的等级测试,不要忘记写下问题的答案,并在答案-lab3.txt中详细描述您的挑战练习的解决方案。提交您的更改,并在实验室目录中输入make handin以提交您的工作。

动手实验前用git status和git diff检查你的改动,别忘了去git add answers-lab3.txt当你完成后,用git commit-am 'my solutions to lab 3 '提交你的改动,然后handin并关注本指南。

途经:https://pdos.csail.mit.edu/6.828/2018/labs/lab3/

这篇文章最初由LCTT编辑,由中国Linux发行

1.《用户环境 Caffeinated 6.828:实验 3:用户环境》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《用户环境 Caffeinated 6.828:实验 3:用户环境》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/guoji/1034732.html

上一篇

河北确诊小学生曾去学校上课 真相原来是这样!

下一篇

又1例持湖北绿码确诊 20人被隔离 究竟发生了什么?

虎门大桥发生异常抖动 对此大家怎么看?

新华社广州5月5日电记者从广州交警处获悉,虎门大桥出现异常晃动,桥梁管理部门已将大桥关闭。今天下午3点32分,广州交警支队对虎门大桥进行了交通管制,提醒过往车辆绕道行驶。目前虎门大桥双向全封闭,关佛高速西侧威远出口主线全封...

丰巢公开致信用户 事情经过真相揭秘!

A5创业网(微信官方账号:iadmin 5)5月10日报道,丰巢快递柜近日公布收费政策,国内部分社区选择停止使用丰巢快递柜。山东、江苏、浙江、福建等地监管部门也纷纷发声,引发巨大社会争议。5月9日晚,丰巢向用户发出公开信,...

子宫出血和月经的区别 同样是出血,异常的子宫出血与月经该如何区分?

子宫出血和月经的区别 同样是出血,异常的子宫出血与月经该如何区分?

月经是由于激素变化导致子宫内膜正常脱落而引起的出血。 然而,在其他时候,子宫可能会出血。子宫出血往往和月经类似,所以有些女性朋友往往把子宫出血和月经当作一回事。这就使得子宫无法得到及时的诊断和治疗,而且由于子宫出血的可能因素很多,需要尽早诊断和治疗。 肿瘤?炎症?子宫为什么会出血? 子宫出血...

yupoo Yupoo 又拍网(网络相册)用户必须得过来一下

  • yupoo Yupoo 又拍网(网络相册)用户必须得过来一下
  • yupoo Yupoo 又拍网(网络相册)用户必须得过来一下
  • yupoo Yupoo 又拍网(网络相册)用户必须得过来一下

环境数据造假风波后 临汾环保局迎新局长

临汾市政府副秘书长、办公厅副主任白主持工作三个月后,深陷环境数据造假风波的临汾市环保局终于迎来了新局长。 此前,原局长张文卿于2018年5月30日被晋中市榆次区人民法院判处有期徒刑两年。 据临汾新闻网报道,2018年8月7...

工程师回应举报家乡环境问题获刑 对此大家怎么看?

  • 工程师回应举报家乡环境问题获刑 对此大家怎么看?
  • 工程师回应举报家乡环境问题获刑 对此大家怎么看?
  • 工程师回应举报家乡环境问题获刑 对此大家怎么看?

iPhone用户被盗刷 专家建议开启账户双重认证服务

近日,不少苹果手机用户向中国好声音举报,自己的苹果ID突然被他人盗用,绑定到原账号的支付平台被多次扣款,资金被盗,最高损失达1万元。对此,苹果表示已了解相关情况,并将在审核客户反映的问题后给出相应的处理结果。 用户遇到了两...

苹果用户被盗刷 中消协喊话苹果不应推卸责任

针对苹果用户被盗事件,中国消费者协会19日发文,称苹果不应推卸责任,淡化自身安全问题,转移消费者关注。 中国消费者协会在文章中称,近日,使用苹果手机的消费者在线财产安全权利受到侵犯,造成重大财产损失,凸显了在线支付安全的严...