MIT6.828 lab3 part B

环境

ubuntu 20.04 64位系统

正文

现在你的内核有了最基本的中断处理能力,我们还会进一步的对它升级使得它能够提供依赖于异常处理的重要的操作系统原语。

Handing Page Fault

page fault,第14号中断(T_PGFLT),是一个非常重要的中断贯穿在本次lab以及下一次lab。当处理来处理page fault的时候,它会将发生错误的虚拟地址放在cr2寄存器当中。在kern/trap.c中已经提供了一个基本的函数page_fault_handler用于处理page fault。
Exercise

修改trap_dispatch()来讲page fault转发到 page_fault_handler()中断处理函数。运行make grade,应该可以通过faultread,faultreadkernel,faultwrite和faultwritekernel。请记住你可以将JOS boot到一个特定的程序用命令make run-x,比如说make run-hello来运行hello这个用户程序

说实话这道题其实不难做。不过为了更好的理解,我们首先来看一下中断处理的过程,在 lab3 part A中我们设置了各个中断的entry point并且将这些entry point写入到了IDT当中。当中断发生的时候,最后一条语句是call trap。跳转到了trap函数去执行,trap函数位于 kern/trap.c当中。代码如下:
trap.c:

void
trap(struct Trapframe *tf)
{      
      //不知道这句什么意思
    asm volatile("cld" ::: "cc");
    assert(!(read_eflags() & FL_IF));
    cprintf("Incoming TRAP frame at %p\n", tf); 
      //这里没有完全理解,我认为是:如果发现异常来自
      //用户程序,就会将当前的curenv->env_tf替换为发生异常的trapframe
      //用于之后的返回
    if ((tf->tf_cs & 3) == 3) {
        assert(curenv);
        curenv->env_tf = *tf;
        tf = &curenv->env_tf;
    }
    last_tf = tf;
        //trap_dispatch将发生异常的trapframe发送到对应的中断处理函数
    trap_dispatch(tf);
    assert(curenv && curenv->env_status == ENV_RUNNING);
        //返回到用户程序继续执行
    env_run(curenv);
}

上面代码比较重要的两句,我想就是trap_dispatch(tf);env_run(curenv);分别做的事就是将trapframe分发到对应的中断处理函数当中去和返回发生中断的进程。所以,题目的要求,修改trap_dispatch()函数,使他能够处理page fault。
回想一下在lab2中,还记得我们压入了一个trapno,这时候trapno就用上了,我们使用trapno来判断当前发生的中断是哪个中断,在做对应的处理就行。
trap_dispatch():

switch(tf->tf_trapno) {
  case T_PGFLT:
    page_fault_handler(tf):
    break;
  default:
    print_trapfrane(tf);
    if(tf->tf_cs == GD_KT) 
          panic("unhandled trap in kernel");
    else {
       env_destory(curenv);
       return;
  }
    break;
} 

The Breakpoint Exception

breakpoint excepiton(就是debug中用的断点),三号中断,debugger通常将它(int 3)暂时替代程序代码。在JOS中,我们使用JOS kernel monitor来暂时替代这个。
Exercise 6:

修改trap_dispatch()的代码,让它能够通过调用monitor来处理breakpoint exception。如果运行make grade,通过breakpoint 测试点

这道题的实现比较简单,如果就上面那道题的多加一个case,多余的代码就不重复了,另外我在part A中已经将breakpoint 这个加入到了IDT当中,所以这里就不再重复,代码如下:

case T_BRKPT:
    monitor(tf);
    break;

challenge偷懒没做。。

question:

  1. break point test会根据你如何在IDT中如何初始化break point entry来产生break point 异常或者general protection fault。为什么?
    这个问题的关键就是DPL的设置,我在partA当中已经将breakpoint这个中断的DPL设置为3。这样才可以通过测试。如果我将DPL改为0,那么就会触发general protection fault,因为用户程序的DPL=3,不可以调用DPL=0的中断。下面是DPL=0的情况的截图:
  2. 你认为这些机制的关键机制是什么?
    说白了,这些情况之所以会发生的原因就是中断的DPL设置。

