😜Introduction

在本实验中,我们将实现受保护的用户模式环境(即“进程”)所需的基本内核功能。我们将要增强 JOS 内核以设置数据结构来跟踪用户环境,创建一个单用户环境,将程序映像加载到其中,并启动它运行;还将使 JOS 内核能够处理用户环境发出的任何系统调用,并处理用户环境引起的任何其他异常。
需要注意的是在这个实验中,术语 env 和 process 是可以互换的,两者都指允许您运行程序的抽象。之所以我们引入术语“env”而不使用传统术语“process”,以强调 JOS 环境和 UNIX 进程提供不同的接口,并且不提供相同的语义。

😝User Environments and Exception Handling

新的头文件 inc/env.h 包含JOS中用户环境的基本定义:

  • 内核使用 Env 数据结构来跟踪每个用户环境
  • 在本实验中,您最初将只创建一个环境
  • 但是需要设计 JOS 内核以支持多个环境;实验室4将利用这个特性,允许用户环境 fork 其他环境

在 kern/env.c 中所看到的,内核维护着与环境相关的三个主要全局变量:

  1. struct Env *envs = NULL; // All environments
  2. struct Env *curenv = NULL; // The current env
  3. 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 中定义如下(在未来的 Lab 中会添加更多字段):

  1. struct Env {
  2. struct Trapframe env_tf; // Saved registers
  3. struct Env* env_link; // Next free Env
  4. envid_t env_id; // Unique environment identifier
  5. envid_t env_parent_id; // env_id of this env's parent
  6. enum EnvType env_type; // Indicates special system environments
  7. unsigned env_status; // Status of the environment
  8. uint32_t env_runs; // Number of times environment has run
  9. // Address space
  10. pde_t* env_pgdir; // Kernel virtual address of page dir
  11. };

以下是 Env 中各个字段的用途:

  • env_tf:在 inc/trap.h 中定义这个结构体,在此环境未运行时(即内核或其他环境正在运行时)保存环境的寄存器值;当从用户模式切换到内核模式时,内核会保存这些信息,以便以后可以在停止时恢复环境
  • env_link:指向 env_free_list 中的下一个 Env,env_free_list 指向列表中的第一个自由环境
  • env_id:该值唯一地标识当前使用这个 Env 结构的环境;在用户环境终止后,内核可能会将相同的 Env 结构重新分配给不同的环境,但是新环境将具有与旧环境不同的 Env_id
  • 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 结构所指的环境成为僵尸环境;僵尸环境将在下次捕获到内核时被释放
  • env_pgdir:此变量保存此环境页目录的内核虚拟地址

与 Unix 进程一样,JOS 环境耦合了“线程”和“地址空间”的概念:

  • 线程主要由保存的寄存器(env_tf 字段)定义、
  • 地址空间由 env_pgdir 指向的页目录和页表定义

要运行环境,内核必须用保存的寄存器和适当的地址空间设置 CPU。
struct Env 类似于 xv6 中的 struct proc,这两种结构都在 Trapframe 结构中保存环境(即进程)的用户模式寄存器状态。
在 JOS 中,各个环境不像 xv6 中的进程那样有自己的内核堆栈。内核中一次只能有一个 JOS 环境处于活动状态,因此 JOS 只需要一个内核堆栈。

🎎Allocating the Environments Array

在 Lab2 中,在 mem_init() 中为 pages[] 数组分配了内存,内核使用这个数组来跟踪哪些页是空闲的,哪些页不是。现在需要进一步修改 mem_init(),以分配 envs 数组。

🎞Creating and Running Environments

现在,将在 kern/env.c 中编写运行用户环境所必需的代码。因为我们还没有文件系统,所以我们将设置内核来加载嵌入内核本身的静态二进制 img。JOS 将这个二进制文件作为 ELF 可执行映像嵌入到内核中。
Lab3 的 GNUmakefile 在 obj/user/ 目录中生成许多二进制镜像。如果查看 kern/Makefrag,您会注意到一些 Magic 方法,它将这些二进制文件直接“链接”到内核可执行文件中。
链接器命令行上的 -b 选项使这些文件作为“原始”未解释的二进制文件链接,而不是作为编译器生成的常规 .o 文件链接(对于链接器,这些文件不必是 ELF img,也就是说它们可以是任何东西,甚至是文本文件或图片!)。
可以查看 obj/kern/kernel.sym,在构建内核之后,将注意到链接器“神奇地”生成了许多符号,如 _binary_obj_user_hello_start、_binary_obj_user_hello_end、and _binary_obj_user_hello_size。链接器通过提取二进制文件的文件名来生成这些符号名;这些符号为常规内核代码提供了一种引用嵌入二进制文件的方法。
在 kern/init.c 中的 i386_init() 中,将看到在环境中运行这些二进制映像代码。

🖼Handling Interrupts and Exceptions

此时,用户空间中的第一条 int $0x30 系统调用指令是一条死胡同:一旦处理器进入用户模式,就无法返回。现在需要实现基本的异常和系统调用处理,这样内核就可以从用户模式代码中恢复对处理器的控制。您应该做的第一件事是彻底熟悉 x86 中断和异常机制。

🥽Basics of Protected Control Transfer

