MIT6.828 LAB3_PartA_User Environments and Exception Handling


Part A: User Environments and Exception Handling


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

Exercise1:分配环境数组

Modify mem_init() in kern/pmap.c to allocate and map the envs array. This array consists of exactly NENV instances of the Env structure allocated much like how you allocated the pages array. Also like the pages array, the memory backing envs should also be mapped user read-only at UENVS (defined in inc/memlayout.h) so user processes can read from this array.

有了lab2的铺垫和理解,这个Exe就很容易做了。

   // LAB 3: Your code here.
    envs = (struct Env*)boot_alloc(sizeof(struct Env)*NENV);
    memset(envs, 0, sizeof(struct Env)*NENV);
    
  // LAB 3: Your code here.
    boot_map_region(kern_pgdir, 
                    UENVS, 
                    ROUNDUP((sizeof(struct Env)*NENV), PGSIZE),
                    PADDR(envs),
                    PTE_U);

创建和运行环境(进程)

在这里,环境和进程是可以对等的,都指程序运行期间的抽象。不直接叫进程是因为jos中实现的系统调用和UNIX是有差别的。

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

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。 链接器通过修改二进制文件的文件名来生成这些符号名称; 这些符号为常规内核代码提供了引用嵌入式二进制文件的方法。

1.3. Exercise 2