System calls

用户进程通过系统调用(system call)来请求内核来做一些事情。当用户进程调用系统调用的时候,处理器进入了内核模式,内核和处理器互相帮助来保存用户程序的状态,内核执行代码来完成系统调用然后返回到用户程序。用户进程如何吸引内核的注意以及如何指定系统调用不同的系统会不同。
在JOS中,我们将会使用int指令,它会造成处理器中断。我们使用int 0x30来调用系统调用。你需要在IDT中设置好来使得用户进程调用。
程序使用寄存器来传递它所需要的是系统调用号以及参数。这样一来,内核不需要从用户栈拿参数了。system call number 放在eax寄存器当中,然后参数(最多只有五个)将会放在edx,ecx,ebx,edi,esi寄存器当中。内核将会把返回值放在eax寄存器当中。调用sysrem call的汇编代码已经给你了,在lib/syscall.c的syscall()当中。看懂这个函数涉及到GCC inline assembly,先略,后续补。

Exercise 7

为T_SYSCALL 增加处理函数。你需要修改kern/trapentry.S和kern/trap.c中的trap_init()。同样的还需要修改下trap_dispatch()让它能够通过调用kern/syscall.c中的syscall()来处理中断,同时还需要弄好返回值以及各个参数。最后你还需要实现kern/syscall.c中的syscall()函数。确保当system call number无效的时候返回-E_INVAL 你应该阅读并且理解lib/syscall.c中的代码,尤其是内联汇编代码
并且还要处理列在inc/syscall.h中的system call。
运行hello程序,此时应该可以打印出hello worlld,并且运行make grade,此时应该已经通过了testbss测试点。

代码实现:
这道题要做的事情不少,一个一个点来看。首先把修改下kern/trapentry.S和kern/trap.c的代码,要加入system call的内容,这部分我已经在part A当中就做了,这里在重复一下吧

void trap_init(void)
{
    extern struct Segdesc gdt[];

    // LAB 3: Your code here.
    //#define SETGATE(gate, istrap, sel, off, dpl) 
    // 把SETGATE这个宏放在这里,帮助理解
    SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3); //系统调用对应的IDT entry
    // Per-CPU setup 
    trap_init_percpu();
}

好了接下来需要修改下trap_dispatch(),让它能够调用kern/syscall.c中的syscall()来处理中断。同样的这个也不难,只要新增加一个case就行。它调用syscall()函数来处理中断。
这一部分的实现稍微有点困难,一开始我没有理解整个代码的流程。一个system call()的流程是这样的:
1.lib/syscall.c中的各个sys_开头的函数是对外提供的,如果用户程序需要调用系统调用,就使用这个写函数。
2.当用户程序调用某个系统调用的时候,lib/syscall.c中的syscall()会根据system call number来发起对应的int指令
3.产生0x30中断,对应到IDT中的system_handler。跳转到trapentry.S中执行

  1. 压入了必要的数据后,接着跳转到kern/trap.c中的trap函数去执行
  2. trap()跳转到trap_dispatch()去执行
  3. 根据swtich case,接着调用的syscall函数(这个函数是kern/syscall.c中的),并且传入参数(即eax,edx等寄存器的内容)
  4. syscall函数中,根据system call number来决定最终的中断处理函数,比如说SYS_cputs,那么就调用sys_cputs()函数

前面说到传给系统调用的参数是放在寄存器的当中的,而非放在栈当中。我们首先需要明白 lib/syscall.c/syscall()中参数和寄存器之间对应关系,不过暂时不需要完全了解gcc内联汇编的语法。
参考这里

寄存器和简写字母关系