异常和中断都是“Protected Control Transfer”,这会导致处理器从用户模式切换到内核模式(CPL=0),而不会给用户模式代码任何干扰内核或其他环境功能的机会。
在英特尔的术语中,中断是一种 Protected Control Transfer,通常由在处理器外部的异步事件引起,例如外部设备I/O活动的通知。相反,一个例外是由当前运行的代码同步引起的受保护的控制传输,例如由于被零除或无效的内存访问。
为了确保这些受保护的控制传输真正意义上受到保护,处理器的中断/异常机制被设计成在中断或异常发生时当前运行的代码不能任意选择进入内核的位置或方式;处理器将会确保只有在严格控制的条件下才能进入内核。在 x86 上,两种机制共同提供这种保护:

  1. The Interrupt Descriptor Table:处理器确保中断和异常只能导致内核在几个特定的、定义良好的入口点进入,这些入口点由内核本身决定,而不是由执行中断或异常时运行的代码决定。x86 允许多达 256 个不同的中断或异常入口点进入内核,每个入口点具有不同的中断向量。向量是介于 0 和 255 之间的数字。中断的向量由中断源决定:不同的设备、错误条件和应用程序对内核的请求生成具有不同向量的中断。CPU使用向量作为处理器中断描述符表(IDT)的索引,内核将其设置在内核私有内存中,与 GDT 非常相似。处理器从该表中的相应条目加载:
    1. 要加载到指令指针(EIP)寄存器的值,指向指定用于处理该类型异常的内核代码
    2. 要加载到代码段(CS)寄存器中的值,它在位 0-1 中包含运行异常处理程序的特权级别(在 JOS 中,所有异常都在内核模式下处理,特权级别为 0)
  2. The Task State Segment:在中断或异常发生之前,处理器需要一个地方来保存旧的处理器状态(例如在处理器调用异常处理程序之前,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 handle 软件中断向量 48(0x30),JOS 将其用作其系统调用中断向量(这个终端值是任意选择的)。
在 Lab4 中,我们将扩展 JOS 来处理外部生成的硬件中断,例如时钟中断。

👙An Example

我们把上述的部分结合到一起,通过一个例子进行分析:

假设处理器在用户环境中执行代码,并遇到一个试图被零除的除法指令。

  1. 处理器切换到由 TSS 的 SS0 和 ESP0 字段定义的堆栈,在 JOS 中这两个字段将分别保存值 GD_KD 和 KSTACKTOP
  2. 处理器将异常参数推送到内核堆栈上,从地址 KSTACKTOP 开始:

    1. +--------------------+ KSTACKTOP
    2. | 0x00000 | old SS | " - 4
    3. | old ESP | " - 8
    4. | old EFLAGS | " - 12
    5. | 0x00000 | old CS | " - 16
    6. | old EIP | " - 20 <---- ESP
    7. +--------------------+
  3. 因为我们正在处理一个除法错误,即 x86 上的中断向量 0,所以处理器读取 IDT 条目 0 并将 CS:EIP 设置为指向该条目所描述的处理函数

  4. handler 函数控制并处理异常,例如终止用户环境

对于某些类型的 x86 异常,除了上面的“标准”五个数据成员(SS、ESP、EFLAGS、CS、EIP)之外,处理器还会将另一个包含错误代码的字推送到堆栈上:

  1. +--------------------+ KSTACKTOP
  2. | 0x00000 | old SS | " - 4
  3. | old ESP | " - 8
  4. | old EFLAGS | " - 12
  5. | 0x00000 | old CS | " - 16
  6. | old EIP | " - 20
  7. | error code | " - 24 <---- ESP
  8. +--------------------+

🥾Nested Exceptions and Interrupts

处理器可以从内核和用户模式接收异常和中断。然而,只有在从用户模式进入内核时,x86 处理器才会自动切换堆栈,然后将其旧寄存器状态推送到堆栈上,并通过 IDT 调用相应的异常处理程序。如果中断或异常发生时处理器已经处于内核模式(CS 寄存器的低位 2 位已经为零),那么 CPU 只会在同一内核堆栈上推送更多的值。通过这种方式,内核可以优雅地处理由内核本身中的代码引起的嵌套异常。此功能是实现保护的一个重要工具,我们将在后面的系统调用部分中看到。
如果处理器已经处于内核模式并且发生嵌套异常,因为它不需要切换堆栈,所以它不会保存旧的 SS 或 ESP 寄存器。因此,对于不推送错误代码的异常类型,内核堆栈在异常处理程序的条目上如下所示:

  1. +--------------------+ <---- old ESP
  2. | old EFLAGS | " - 4
  3. | 0x00000 | old CS | " - 8
  4. | old EIP | " - 12
  5. +--------------------+

对于推送错误代码的异常类型,处理器会像以前一样,在旧 EIP 之后立即推送错误代码。
如果处理器在已处于内核模式时发生异常,并且由于堆栈空间不足等任何原因无法将其旧状态推送到内核堆栈上,则处理器无法进行任何恢复,因此它只会重置自身。

⛑Setting Up the IDT

现在我们已经拥有了在 JOS 中设置 IDT 和处理异常所需的基本信息。
在这一部分我们将设置 IDT 来处理中断向量 0-31 (处理器异常);在本实验的后续部分将会处理系统调用中断,即中断 32-47。
头文件 inc/trap.h 和 kern/trap.h 包含与中断和异常相关的重要定义,需要在编写代码前熟悉这些定义:

  • kern/trap.h 包含对内核严格私有的定义
  • inc/trap.h 包含对用户级程序和库可能有用的定义

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

  1. IDT trapentry.S trap.c
  2. +----------------+
  3. | &handler1 |---------> handler1: trap (struct Trapframe *tf)
  4. | | // do stuff {
  5. | | call trap // handle the exception/interrupt
  6. | | // ... }
  7. +----------------+
  8. | &handler2 |--------> handler2:
  9. | | // do stuff
  10. | | call trap
  11. | | // ...
  12. +----------------+
  13. .
  14. .
  15. .
  16. +----------------+
  17. | &handlerX |--------> handlerX:
  18. | | // do stuff
  19. | | call trap
  20. | | // ...
  21. +----------------+
  • 每个异常或中断都应该在 trapentry.S 中有自己的处理程序,trap_init() 应该用这些处理程序的地址初始化 IDT
  • 每个处理程序都应该在堆栈上构建一个 struct Trapframe(参见 inc/trap.h),并使用指向 Trapframe 的指针调用trap()(在 trap.c 中)
  • trap() 然后处理异常/中断或分派给特定的处理函数

    😕Page Faults, Breakpoints Exceptions, and System Calls

    通过上面的环节 JOS 内核已经具备了基本的异常处理功能,现在将要对其进行优化,以提供依赖于异常处理的重要操作系统原语(系统调用)。

    🎊Handling Page Faults

    页错误异常中断向量 14(interrupt vector 14,T_PGFLT)是一个特别重要的异常,我们将在本实验和下一个实验中进行大量练习。当处理器发生页错误时,它将导致错误的线性(即虚拟)地址存储在一个特殊的处理器控制寄存器 CR2 中。在 trap.c 中,我们提供了一个特殊函数 page_fault_handler() 的开头,用于处理页错误异常。
    这里我们需要完成 Exercise 5
    在实现系统调用时,您将在下面进一步细化内核的页面错误处理。

    🎑The Breakpoint Exception

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

    🎠System calls

    用户进程会通过系统调用请求内核为它们做一些事情。当用户进程调用系统调用时,处理器进入内核模式,处理器和内核协同保存用户进程的状态,内核执行相应的代码来执行系统调用,然后恢复用户进程。用户进程如何引起系统调用以及如何指定要执行的调用的具体细节因系统而异。
    在 JOS 内核中,我们将使用 int 指令,这会导致处理器中断。特别的,我们将使用 int $0x30 作为系统调用中断。我们为您定义常量 T_SYSCALL 代表 0x30(48)。您必须设置中断描述符,以允许用户进程引起该中断。请注意,中断 0x30 不能由硬件引发,因此允许用户代码引发它不会引起歧义。
    应用程序将在寄存器中传递系统调用号和系统调用参数,这样,内核就不需要在用户环境的堆栈或指令流中搜寻:

  • 系统调用号将位于 %eax 中

  • 参数(最多五个)将分别位于 %edx、%ecx、%ebx、%edi 和 %esi 中
  • 内核在 %eax 中传递返回值

调用系统调用的代码已经在 lib/syscall.c 中的 syscall() 中编写了一部分。您应该通读它并确保您理解发生了什么。

🧶User-mode startup

用户程序从 lib/entry.S 的顶部开始运行。经过一些设置后,此代码调用 lib/libmain.c 中的 libmain()。您应该修libmain() 以初始化全局指针 thisenv,使其指向 envs[] 数组中此环境的 Env 结构体(注意 lib/entry.S 已经定义了 env 来指向在 A 部分中设置的 UENVS 映射)。
提示:在 inc/lib.h 中查找并使用 sys_getenvid。
libmain() 然后调用 umain,在 hello 程序中,umain 位于 user/hello.c 中。请注意,在打印“hello,world”之后,它尝试访问 thisenv->env_id。这就是它之前出现故障的原因。

🍗Page faults and memory protection

内存保护是操作系统的一项重要功能,它可以确保一个程序中的错误不会损坏其他程序或损坏操作系统本身。
操作系统通常依赖硬件支持来实现内存保护。操作系统让硬件知道哪些虚拟地址有效,哪些无效。当一个程序试图访问一个无效的地址或它没有权限访问的地址时,处理器会在导致错误的指令处停止程序,然后将有关尝试操作的信息放入内核。如果故障是可修复的,内核可以修复它并让程序继续运行。如果故障无法修复,则程序将无法继续,因为它将永远无法通过导致故障的指令。
举一个可修复故障的例子——自动扩展堆栈。在许多系统中,内核最初分配一个堆栈页,然后如果一个程序在访问堆栈下一层的页面时出错,内核将自动分配这些页面并让程序继续。通过这样做,内核只分配程序所需的堆栈内存,但程序可以在一个任意大的堆栈的假象下工作。
系统调用为内存保护提出了一个有趣的问题。大多数系统调用接口允许用户程序向内核传递指针。这些指针指向要读取或写入的用户缓冲区。然后,内核在执行系统调用时取消对这些指针的引用。这有两个问题:

  1. 内核中的页错误可能比用户程序中的页面错误严重得多。如果内核页在操作自己的数据结构时出错,那就是内核错误,错误处理程序应该使内核(从而使整个系统)死机。但是,当内核取消对用户程序给它的指针的引用时,它需要一种方法来记住这些取消引用导致的任何页错误实际上都是代表用户程序的。
  2. 内核通常比用户程序有更多的内存权限。用户程序可能会传递一个指向系统调用的指针,该系统调用指向内核可以读写但程序不能读写的内存。内核必须小心,不要被欺骗去引用这样的指针,因为这样可能会泄露私有信息或破坏内核的完整性。

出于这两个原因,内核在处理用户程序提供的指针时必须非常小心。
现在,您将使用一种机制来解决这两个问题,该机制检查从用户空间传递到内核的所有指针。当程序向内核传递指针时,内核将检查地址是否在地址空间的用户部分,以及页表是否允许内存操作。

🤤Exercise

✨No1

修改 kern/pmap.c 中的 mem_init() 来分配和映射 envs 数组,这个数组由 NENV 个 Env 结构体组成。像 pages 数组一样,内存 backing env 也应该在 UENVS(在 inc/memlayout.h 中定义)以用户只读的方式映射,这样用户进程就可以从这个数组中读取。 使用 check_kern_pgdir() 进行验证。

在 mem_init() 函数中添加如下代码,不太清楚这部分做法的可以回看一下 Lab2:

  1. //////////////////////////////////////////////////////////////////////
  2. // Make 'envs' point to an array of size 'NENV' of 'struct Env'.
  3. // LAB 3: Your code here.
  4. envs = (struct Env*)boot_alloc(sizeof(struct Env) * NENV);
  5. memset(envs, 0, sizeof(struct Env) * NENV);
  6. //////////////////////////////////////////////////////////////////////
  7. // Map the 'envs' array read-only by the user at linear address UENVS
  8. // (ie. perm = PTE_U | PTE_P).
  9. // Permissions:
  10. // - the new image at UENVS -- kernel R, user R
  11. // - envs itself -- kernel RW, user NONE
  12. // LAB 3: Your code here.
  13. boot_map_region(kern_pgdir, UENVS, ROUNDUP(sizeof(struct Env) * NENV, PGSIZE),
  14. PADDR(envs), PTE_U);

🎏No2

在 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 标识符 %e 很有用—它会打印与错误代码相对应的描述。例如:

  1. r = -E_NO_MEM;
  2. panic("env_alloc: %e", r);

将会引发附带有“env_alloc: out of memory”消息的 panic。

🥚env_init()

将 envs 中的所有环境标记为 free,将其 env_id 设置为 0,并将其插入env_free_list。
应该确保环境在空闲列表中的顺序与它们在 envs 数组中的顺序相同(即,第一次调用 env_alloc() 返回envs[0])。

  1. void env_init(void) {
  2. // 初始化 envs array
  3. for (int i = NENV - 1; i >= 0; i--) {
  4. // 设置状态和 id
  5. envs[i].env_status = ENV_FREE;
  6. envs[i].env_id = 0;
  7. // 将其链接入 env_free_list
  8. envs[i].env_link = env_free_list;
  9. env_free_list = &envs[i];
  10. }
  11. // Per-CPU part of the initialization
  12. env_init_percpu();
  13. }

🥯env_setup_vm()

初始化环境 e 的内核虚拟内存布局;将会分配一个页目录,相应地设置 e->env_pgdir,并初始化新环境地址空间的内核部分。
需要注意的是,不要将任何东西映射到环境虚拟地址空间的用户部分。
成功时返回 0,错误时返回 <0;-如果无法分配页目录或表,则返回“-E_NO_MEM”。

  1. static int env_setup_vm(struct Env* e) {
  2. int i;
  3. struct PageInfo* p = NULL;
  4. // 为新的页目录分配内存
  5. if (!(p = page_alloc(ALLOC_ZERO)))
  6. return -E_NO_MEM;
  7. // 现在, 设置 e->env_pgdir 并且初始化页目录.
  8. //
  9. // 提示:
  10. // - 所有 envs 的虚拟地址空间都在 UTOP 的上方
  11. // (除了 UVPT, 我们将其设置在下方).
  12. // 可以查看 inc/memlayout.h 中的权限定义和布局.
  13. // 你可以直接替换 kern_pgdir.
  14. // (确保你在 Lab 2 中正确的设置了权限.)
  15. // - 初始化在 UTOP 下的 VA 应该是空闲的.
  16. // - 你不再需要调用 page_alloc.
  17. // - Note: 通常情况下, pp_ref 在 UTOP 上方映射时不需要 ++,
  18. // 但是 env_pgdir 是个例外 -- 你需要加一确保 env_free 可以正确执行
  19. // - kern/pmap.h 中的函数是非常有用的.
  20. // LAB 3: Your code here.
  21. e->env_pgdir = page2kva(p);
  22. memcpy(e->env_pgdir, kern_pgdir,PGSIZE);
  23. p->pp_ref++;
  24. // UVPT maps the env's own page table read-only.
  25. // Permissions: kernel R, user R
  26. e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
  27. return 0;
  28. }

🥫region_alloc()

为环境 env 分配 len 字节的物理内存,并将其映射到环境地址空间中的虚拟地址 va。

  • 不以任何方式归零或初始化映射页
  • 页应该可以由用户和内核读写
  • 如果任何分配尝试失败,请引发 panic

    1. static void region_alloc(struct Env* e, void* va, size_t len) {
    2. // LAB 3: Your code here.
    3. // (But only if you need it for load_icode.)
    4. //
    5. // 提示: 如果调用者传递了的 'va' 和 'len'是页对齐的
    6. // 使用 region_alloc 是容易的.
    7. // 你应该将 va 向下对 PGSIZE 取整,
    8. // 将 (va + len) 向上对 PGSIZE 取整.
    9. uintptr_t va_start = ROUNDDOWN((uintptr_t)va, PGSIZE);
    10. uintptr_t va_end = ROUNDUP((uintptr_t)va + len, PGSIZE);
    11. for (int i = va_start; i < va_end; i += PGSIZE) {
    12. struct PageInfo* pp = page_alloc(0);
    13. if (!pp) {
    14. panic("region_alloc: %e", -E_NO_MEM);
    15. return;
    16. }
    17. page_insert(e->env_pgdir, pp, (void*)i, PTE_U | PTE_W);
    18. }
    19. }

    🍱load_icode()

    为用户进程初始程序二进制文件、堆栈和处理器标志位:

  • 此函数仅在内核初始化期间、运行第一个用户模式环境之前调用

  • 此函数从 ELF 程序头中指示的相应虚拟地址开始,将 ELF 二进制映像中的所有可加载段加载到环境的用户内存中
  • 同时,它将这些段中的任何部分清除为零,这些部分在程序头中标记为映射,但实际上不存在于 ELF 文件中,即程序的 bss 部分

这些功能 Boot Loader 非常相似,只是引导加载程序还需要从磁盘读取代码。可以参考 boot/main.c 完成本函数。
最后,这个函数为程序的初始堆栈映射一个内存页;如果遇到问题,load_icode 应该引发 panic。

  1. static void load_icode(struct Env* e, uint8_t* binary) {
  2. // 提示:
  3. // 将每一个程序段加载到虚拟内存中,
  4. // 具体的地址在 ELF 段的 header 中指定.
  5. // 你只应该加载 ph->p_type == ELF_PROG_LOAD 的段.
  6. // 每一个段的虚拟内存地址可以在 ph->p_va 中找到,
  7. // 并且其在内存中的大小可以在 ph->p_memsz中找到.
  8. // ph->p_filesz 个字节应该从 ELF 二进制文件中加载, 开始于
  9. // 'binary + ph->p_offset', 因该拷贝到虚拟内存 ph->p_va 处.
  10. // 其他剩余分配的内存应该设置为 0.
  11. // (ELF header 应该具有下面的特征 ph->p_filesz <= ph->p_memsz.)
  12. // 使用之前实验中的函数分配和映射内存页.
  13. //
  14. // 现在所有的页保护位应该设置为 user read/write.
  15. // ELF 段没必要页对齐, 但是你可以假设本函数
  16. // 不会将两个不同的段映射到相同的虚拟地址.
  17. //
  18. // 你会发现 region_alloc 函数是有用的.
  19. //
  20. // 如果你可以直接将储存在ELF binary 中的数据
  21. // 直接移动到虚拟内存中, 加载段会变得更加简单.
  22. // 所以你需要在本函数中强制指定页目录.
  23. //
  24. // 最后你还需要做一些事情指定程序的入口,
  25. // 以便确保程序可以正确执行.
  26. // What? (See env_run() and env_pop_tf() below.)
  27. // LAB 3: Your code here.
  28. // 在 boot/main.c 中使用 readseg 函数读取 ELF
  29. // 这里只要将 binary 转型即可
  30. struct Elf* elfhdr = (struct Elf*)binary;
  31. struct Proghdr *ph, *eph;
  32. // 验证 ELF 是否有效?
  33. if (elfhdr->e_magic != ELF_MAGIC) {
  34. panic("elf header's magic is not correct\n");
  35. }
  36. // 加载所有的段 (ignores ph flags)
  37. ph = (struct Proghdr*)((uint8_t*)elfhdr + elfhdr->e_phoff);
  38. eph = ph + elfhdr->e_phnum;
  39. // CR3 寄存器可以直接存储页目录地址
  40. // 这里直接切换为环境 e 的页目录
  41. lcr3(PADDR(e->env_pgdir));
  42. for (; ph < eph; ph++) {
  43. // 验证 Proghdr 类型
  44. if (ph->p_type != ELF_PROG_LOAD) {
  45. continue;
  46. }
  47. // 验证二进制文件大小必须小于等于内存大小
  48. if (ph->p_filesz > ph->p_memsz) {
  49. panic("file size is great than memory size\n");
  50. }
  51. // 为环境 e 分配内存
  52. region_alloc(e, (void*)ph->p_va, ph->p_memsz);
  53. // 将二进制文件复制到内存中
  54. memcpy((void*)ph->p_va, binary + ph->p_offset, ph->p_filesz);
  55. // 清除 bss 节
  56. memset((void*)ph->p_va + ph->p_filesz, 0, (ph->p_memsz - ph->p_filesz));
  57. }
  58. // 将 ELF header 中的入口点存储到环境 e 的寄存器结构体中
  59. e->env_tf.tf_eip = elfhdr->e_entry;
  60. // 映射一个目录页在虚拟地址 USTACKTOP - PGSIZE
  61. // 处初始化程序的栈帧.
  62. // LAB 3: Your code here.
  63. region_alloc(e, (void*)(USTACKTOP - PGSIZE), PGSIZE);
  64. // 注意切换成内核的页目录
  65. lcr3(PADDR(kern_pgdir));
  66. }

🍤env_create()

使用 env_alloc 分配新的 env,使用 load_icode 将命名的 elf 二进制文件加载到其中,并设置其 env_type。
此函数仅在内核初始化期间、运行第一个用户模式环境之前调用;新环境的父ID设置为0。

  1. void env_create(uint8_t* binary, enum EnvType type) {
  2. // LAB 3: Your code here.
  3. int r;
  4. struct Env* e = NULL;
  5. // 新建新的 env 注意其父 id 是 0
  6. if ((r = env_alloc(&e, 0)) < 0) {
  7. panic("env create: %e", r);
  8. return;
  9. }
  10. // 向其中加载二进制文件
  11. load_icode(e, binary);
  12. // 设置环境类型
  13. e->env_type = type;
  14. }

🥣env_run()

将 curenv 的上下文切换为环境 e。需要注意的是,如果是第一次运行 curenv 是 NULL;本函数没有返回值。

  1. void env_run(struct Env* e) {
  2. // 步骤 1: 切换 curenv:
  3. // 1. 如果正在运行的 Env 状态为 ENV_RUNNING
  4. // 请设置为ENV_RUNNABLE ,
  5. // 2. 设置 'curenv' 为新的环境,
  6. // 3. 设置其状态为 ENV_RUNNING,
  7. // 4. 更新其 'env_runs' 计数,
  8. // 5. 使用 lcr3() 切换地址空间.
  9. // Step 2: 使用 env_pop_tf() 备份环境的初始寄存器值
  10. // 并且陷入到用户环境.
  11. // 提示: 此函数从 e->env_tf 加载新环境的状态.
  12. // 回顾上面编写的代码, 确保已将 e->env_tf 的相关部分设置为合理的值.
  13. // LAB 3: Your code here.
  14. if (curenv && curenv->env_status == ENV_RUNNING) {
  15. curenv->env_status = ENV_RUNNABLE;
  16. }
  17. curenv = e;
  18. e->env_status = ENV_RUNNING;
  19. e->env_runs++;
  20. lcr3(PADDR(e->env_pgdir));
  21. // 合适的值指的是我们刚刚设置的程序入口 eip
  22. env_pop_tf(&e->env_tf);
  23. }

运行结果如下,程序会陷入 0x30 号中断(使用 gdb 调试可以看到):

  1. // qemu
  2. wangjq@ubuntu:~/MIT/lab$ make qemu-gdb
  3. ***
  4. *** Now run 'make gdb'.
  5. ***
  6. qemu-system-i386 -drive file=obj/kern/kernel.img,index=0,media=disk,format=raw -serial mon:stdio -gdb tcp::26000 -D qemu.log -S
  7. 6828 decimal is 015254 octal!
  8. Physical memory: 131072K available, base = 640K, extended = 130432K
  9. check_page_free_list() succeeded!
  10. check_page_alloc() succeeded!
  11. check_page() succeeded!
  12. check_kern_pgdir() succeeded!
  13. check_page_free_list() succeeded!
  14. check_page_installed_pgdir() succeeded!
  15. [00000000] new env 00001000
  16. EAX=00000000 EBX=00000000 ECX=0000000d EDX=eebfde88
  17. ESI=00000000 EDI=00000000 EBP=eebfde60 ESP=eebfde54
  18. EIP=00800a3a EFL=00000092 [--S-A--] CPL=3 II=0 A20=1 SMM=0 HLT=0
  19. ES =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
  20. CS =001b 00000000 ffffffff 00cffa00 DPL=3 CS32 [-R-]
  21. SS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
  22. DS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
  23. FS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
  24. GS =0023 00000000 ffffffff 00cff300 DPL=3 DS [-WA]
  25. LDT=0000 00000000 00000000 00008200 DPL=0 LDT
  26. TR =0028 f01727c0 00000067 00408900 DPL=0 TSS32-avl
  27. GDT= f011a320 0000002f
  28. IDT= f0171fa0 000007ff
  29. CR0=80050033 CR2=00000000 CR3=003bc000 CR4=00000000
  30. DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
  31. DR6=ffff0ff0 DR7=00000400
  32. EFER=0000000000000000
  33. Triple fault. Halting for inspection via QEMU monitor.
  34. // gdb
  35. The target architecture is assumed to be i8086
  36. [f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
  37. 0x0000fff0 in ?? ()
  38. + symbol-file obj/kern/kernel
  39. (gdb) c
  40. Continuing.
  41. Program received signal SIGTRAP, Trace/breakpoint trap.
  42. The target architecture is assumed to be i386
  43. => 0x800a3a: int $0x30
  44. 0x00800a3a in ?? ()
  45. (gdb)

🎟No3

略,对于 x86 中断和异常机制的了解。

🎨No4

编辑 trapentry.S 和 trap.c 并实现上述特性。trapentry.S 中的 TRAPHANDLER 和 TRAPHANDLERNOEC 宏以及 inc/trap.h 中的 T* defines 应该有所帮助。

  • 需要在 trapentry.S 中为 inc/trap.h 中定义的每个 trap 添加一个入口点(使用上面提到的这些宏),并且提供 TRAPHANDLER 宏所引用的所有 trap
  • 您还需要修改 trap_init() 来初始化 IDT,以指向 trapentry.S 中定义的每个入口点(SETGATE 宏在这里很有用)

_alltraps 应该:

  1. 推送值以使堆栈看起来像 struct Trapframe
  2. 将 GD_KD加载到 %ds 和 %es 中
  3. pushl %esp 将指向 Trapframe 的指针作为 trap() 的参数传递
  4. 调用 trap 处理函数(处理函数需要返回此处么?)

考虑使用 pushal 指令;它非常适合 struct Trapframe 的布局。 完成之后,使用 make grade 命令进行测试,应该可以通过 divzero、softint 和 badsegrame 测试。

🥓宏定义描述

首先我们先看看需要实现的处理器的中断有那些,具体的信息可以查看这个 wiki

  1. // Trap numbers
  2. // These are processor defined:
  3. #define T_DIVIDE 0 // divide error
  4. #define T_DEBUG 1 // debug exception
  5. #define T_NMI 2 // non-maskable interrupt
  6. #define T_BRKPT 3 // breakpoint
  7. #define T_OFLOW 4 // overflow
  8. #define T_BOUND 5 // bounds check
  9. #define T_ILLOP 6 // illegal opcode
  10. #define T_DEVICE 7 // device not available
  11. #define T_DBLFLT 8 // double fault
  12. /* #define T_COPROC 9 */ // reserved (not generated by recent processors)
  13. #define T_TSS 10 // invalid task switch segment
  14. #define T_SEGNP 11 // segment not present
  15. #define T_STACK 12 // stack exception
  16. #define T_GPFLT 13 // general protection fault
  17. #define T_PGFLT 14 // page fault
  18. /* #define T_RES 15 */ // reserved
  19. #define T_FPERR 16 // floating point error
  20. #define T_ALIGN 17 // aligment check
  21. #define T_MCHK 18 // machine check
  22. #define T_SIMDERR 19 // SIMD floating point error

根据上面从 Handling Interrupts and ExceptionsSetting Up the IDT 的介绍,对于中断和异常应该已经有了粗浅的理解,我们首先看看题目中提到的几个宏定义完成的工作:

  1. // 设置正常 interrupt/trap gate 描述符.
  2. // - istrap: 1 代表 trap (= exception) gate, 0 代表 interrupt gate.
  3. // 查看 i386 参考的 section 9.6.1.3: "trap gate 和 interrupt gate 之间
  4. // 的不同是对于IF (the interrupt-enable flag) 的不同影响.
  5. // 抛出 interrupt gate 的 interrupt 向量会重置 IF,
  6. // 防止其他中断干扰现在的中断处理函数.
  7. // 随后 IRET 命令会从堆栈的 EFLAGS 中恢复 IF 的值.
  8. // 抛出 trap gate 的 interrupt 向量不会改变 IF."
  9. // - sel: interrupt/trap 处理程序的代码段选择器
  10. // - off: interrupt/trap 处理程序的代码段偏移量
  11. // - dpl: 描述符特权级别 -
  12. // 软件调用所需的特权级别
  13. // 此 interrupt/trap gate 显式使用 int 指令.
  14. #define SETGATE(gate, istrap, sel, off, dpl) \
  15. { \
  16. (gate).gd_off_15_0 = (uint32_t)(off)&0xffff; \
  17. (gate).gd_sel = (sel); \
  18. (gate).gd_args = 0; \
  19. (gate).gd_rsv1 = 0; \
  20. (gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
  21. (gate).gd_s = 0; \
  22. (gate).gd_dpl = (dpl); \
  23. (gate).gd_p = 1; \
  24. (gate).gd_off_31_16 = (uint32_t)(off) >> 16; \
  25. }
  26. /* TRAPHANDLER 定义全局可见的 trap 处理函数.
  27. * 它将 trap 代码入栈, 并且跳转到 _alltraps.
  28. * 使用 TRAPHANDLER 定义 CPU 自动将 error code 入栈的 tarp 处理函数.
  29. *
  30. * 你不应该在 C 代码中使用这个宏定义, 但是你或许
  31. * 在 C 语言中需要声明一个函数 (例如,在 IDT初始化时获得函数指针).
  32. * 你可以使用下面的语句声明
  33. * void NAME();
  34. * 其中 NAME 是传递给 TRAPHANDLER 的参数.
  35. */
  36. #define TRAPHANDLER(name, num) \
  37. .globl name; /* define global symbol for 'name' */ \
  38. .type name, @function; /* symbol type is function */ \
  39. .align 2; /* align function definition */ \
  40. name: /* function starts here */ \
  41. pushl $(num); \
  42. jmp _alltraps
  43. /* 使用 TRAPHANDLER_NOEC 定义 CPU 不会入栈 error code 的中断处理函数.
  44. * 它将 0 入栈代替 error code, 因此 trap frame 具有相同的结构.
  45. */
  46. #define TRAPHANDLER_NOEC(name, num) \
  47. .globl name; \
  48. .type name, @function; \
  49. .align 2; \
  50. name: \
  51. pushl $0; \
  52. pushl $(num); \
  53. jmp _alltraps

🥨trapentry.S

首先我们在 trapentry.S 中添加入口点,我们需要注意是否 CPU 会自动入栈 error code 使用不同的宏:

  1. /*
  2. * Lab 3: Your code here for generating entry points for the different traps.
  3. */
  4. TRAPHANDLER_NOEC(divide_handler, T_DIVIDE)
  5. TRAPHANDLER_NOEC(debug_handler, T_DEBUG)
  6. TRAPHANDLER_NOEC(nmi_handler, T_NMI)
  7. TRAPHANDLER_NOEC(brkpt_handler, T_BRKPT)
  8. TRAPHANDLER_NOEC(oflow_handler, T_OFLOW)
  9. TRAPHANDLER_NOEC(bound_handler, T_BOUND)
  10. TRAPHANDLER_NOEC(illop_handler, T_ILLOP)
  11. TRAPHANDLER_NOEC(device_handler, T_DEVICE)
  12. TRAPHANDLER(dblflt_handler, T_DBLFLT)
  13. TRAPHANDLER(tss_handler, T_TSS)
  14. TRAPHANDLER(segnp_handler, T_SEGNP)
  15. TRAPHANDLER(stack_handler, T_STACK)
  16. TRAPHANDLER(gpflt_handler, T_GPFLT)
  17. TRAPHANDLER(pgflt_handler, T_PGFLT)
  18. TRAPHANDLER_NOEC(fperr_handler, T_FPERR)
  19. TRAPHANDLER(align_handler, T_ALIGN)
  20. TRAPHANDLER_NOEC(mchk_handler, T_MCHK)
  21. TRAPHANDLER_NOEC(simderr_handler, T_SIMDERR)

接下来我们需要构建 _alltraps,此时我们需要构建 Trapframe 结构体,将 GD_KD加载到 %ds 和 %es 中,使用 pushl %esp 将指向 Trapframe 的指针作为 trap() 的参数传递。
首先我们先查看一下 Trapframe 结构体的定义:

  1. struct PushRegs {
  2. /* registers as pushed by pusha */
  3. uint32_t reg_edi;
  4. uint32_t reg_esi;
  5. uint32_t reg_ebp;
  6. uint32_t reg_oesp; /* Useless */
  7. uint32_t reg_ebx;
  8. uint32_t reg_edx;
  9. uint32_t reg_ecx;
  10. uint32_t reg_eax;
  11. } __attribute__((packed));
  12. struct Trapframe {
  13. struct PushRegs tf_regs;
  14. uint16_t tf_es;
  15. uint16_t tf_padding1;
  16. uint16_t tf_ds;
  17. uint16_t tf_padding2;
  18. uint32_t tf_trapno;
  19. /* below here defined by x86 hardware */
  20. uint32_t tf_err;
  21. uintptr_t tf_eip;
  22. uint16_t tf_cs;
  23. uint16_t tf_padding3;
  24. uint32_t tf_eflags;
  25. /* below here only when crossing rings, such as from user to kernel */
  26. uintptr_t tf_esp;
  27. uint16_t tf_ss;
  28. uint16_t tf_padding4;
  29. } __attribute__((packed));

其中的 tf_err、rf_eip、tf_cs、tf_eflags、tf_esp、tf_ss 已经由硬件自动入栈,那么我们需要手动构建的字段就只剩下:

  • tf_trapno
  • tf_ds
  • tf_es
  • tf_regs

根据提示,针对 tf_regs 字段需要将所有的寄存器入栈,那么我们只要使用 pushal 即可,因为在 tf_trapno、tf_ds、tf_es 之间已经有了 padding 字段,那么我们直接使用 pushl 入栈 32 位数即可。
所以由如下的代码:

  1. /*
  2. * Lab 3: Your code here for _alltraps
  3. */
  4. _alltraps:
  5. pushl %ds
  6. pushl %es
  7. pushal
  8. movw $GD_KD, %ax
  9. movw %ax, %ds
  10. movw %ax, %es
  11. pushl %esp /* trap(%esp) */
  12. call trap

🌯trap.c

只需要调用响应的宏定义即可:

  1. void trap_init(void) {
  2. extern struct Segdesc gdt[];
  3. // LAB 3: Your code here.
  4. void divide_handler();
  5. void debug_handler();
  6. void nmi_handler();
  7. void brkpt_handler();
  8. void oflow_handler();
  9. void bound_handler();
  10. void illop_handler();
  11. void device_handler();
  12. void dblflt_handler();
  13. void tss_handler();
  14. void segnp_handler();
  15. void stack_handler();
  16. void gpflt_handler();
  17. void pgflt_handler();
  18. void fperr_handler();
  19. void align_handler();
  20. void mchk_handler();
  21. void simderr_handler();
  22. SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_handler, 0);
  23. SETGATE(idt[T_DEBUG], 0, GD_KT, debug_handler, 0);
  24. SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0);
  25. SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 0);
  26. SETGATE(idt[T_OFLOW], 0, GD_KT, oflow_handler, 0);
  27. SETGATE(idt[T_BOUND], 0, GD_KT, bound_handler, 0);
  28. SETGATE(idt[T_ILLOP], 0, GD_KT, illop_handler, 0);
  29. SETGATE(idt[T_DEVICE], 0, GD_KT, device_handler, 0);
  30. SETGATE(idt[T_DBLFLT], 0, GD_KT, dblflt_handler, 0);
  31. SETGATE(idt[T_TSS], 0, GD_KT, tss_handler, 0);
  32. SETGATE(idt[T_SEGNP], 0, GD_KT, segnp_handler, 0);
  33. SETGATE(idt[T_STACK], 0, GD_KT, stack_handler, 0);
  34. SETGATE(idt[T_GPFLT], 0, GD_KT, gpflt_handler, 0);
  35. SETGATE(idt[T_PGFLT], 0, GD_KT, pgflt_handler, 0);
  36. SETGATE(idt[T_FPERR], 0, GD_KT, fperr_handler, 0);
  37. SETGATE(idt[T_ALIGN], 0, GD_KT, align_handler, 0);
  38. SETGATE(idt[T_MCHK], 0, GD_KT, mchk_handler, 0);
  39. SETGATE(idt[T_SIMDERR], 0, GD_KT, simderr_handler, 0);
  40. // Per-CPU setup
  41. trap_init_percpu();
  42. }