in the file env.c, finish coding the following functions:
  • env_init()
  • env_setup_vm()
  • region_alloc()
  • load_icode()
  • env_create()
  • env_run()

    env_init()

    void
    env_init(void)
    {
      // Set up envs array
      // LAB 3: Your code here.
      int i;
      // 确保最小的env在最前端
      for (i = NENV-1; i >= 0; --i) {
          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()

为当前的进程分配一个页,用来存放页表目录,同时将内核部分的内存的映射完成。所有的进程,不论是内核还是用户,在虚地址UTOP之上的内容都是一样的。

在lab2的mem_init()中我们完成了地址空间的内核部分映射

load_icode()

这里的拷贝到指定的虚地址处,是指用户空间的虚地址,而不是内核空间的虚地址,所以还需要用lcr3函数加载用户空间的页表目录才能将地址转换为用户空间地址。

结果

通常,您会看到CPU重置和系统重启。

但我的实验结果并不是CPU重置和系统重启, 而是访问了一个错误地址Trying to execute code outside RAM or ROM at 0x97960000,导致qemu都abort了。 暂时不知道为什么。

check_page_installed_pgdir() succeeded!
[00000000] new env 00001000
qemu-system-i386: Trying to execute code outside RAM or ROM at 0x97960000
This usually means one of the following happened:

我们很快就会解决这个问题,但是现在我们可以使用调试器来检查我们是否正在进入用户模式。

(gdb) b *0xf0100092


(gdb) x/10i 
   0xf0102dd1 <env_run+1>:    mov    %esp,%ebp
   0xf0102dd3 <env_run+3>:    sub    $0x8,%esp
   0xf0102dd6 <env_run+6>:    mov    0x8(%ebp),%eax
   0xf0102dd9 <env_run+9>:    mov    0xf017be44,%edx
   0xf0102ddf <env_run+15>:    test   %edx,%edx
   0xf0102de1 <env_run+17>:    je     0xf0102df0 <env_run+32>
   0xf0102de3 <env_run+19>:    cmpl   $0x3,0x54(%edx)
   0xf0102de7 <env_run+23>:    jne    0xf0102df0 <env_run+32>
   0xf0102de9 <env_run+25>:    movl   $0x2,0x54(%edx)
   0xf0102df0 <env_run+32>:    mov    %eax,0xf017be44
可以直接断电打到vcprintf中的sys_cputs。
b *80012c


void
sys_cputs(const char *s, size_t len)
{
  800a84:    55                       push   %ebp
  800a85:    89 e5                    mov    %esp,%ebp
  800a87:    57                       push   %edi
  800a88:    56                       push   %esi
  800a89:    53                       push   %ebx
    //
    // The last clause tells the assembler that this can
    // potentially change the condition codes and arbitrary
    // memory locations.

    asm volatile("int %1\n"
  800a8a:    b8 00 00 00 00           mov    $0x0,%eax
  800a8f:    8b 4d 0c                 mov    0xc(%ebp),%ecx
  800a92:    8b 55 08                 mov    0x8(%ebp),%edx
  800a95:    89 c3                    mov    %eax,%ebx
  800a97:    89 c7                    mov    %eax,%edi
  800a99:    89 c6                    mov    %eax,%esi
  800a9b:    cd 30                    int    $0x30

(gdb) si
=> 0x800a9b:    int    $0x30

单步执行,最后死在了hellovcprintf()中的sys_cputs(b.buf, b.idx);函数。照这样看来,内核加载了hello.c的程序,但是在什么时候加载(也就是初始化env[0])的呢?

处理中断和异常

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

Exercise 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.

IDT可以驻留在物理内存中的任何位置。 处理器通过IDT寄存器(IDTR)定位IDT。

IDT包含了三种描述子

  • 任务门
  • 中断门
  • 陷阱门


每个entry为8bytes,有以下关键bit:
16~31:code segment selector
0~15 & 46-64:segment offset (根据以上两项可确定中断处理函数的地址)
Type (8-11):区分中断门、陷阱门、任务门等
DPL:Descriptor Privilege Level, 访问特权级
P:该描述符是否在内存中

1.5. 保护控制转移基础

为了确保这些受保护的控制传输实际受到保护,处理器的中断/异常机制被设计为使得当发生中断或异常时,当前运行的代码无法随意选择内核的进入点或方式。有两种机制

  • 中断向量表:

    • x86允许最多256个不同的中断或异常入口点进入内核,每个入口点都有不同的中断向量。 向量是介于0和255之间的数字。中断的向量由中断源决定:不同的设备,错误条件和对内核的应用程序请求会产生具有不同向量的中断。 CPU使用向量作为处理器中断描述符表(IDT)的索引,其由内核在内核专用内存中设置,就像GDT一样。 处理器从该表中的相应条目处加载信息。
  • 任务状态段

    • 状态保存于状态恢复

尽管TSS很大并且可能用于各种目的,但是JOS仅使用它来定义处理器在从用户模式转换到内核模式时应切换到的内核堆栈。 由于JOS中的“内核模式”是x86上的特权级别0,因此处理器在进入内核模式时使用TSS的ESP0和SS0字段来定义内核堆栈。 JOS不使用任何其他TSS字段。

异常与中断的嵌套

处理器可以从内核和用户模式中获取异常和中断。 然而,只有当从用户模式进入内核时,x86处理器才会在将旧的寄存器状态压栈并通过IDT调用相应的异常处理程序之前自动切换堆栈。 如果处理器在发生中断或异常时已经处于内核模式(CS寄存器的低2位已经为零),则CPU只会向同一内核堆栈压入参数。通过这种方式,内核可以优雅地处理由内核本身内的代码引起的嵌套异常。 此功能是实现保护的重要工具,我们将在后面的系统调用一节中看到。

处理器的嵌套异常功能有一个重要的注意点。 如果处理器在已经处于内核模式时发生异常,并且由于任何原因(例如缺少堆栈空间)而无法将其旧状态压入内核堆栈,则处理器无法进行任何恢复,因此它只能重启。 不用说,设计内核应该避免这种情况的发生。

1.7. 建立IDT表

The header files inc/trap.h and kern/trap.h contain important definitions related to interrupts and exceptions that you will need to become familiar with. The file kern/trap.h contains definitions that are strictly private to the kernel, while inc/trap.h contains definitions that may also be useful to user-level programs and libraries.

1.8. Exercise 4

Edit trapentry.S and trap.c and implement the features described above. The macros TRAPHANDLER and TRAPHANDLER_NOEC in trapentry.S should help you, as well as the T_ defines in inc/trap.h. You will need to add an entry point in trapentry.S (using those macros) for each trap defined in inc/trap.h, and you'll have to provide _alltraps which the TRAPHANDLER macros refer to. You will also need to modify trap_init() to initialize the idt to point to each of these entry points defined in trapentry.S; the SETGATE macro will be helpful here.

1.8.1. 源码阅读

  1. trapentry.S
#define TRAPHANDLER(name, num)                        \
    .globl name;        /* define global symbol for 'name' */    \
    .type name, @function;    /* symbol type is function */        \
    .align 2;        /* align function definition */        \
    name:            /* function starts here */        \
    pushl $(num);                            \
    jmp _alltraps
.type 指令指定 name 这个符号是函数类型的
name 为函数名
@function 表示函数内容开始

1.8.2. 过程分析

首先需要产生一个struct trapframe结构的栈, 而压参数是从右往左,对应这个结构体就是从下往上对应。注意到tf_esp以及tf_ss只用在发生特权级变化的时候才会有,再往上是由硬件自动产生的。在TRAPHANDLER函数中压入了trapno,同时为了保证没有错误代码的trap能符合这个结构体,使用TRAPHANDLER_NOEC压入0占位err。最后我们的程序只需要压入trapno以上的参数即可。

struct Trapframe {
    struct PushRegs tf_regs;
    uint16_t tf_es;
    uint16_t tf_padding1;
    uint16_t tf_ds;
    uint16_t tf_padding2;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding3;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding4;
} __attribute__((packed));

还需要修改trap_init()以初始化 idt 以指向trapentry.S中定义的每个入口点; SETGATE宏在这里会有所帮助。
这里因为对x86的IDT不太熟悉(Exercise 3略看了)导致有点不太理解各个参数的含义。比如说gate这个概念。

#define SETGATE(gate, istrap, sel, off, dpl)

  • istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
  • sel: 代码段选择子 for interrupt/trap handler

    • off: 代码段偏移 for interrupt/trap handler
    • dpl: 描述符特权级
  void divide_handler();
    void debug_handler();
    void nmi_handler();
    void brkpt_handler();
    void oflow_handler();
    void bound_handler();
    void device_handler();
    void illop_handler();
    void tss_handler();
    void segnp_handler();
    void stack_handler();
    void gpflt_handler();
    void pgflt_handler();
    void fperr_handler();
    void align_handler();
    void mchk_handler();
    void simderr_handler();
    void syscall_handler();
    void dblflt_handler();
    // LAB 3: Your code here.
    // GD_KT 全局描述符, kernel text
    SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_handler, 0);
    SETGATE(idt[T_DEBUG], 0, GD_KT, debug_handler, 0);
    SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0);
    SETGATE(idt[T_BRKPT], 0, GD_KT, brkpt_handler, 0);
    SETGATE(idt[T_OFLOW], 0, GD_KT, oflow_handler, 0);
    SETGATE(idt[T_BOUND], 0, GD_KT, bound_handler, 0);
    SETGATE(idt[T_DEVICE], 0, GD_KT, device_handler, 0);
    SETGATE(idt[T_ILLOP], 0, GD_KT, illop_handler, 0);
    SETGATE(idt[T_DBLFLT], 0, GD_KT, dblflt_handler, 0);
    SETGATE(idt[T_TSS], 0, GD_KT, tss_handler, 0);
    SETGATE(idt[T_SEGNP], 0, GD_KT, segnp_handler, 0);
    SETGATE(idt[T_STACK], 0, GD_KT, stack_handler, 0);
    SETGATE(idt[T_GPFLT], 0, GD_KT, gpflt_handler, 0);
    SETGATE(idt[T_PGFLT], 0, GD_KT, pgflt_handler, 0);
    SETGATE(idt[T_FPERR], 0, GD_KT, fperr_handler, 0);
    SETGATE(idt[T_ALIGN], 0, GD_KT, align_handler, 0);
    SETGATE(idt[T_MCHK], 0, GD_KT, mchk_handler, 0);
    SETGATE(idt[T_SIMDERR], 0, GD_KT, simderr_handler, 0);
    SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3);
    

make grade结果:
divzero: OK (4.4s)
softint: OK (3.4s)
badsegment: OK (3.1s)
Part A score: 30/30

1.9. Questions

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

不同的中断需要不同的中断处理程序。因为对待不同的中断需要进行不同的处理方式,有些中断比如指令错误,就需要直接中断程序的运行。 而I/O中断只需要读取数据后,程序再继续运行。

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

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

PartA问题

  1. Exe2: 内核加载了hello.c的程序,但是在什么时候加载(也就是初始化env[0])的呢?
  2. Exe4: 为什么handler不用具体实现,只需要声明再注册到IDT就行了 ?

声明:一丁点儿的网络日志|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - MIT6.828 LAB3_PartA_User Environments and Exception Handling


勿在浮沙筑高台,每天进步一丁点儿!