再来看syscall()的代码,如下所示。我们就知道了如何正确的传参了:

        case T_SYSCALL:
            ret_value = syscall(
                tf->tf_regs.reg_eax,
                tf->tf_regs.reg_edx,
                tf->tf_regs.reg_ecx,
                tf->tf_regs.reg_ebx,
                tf->tf_regs.reg_edi,
                tf->tf_regs.reg_esi
            );
            tf->tf_regs.reg_eax = ret_value;
            break;

还有一个问题,在trap_dispatch()中调用的syscall()并不是在lib/syscall.c中的。所以为了可以正确的调用,我们必须保证kern/syscall.c/syscall()和lib/syscall.c/syscall()两者之间参数的传递顺序要相同。
好了既然已经知道如何往kern/syscall.c的syscall()中传递参数,接下来要做的就是在syscall()当中做合适的参数判断,来选择对应的系统调用函数。看kern/syscall.c的syscall()的参数,很自然地我们使用syscallno来区分我们将要使用的系统调用函数,这个可以使用switch case来完成

User-mode startup

一个用户程序在lib/entry.S最开始。经过一些设置后,接着调用了libmain()函数,在lib/main.c当中。你应该修改libmain()函数来初始thisenv这个指针,让它指向当前进程在envs[]数组中的属于当前进程的Env结构。提示:看看inc/env.h和使用sys_getenvid
libmain()接着调用umain()函数。在打印"hello world"后,它尝试去访问thisenv->env_id。这就是为什么之前会出错的原因了。
Exercise 8

增加必要的代码,然后重启内核。此时运行hello程序应该或看到"hello,world!",然后接着打印"i am envrionment 00000100"。接着调用exit()来退出当前这个程序。运行make grade,此时应该已经通过测试点 hello

代码实现
这个应该不难,这里涉及到的一点就是如何使用进程ID并以此来从envs中获得对应的Env结构。在env.h中有一个宏,ENVX(envid),根据上面的注释信息,可以知道这个宏就是获得对应Env的index。所以这个很简单了,代码如下:

void
libmain(int argc, char **argv)
{
    // set thisenv to point at our Env structure in envs[].
    // LAB 3: Your code here.
    thisenv = &envs[ENVX(sys_getenvid())];

    // save the name of the program so that panic() can use it
    if (argc > 0)
        binaryname = argv[0];

    // call user main routine
    umain(argc, argv);

    // exit gracefully
    exit();
}

Page faults and memory protection

内存保护是操作系统一个非常重要的功能,它确保一个程序中的bugs并不会影响到另外一个程序或者操作系统它本身。
操作系统通常通常需要依赖硬件来实现内存保护。当程序尝试去访问无效的地址或者它无权访问的地址,处理器会停在造成错误的地址接着跳入到内核当中去。如果这个fault是可以修复的,就修复他。如果不行,程序就无法继续执行了。
在内存保护中,系统调用有一个很有意思的问题。大多数系统调接口允许用户程序传一个指针给内核。这些指针指向了用于被写入或者读取的缓存。这样一来就会带来两个问题:

  1. 内核代码发生page fault的后果比发生用用户程序当中更严重。当内核在操作它自己的数据结构的时候,此时发生了page fault,那么内核就直接panic了,也因此整个系统都不能用了。但是,当内核使用用户程序提供的指针的时候,它需要记住一种方法来记住任何deferences所引起的page fault都是代表用户程序的
  2. 内核通常有更高的访问权限,相较于用户程序来说。用户程序传给内核的指针指向的地址可能是内核可以访问但是用户程序不能访问的。内核必须谨慎对待这样的情况,因为用户程序可能会窃取内核信息或者搞点坏事

现在你需要处理这两个问题通过审查所有从用户程序传送到内核的指针。当一个程序传送指针给内核的时候,内核需要检查这个指针是在用户的地址空间内的,并且所对应的page table也允许进行内存操作的(就是PTE_U标志位)。
Exercise 9:

修改kern/trap.c,如果page fault发生在内核就panic。提示:使用tf_cs来判断代码是在内核还是用户程序
阅读kern/pmap.c中的user_mem_assert然后实现user_mem_check。kern/syscall.c检查一下传给syscall的参数。重启内核,运行user/buggyhello。进程应该会被destroyed,但是不会panic。最后修改一下kern/kdebug.c中的debuginfo_eip,让它调用user_mem_check。

代码实现:
这里也分几个小问题来分别实现。首先第一个小问题是判断page fault是否发生在kernel。一开始我以为是在switch case里面直接写的。后来发现原来要我们在page_fault_handler()里面实现的。回想一下,早保护模式中如何区分是用户程序还是内核程序。答案就是判断cs寄存器的CPL是否为0。所以答案明了了,只需要做和运算,判断结果是否等0就好,如果是0说明是内核代码。

void
page_fault_handler(struct Trapframe *tf)
{
    uint32_t fault_va;

    // Read processor's CR2 register to find the faulting address
    fault_va = rcr2();

    // Handle kernel-mode page faults.

    // LAB 3: Your code here.
    if((tf->tf_cs & 3) == 0) {
        panic("page_fault occured in kernel mode, fault address %d\n", fault_va);
    }
    // We've already handled kernel-mode exceptions, so if we get here,
    // the page fault happened in user mode.

    // Destroy the environment that caused the fault.
    cprintf("[%08x] user fault va %08x ip %08x\n",
        curenv->env_id, fault_va, tf->tf_eip);
    print_trapframe(tf);
    env_destroy(curenv);
}

前面说过,我们要检查用户程序指针指向的地址是否是在用户的地址空间内。这就是pmap.c/user_mem_check()要做的事情,代码如下,结合注释应该可以理解:

int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
    // LAB 3: Your code here.
   char * end = NULL;
    char * start = NULL;
    start = ROUNDDOWN((char *)va, PGSIZE); 
    end = ROUNDUP((char *)(va + len), PGSIZE);
    pte_t *cur = NULL;

    for(; start < end; start += PGSIZE) {
        cur = pgdir_walk(env->env_pgdir, (void *)start, 0);
        if((int)start > ULIM || cur == NULL || ((uint32_t)(*cur) & perm) != perm) {
              if(start == ROUNDDOWN((char *)va, PGSIZE)) {
                    user_mem_check_addr = (uintptr_t)va;
              }
              else {
                      user_mem_check_addr = (uintptr_t)start;
              }
              return -E_FAULT;
        }
    }

    return 0;
}

最后一个问题,在kern/syscall.c中修改代码,对用户传过来的参数进行检查。因为我们刚刚已经实现了参数检查的函数。
在kern/syscall.c中的sys_cputs()加入下面代码:

static void
sys_cputs(const char *s, size_t len)
{
    // Check that the user has permission to read memory [s, s+len).
    // Destroy the environment if not.

    // LAB 3: Your code here.
    user_mem_assert(curenv,s,len,0);
    // Print the string supplied by the user.
![Screenshot from 2020-08-29 20-07-00.png](https://upload-images.jianshu.io/upload_images/10683218-38d7beef3759d3b2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    cprintf("%.*s", len, s);
}

下面的不会做,抄别人的代码,这一段有点难懂,我就放弃了,其他不相关代码已经省略了。

int
debuginfo_eip(uintptr_t addr, struct Eipdebuginfo *info) {

        // Make sure this memory is valid.
        // Return -1 if it is not.  Hint: Call user_mem_check.
        // LAB 3: Your code here.
        if (user_mem_check(curenv, usd, sizeof(*usd),PTE_U) <0 )
            return -1;

        // Make sure the STABS and string table memory is valid.
        // LAB 3: Your code here.
        if (user_mem_check(curenv, stabs, stab_end-stabs,PTE_U) <0 || user_mem_check(curenv, stabstr, stabstr_end-stabstr,PTE_U) <0)
            return -1;
}

最后的实验结果,运行make grade就可以看到了:


结果

你可能感兴趣的:(MIT6.828 lab3 part B)