🥡Q1

为每个异常/中断设置单独的处理函数的目的是什么?

不同的中断需要不同的中断处理程序。因为对待不同的中断需要进行不同的处理方式:

  • 有些中断比如指令错误,就需要直接中断程序的运行
  • 而 I/O 中断只需要读取数据后,程序再继续运行

    🍣Q2

    需要做什么才能使 user/softint 程序正常运行? 评分脚本期望它产生一般保护错误(trap 13),但 softint 的代码为 int $14。 为什么这会产生中断向量 13? 如果内核实际上允许 softint 的 int $14 指令调用内核的页面错误处理程序(中断向量14)会发生什么?

因为当前系统运行在用户态下,特权级为 3,而 INT 指令为系统指令,特权级为 0。 会引发 General Protection Exception,即 13 号中断。

🥼No5

修改 trap_dispatch() 以便将页错误异常分派给 page_fault_handler()。 现在,运行 make grade 可以使 faultread、faultreadkernel、faultwrite 和 faultwritekernel 测试成功。 如果其中任何一个运行失败,找出原因并加以解决。可以使用 make run-x 或 make run-x-nox 将 JOS 引导到特定的用户程序中;例如,make run hello nox 运行 hello user 程序。

Exercise 5 比较简单,我们只需要通过 trapno 调用相应的 page_fault_handler() 函数即可。

  1. static void trap_dispatch(struct Trapframe* tf) {
  2. // Handle processor exceptions.
  3. // LAB 3: Your code here.
  4. switch (tf->tf_trapno) {
  5. case T_PGFLT:
  6. page_fault_handler(tf);
  7. break;
  8. default:
  9. break;
  10. }
  11. // Unexpected trap: The user process or the kernel has a bug.
  12. print_trapframe(tf);
  13. if (tf->tf_cs == GD_KT)
  14. panic("unhandled trap in kernel");
  15. else {
  16. env_destroy(curenv);
  17. return;
  18. }
  19. }

🧦No6

修改 trap_dispatch() 以使断点异常调用内核监视器。 现在,运行 make grade 可以通过 breakpoint 测试。

本部分也比较简单,只需要再添加一个分支即可,但是要注意的是我们在上面将 T_BRKPT 异常设置为了内核级错误,这不能由用户程序触发,所以我们需要修改其特权级:

  1. // 修改前
  2. SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 0);
  3. // 修改后
  4. SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 3);

完成代码:

  1. static void trap_dispatch(struct Trapframe* tf) {
  2. // Handle processor exceptions.
  3. // LAB 3: Your code here.
  4. switch (tf->tf_trapno) {
  5. case T_PGFLT:
  6. page_fault_handler(tf);
  7. return;
  8. case T_BRKPT:
  9. monitor(tf);
  10. return;
  11. default:
  12. break;
  13. }
  14. // Unexpected trap: The user process or the kernel has a bug.
  15. print_trapframe(tf);
  16. if (tf->tf_cs == GD_KT)
  17. panic("unhandled trap in kernel");
  18. else {
  19. env_destroy(curenv);
  20. return;
  21. }
  22. }

🍝Q3

breakpoint 测试用例将生成断点异常或一般保护故障,这具体取决于您如何初始化 IDT 中的断点条目(即,在 trap_init 调用 SETGATE)。为什么?需要如何设置它才能使断点异常按上面指定的方式工作?什么不正确的设置会导致它触发一般保护故障?

具体的原因可以参考 Q2,他们是一样的原因,修改方法在 No6 中已经说明了。

🧇Q4

将breakpoint 和 user/softint 测试一起考虑,你认为这些机制的关键点是什么?

DPL 的设置,可以限制用户态对关键指令的使用。

🛒No7

在内核中为中断向量 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 测试样例。

🧂添加签名

kern/trapentry.S 中添加入口:

  1. TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL)

kern/trap.c 中加入 IDT:

  1. void syscall_handler();
  2. SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3);

🥐trap_dispatch()

只需要注意参数的顺序和设置返回值即可。

  1. static void trap_dispatch(struct Trapframe* tf) {
  2. // Handle processor exceptions.
  3. // LAB 3: Your code here.
  4. switch (tf->tf_trapno) {
  5. case T_PGFLT:
  6. page_fault_handler(tf);
  7. return;
  8. case T_BRKPT:
  9. monitor(tf);
  10. return;
  11. case T_SYSCALL:
  12. // 依次传入 code 和 传递的参数
  13. int32_t ret_code = syscall(tf->tf_regs.reg_eax, tf->tf_regs.reg_edx,
  14. tf->tf_regs.reg_ecx, tf->tf_regs.reg_ebx,
  15. tf->tf_regs.reg_edi, tf->tf_regs.reg_esi);
  16. // 设置返回值
  17. tf->tf_regs.reg_eax = ret_code;
  18. return;
  19. default:
  20. break;
  21. }
  22. // Unexpected trap: The user process or the kernel has a bug.
  23. print_trapframe(tf);
  24. if (tf->tf_cs == GD_KT)
  25. panic("unhandled trap in kernel");
  26. else {
  27. env_destroy(curenv);
  28. return;
  29. }
  30. }

🍛syscall()

按照 inc/syscall.h 中定义的常量编写一个 switch 即可。

  1. // Dispatches to the correct kernel function, passing the arguments.
  2. int32_t syscall(uint32_t syscallno,
  3. uint32_t a1,
  4. uint32_t a2,
  5. uint32_t a3,
  6. uint32_t a4,
  7. uint32_t a5) {
  8. // Call the function corresponding to the 'syscallno' parameter.
  9. // Return any appropriate return value.
  10. // LAB 3: Your code here.
  11. switch (syscallno) {
  12. case SYS_cputs:
  13. sys_cputs((const char*)a1, (size_t)a2);
  14. return 0;
  15. case SYS_cgetc:
  16. return sys_cgetc();
  17. case SYS_getenvid:
  18. return sys_getenvid();
  19. case SYS_env_destroy:
  20. return sys_env_destroy(a1);
  21. case NSYSCALLS:
  22. return 0;
  23. default:
  24. return -E_INVAL;
  25. }
  26. }

🦪sys_cputs

使用 kern/pmap.c 中的函数 user_mem_assert 判断用户权限即可。

  1. // Print a string to the system console.
  2. // The string is exactly 'len' characters long.
  3. // Destroys the environment on memory errors.
  4. static void sys_cputs(const char* s, size_t len) {
  5. // Check that the user has permission to read memory [s, s+len).
  6. // Destroy the environment if not.
  7. // LAB 3: Your code here.
  8. user_mem_assert(curenv, s, len, PTE_U);
  9. // Print the string supplied by the user.
  10. cprintf("%.*s", len, s);
  11. }

运行 make run-hello 应出现页错误,如下:

  1. serial mon:stdio -gdb tcp::26000 -D qemu.log
  2. 6828 decimal is 15254 octal!
  3. Physical memory: 131072K available, base = 640K, extended = 130432K
  4. check_page_free_list() succeeded!
  5. check_page_alloc() succeeded!
  6. check_page() succeeded!
  7. check_kern_pgdir() succeeded!
  8. check_page_free_list() succeeded!
  9. check_page_installed_pgdir() succeeded!
  10. [00000000] new env 00001000
  11. Incoming TRAP frame at 0xefffffbc
  12. hello, world
  13. Incoming TRAP frame at 0xefffffbc
  14. [00001000] user fault va 00000048 ip 00800048
  15. TRAP frame at 0xf01b2000
  16. edi 0x00000000
  17. esi 0x00000000
  18. ebp 0xeebfdfd0
  19. oesp 0xefffffdc
  20. ebx 0x00000000
  21. edx 0xeebfde88
  22. ecx 0x0000000d
  23. eax 0x00000000
  24. es 0x----0023
  25. ds 0x----0023
  26. trap 0x0000000e Page Fault
  27. cr2 0x00000048
  28. err 0x00000004 [user, read, not-present]
  29. eip 0x00800048
  30. cs 0x----001b
  31. flag 0x00000092
  32. esp 0xeebfdfb8
  33. ss 0x----0023
  34. [00001000] free env 00001000
  35. Destroyed the only environment - nothing more to do!
  36. Welcome to the JOS kernel monitor!
  37. K>

🎂No8

将所需的代码添加到对应的文件中,然后引导启动内核:

  • 您应该看到 user/hello 打印“hello,world”,然后打印“i am environment 00001000”
  • 然后,user/hello 试图通过调用 sys_env_destroy() 来“退出”(参见 lib/libmain.c 和 lib/exit.c)

由于内核当前只支持一个用户环境,它应该报告它已经破坏了唯一的环境,然后进入内核监视器。 现在应该通过 hello 测试样例。

使用 sys_getenvid() 系统调用获得 env 的 id 因为其低 10 位是在 envs 数组中的索引,使用 ENVX 进行提取即可得到当前环境。

  1. void libmain(int argc, char** argv) {
  2. // set thisenv to point at our Env structure in envs[].
  3. // LAB 3: Your code here.
  4. thisenv = &envs[ENVX(sys_getenvid())];
  5. // save the name of the program so that panic() can use it
  6. if (argc > 0)
  7. binaryname = argv[0];
  8. // call user main routine
  9. umain(argc, argv);
  10. // exit gracefully
  11. exit();
  12. }

🥗No9

修改 kern/trap.c 以在内核中发生段错误时引起 panic。 提示:为了判断页错误发生在用户模式还是内核模式,我们需要检查 tf_cs 的低位。 阅读 kern/pmap.c 中的 user_mem_assert 并且实现 user_mem_check 函数。 修改 kern/syscall.c 检查系统调用的参数是否正确。 引导你的内核,运行 user/buggyhello。环境应该被正常销毁,并且内核不会引发 panic。你应该看到: [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、stabs、stabstr 上调用 user_mem_check。如果你现在运行 user/breakpoint,你将可以运行内核中的 backtrace 函数,并且看到 backtrace 在内核引发页错误 panic 之前输出到 lib/libmain.c 的调用信息。是什么引发了段错误?你不必修复它,凡是要明白为什么发生了。

🧀trap.c

修改 kern/trap.c 以在内核中发生段错误时引起 panic。

  1. static void trap_dispatch(struct Trapframe* tf) {
  2. // Handle processor exceptions.
  3. // LAB 3: Your code here.
  4. int32_t ret_code;
  5. switch (tf->tf_trapno) {
  6. case T_PGFLT:
  7. // 通过检查 CLP 位 (CS 寄存器的低两位) 判断特权级别
  8. if ((tf->tf_cs & 0x3) == 0) {
  9. panic("Page fault in kernel code, halt\n");
  10. }
  11. page_fault_handler(tf);
  12. return;
  13. case T_BRKPT:
  14. monitor(tf);
  15. return;
  16. case T_SYSCALL:
  17. ret_code = syscall(tf->tf_regs.reg_eax, tf->tf_regs.reg_edx,
  18. tf->tf_regs.reg_ecx, tf->tf_regs.reg_ebx,
  19. tf->tf_regs.reg_edi, tf->tf_regs.reg_esi);
  20. tf->tf_regs.reg_eax = ret_code;
  21. return;
  22. default:
  23. break;
  24. }
  25. // Unexpected trap: The user process or the kernel has a bug.
  26. print_trapframe(tf);
  27. if (tf->tf_cs == GD_KT)
  28. panic("unhandled trap in kernel");
  29. else {
  30. env_destroy(curenv);
  31. return;
  32. }
  33. }

🧈pmap.c

检查是否允许 env 访问具有 perm | PTE_P 权限的内存范围 [va,va+len)。
通常 perm 至少会包含 PTE_U,但这不是必需的。
va 和 len 不需要页对齐,所以必须测试包含任何该范围的每个页:

  • len/PGSIZE
  • len/PGSIZE+1
  • len/PGSIZE+2

如果地址低于 ULIM 并且页表授予它权限,则用户程序可以访问虚拟地址。
如果有错误,请将 user_mem_check_addr 变量设置为第一个错误的虚拟地址。
如果用户程序可以访问此地址范围,则返回 0,否则返回 -E_FAULT。

  1. int user_mem_check(struct Env* env, const void* va, size_t len, int perm) {
  2. // LAB 3: Your code here.
  3. pte_t* pte;
  4. void *va_start, *va_end;
  5. // 向上向下对 PGSIZE 取整
  6. va_start = ROUNDDOWN((void*)va, PGSIZE);
  7. va_end = ROUNDUP((void*)(va + len), PGSIZE);
  8. // 验证起始地址
  9. if (va_start > (void*)ULIM) {
  10. user_mem_check_addr = (uintptr_t)va;
  11. return -E_FAULT;
  12. }
  13. for (va_start; va_start < va_end; va_start += PGSIZE) {
  14. // 获得本环境的虚拟地址对应的页表
  15. pte = pgdir_walk(env->env_pgdir, va_start, 0);
  16. // 检验 pte 是否存在、有效、权限相同
  17. if (!pte || !(*pte & PTE_P) || (*pte & perm) != perm) {
  18. // 注意第一个页的判特
  19. user_mem_check_addr = va_start < va ? (uintptr_t)va : (uintptr_t)va_start;
  20. return -E_FAULT;
  21. }
  22. }
  23. return 0;
  24. }

🥪kdebug.c

用指定指令地址 addr 的信息填写 info 结构。如果找到信息,则返回0,否则返回负值。但即使返回负数,它也会将一些信息存储到 *info 中。只需要简单的进行验证即可,此时我们运行 make grade 可以通过所有的测试用例。

  1. const struct UserStabData* usd = (const struct UserStabData*)USTABDATA;
  2. // Make sure this memory is valid.
  3. // Return -1 if it is not. Hint: Call user_mem_check.
  4. // LAB 3: Your code here.
  5. if (user_mem_check(curenv, usd, sizeof(struct UserStabData), PTE_U) < 0) {
  6. return -1;
  7. }
  8. stabs = usd->stabs;
  9. stab_end = usd->stab_end;
  10. stabstr = usd->stabstr;
  11. stabstr_end = usd->stabstr_end;
  12. // Make sure the STABS and string table memory is valid.
  13. // LAB 3: Your code here.
  14. stablen = stab_end - stabs + 1;
  15. strlen = stabstr_end - stabstr + 1;
  16. if (user_mem_check(curenv, stabs, stablen, PTE_U) < 0) {
  17. return -1;
  18. }
  19. if (user_mem_check(curenv, stabstr, strlen, PTE_U) < 0) {
  20. return -1;
  21. }

🥩No10

此时 No10 也可以正常通过。