MIT 6.828 学习笔记5 Lab3实验报告

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/scnu20142005027/article/details/51485616

Lab3 实验报告

Exercise 1

  • Modify mem_init() in kern/pmap.c to allocate and map the envs array.
// mem_int()
// 第一处
envs = (struct Env *) boot_alloc(NENV * sizeof(struct Env));
memset(pages, 0, NENV * sizeof(struct Env));

// 第二处
boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U | PTE_P);

这里仿照上一个实验可以比较轻松的写出来,注意,由于内核已用内存多出了 NENV 这一段,所以还需要修改 page_init 函数内的上限,参照 LAB 2 实验报告的代码,修改如下

// page_init()
size_t right_i = PGNUM(PADDR(envs + NENV));

Exercise 2

  • In the file env.c, finish coding the following functions.
// env_init()
env_free_list = envs;
struct Env *pre = envs;
for (int i = 1; i != NENV; ++i) {
    pre -> env_link = envs + i;
    pre = envs + i;
}

这里是初始化 envs 数组,然后插入到空闲链表中,由注释可知,需要确保分配 env 的时候是从 env[0] 开始分配的,也就是使用尾接法建链表,这和之前的 pages 不同

// env_setup_vm()
p->pp_ref++;
e->env_pgdir = page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);

初始化用户地址空间中内核部分的虚拟内存并设置一级页表,可以将之前的 kern_pgdir 复制过去

// region_alloc()
uintptr_t low = ROUNDDOWN((uintptr_t) va, PGSIZE);
uintptr_t high = ROUNDUP((uintptr_t) va + len, PGSIZE);
if (high > UTOP) {
    panic("allocation fails");
}
while (low < high) {
    struct PageInfo *pp = page_alloc(ALLOC_ZERO);
    if (pp == NULL) {
        panic("allocation falis");
    }
    pp->pp_ref++;
    int r = page_insert(e->env_pgdir, pp, (void *) low, PTE_P | PTE_U | PTE_W);
    if (r != 0) {
        panic("region_alloc: %e", r);       
    }
    low += PGSIZE;
}

env 分配物理内存,接着映射到虚拟内存,注意需要对齐以及考虑边界情况

// load_icode()
// 第一处
struct Elf *elf = (struct Elf *) binary;
struct Proghdr *ph = (struct Proghdr *) (binary + elf->e_phoff);
struct Proghdr *eph = ph + elf->e_phnum;
lcr3(PADDR(e->env_pgdir));
while (ph < eph) {
    if (ph->p_type == ELF_PROG_LOAD) {
        region_alloc(e, (void *) ph->p_va, ph->p_memsz);
        memcpy((void *) ph->p_va, binary + ph->p_offset, ph->p_filesz);
    }
    ph++;
}
lcr3(PADDR(kern_pgdir));
e->env_tf.tf_eip = elf->e_entry;

// 第二处
region_alloc(e, (void *) (USTACKTOP - PGSIZE), PGSIZE);

elf 二进制文件读入用户地址空间中,仿照 bootloadermain.c 的做法,需要了解 elf 文件的结构,这个之前学习过,需要注意的是,为了读入用户地址空间,需要切换到用户的页表,还有记得设置 eip 指向入口处

// env_create()
struct Env *e;
int r = env_alloc(&e, 0);
if (r < 0) {
    panic("env_alloc: %e", r);
}
load_icode(e, binary);
e->env_type = type;

利用之前的函数创建 env ,对于 parent_id 由于分配页面的时候,把页面都清 0 了,所以这里不显式设置也行

// env_run()
if (curenv && curenv->env_status == ENV_RUNNING) {
    curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
lcr3(PADDR(curenv->env_pgdir));
env_pop_tf(&(curenv->env_tf));

运行 env ,需要设置状态并切换页表,这里起到关键作用的函数是 env_pop_tf ,通过查看代码可以知道,它将 trapframe 里保存的信息 pop 到了对应的寄存器上,注意在 load_icode 函数的最后一句把 trapframe 中的 eip 设置到了二进制文件的入口,所以在执行完 env_pop_tf 后,下一条指令的地址将会是二进制文件的入口,因此达到了切换的目的

Exercise 3

  • Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer’s Manual.

这个需要读一读,不然后面可能会遇到一些困难,不过我也没认真读完,主要是英文看起来好累……过程中到网上查一些相关的中文资料,收获还是挺大的

Exercise 4

  • Edit trapentry.S and trap.c and implement the features described above.
// trapentry.S
// 第一处
TRAPHANDLER_NOEC(divide_error, T_DIVIDE)
TRAPHANDLER_NOEC(debug_exception, T_DEBUG)
TRAPHANDLER_NOEC(non_maskable_interrupt, T_NMI)
TRAPHANDLER_NOEC(break_point, T_BRKPT)  // 注意这个地方千万不能用 breakpoint 作为函数名
TRAPHANDLER_NOEC(overflow, T_OFLOW)
TRAPHANDLER_NOEC(bounds_check, T_BOUND)
TRAPHANDLER_NOEC(illegal_opcode, T_ILLOP)
TRAPHANDLER_NOEC(device_not_available, T_DEVICE)
TRAPHANDLER(double_fault, T_DBLFLT)
TRAPHANDLER(invalid_task_switch_segment, T_TSS)
TRAPHANDLER(segment_not_present, T_SEGNP)
TRAPHANDLER(stack_fault, T_STACK)
TRAPHANDLER(general_protection_fault, T_GPFLT)
TRAPHANDLER(page_fault, T_PGFLT)
TRAPHANDLER_NOEC(floating_point_error, T_FPERR)
TRAPHANDLER(alignment_check, T_ALIGN)
TRAPHANDLER_NOEC(machine_check, T_MCHK)
TRAPHANDLER_NOEC(SIMD_floating_point_exception, T_SIMDERR)

使用上面给的宏定义来设置处理 trap 的函数,这里的函数名可以自己取,但是要注意, T_BRKPT 的函数名不能是 breakpoint ,因为 inc/x86.h 中含有同名函数,系统在调用时会调用 inc/x86.h 里的那个函数,关于是否需要 errorcode 可以查看 LEC 8handouts ,里面含有相关信息

// trapentry.S
// 第二处
_alltraps:
  pushl %ds
  pushl %es
  pushal
  movl $GD_KD, %eax
  movw %ax, %ds
  movw %ax, %es
  pushl %esp
  call trap
  popal
  popl %es
  popl %ds
  addl $8, %esp 
  iret

根据之前所说,在引发异常时 CPU 会把 SS 寄存器到 EIP 寄存器压入栈中,如果需要 error code 的话也会压入,而在上面宏定义的函数中,trapno 也被压入了,所以这里只需要 push 余下的寄存器,注意需要根据 trapframe 的结构倒序压入, pushal 指令会按顺序将 eaxedi 压入栈中,call 之后的指令是当 call trap 失败时可以还原相关寄存器

// trap.c
// trap_init()
extern void divide_error();
extern void debug_exception();
extern void non_maskable_interrupt();
extern void break_point();
extern void overflow();
extern void bounds_check();
extern void illegal_opcode();
extern void device_not_available();
extern void double_fault();
extern void invalid_task_switch_segment();
extern void segment_not_present();
extern void stack_fault();
extern void general_protection_fault();
extern void page_fault();
extern void floating_point_error();
extern void alignment_check();
extern void machine_check();
extern void SIMD_floating_point_exception();
SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_error, 0);
SETGATE(idt[T_DEBUG], 0, GD_KT, debug_exception, 0);
SETGATE(idt[T_NMI], 0, GD_KT, non_maskable_interrupt, 0);
SETGATE(idt[T_BRKPT], 0, GD_KT, break_point, 3);
SETGATE(idt[T_OFLOW], 0, GD_KT, overflow, 0);
SETGATE(idt[T_BOUND], 0, GD_KT, bounds_check, 0);
SETGATE(idt[T_ILLOP], 0, GD_KT, illegal_opcode, 0);
SETGATE(idt[T_DEVICE], 0, GD_KT, device_not_available, 0);
SETGATE(idt[T_DBLFLT], 0, GD_KT, double_fault, 0);
SETGATE(idt[T_TSS], 0, GD_KT, invalid_task_switch_segment, 0);
SETGATE(idt[T_SEGNP], 0, GD_KT, segment_not_present, 0);
SETGATE(idt[T_STACK], 0, GD_KT, stack_fault, 0);
SETGATE(idt[T_GPFLT], 0, GD_KT, general_protection_fault, 0);
SETGATE(idt[T_PGFLT], 0, GD_KT, page_fault, 0);
SETGATE(idt[T_FPERR], 0, GD_KT, floating_point_error, 0);
SETGATE(idt[T_ALIGN], 0, GD_KT, alignment_check, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, machine_check, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, SIMD_floating_point_exception, 0);

设置 IDT ,需要先声明函数,需要注意,由于 break_point 普通用户也可以使用,所以 DPL = 3SETGATE 的定义在 inc/mmu.h 之中

Questions

  • What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

不同异常或中断的处理方式与结果不相同,例如是否可以恢复或从哪里恢复,条件也不一定相同,例如对权限等级与 errorcode 等参数的要求不同,所以需要拥有不同的处理函数

  • Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint’s code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint’s int $14 instruction to invoke the kernel’s page fault handler (which is interrupt vector 14)?

由于 trap 14IDT 内描述符的 DPL = 0 ,而此时 CPL = 3 即权限不足,所以执行这条指令会引发 trap 13

Exercise 5

  • Modify trap_dispatch() to dispatch page fault exceptions to page_fault_handler().
// trap.c
// trap_init()
switch (tf->tf_trapno) {
    case T_PGFLT:
        page_fault_handler(tf);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

根据 trapno 判断异常的类型,然后分配给相应函数

Exercise 6

  • Modify trap_dispatch() to make breakpoint exceptions invoke the kernel monitor.
// trap_init()
switch (tf->tf_trapno) {
    case T_PGFLT:
        page_fault_handler(tf);
        break;
    case T_BRKPT:
        monitor(tf);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

简单加上相应分支即可

Questions

  • The break point test case will either generate a break point exception or a general protection fault depending on how you initialized the break point entry in the IDT (i.e., your call to SETGATE from trap_init). Why? How do you need to set it up in order to get the breakpoint exception to work as specified above and what incorrect setup would cause it to trigger a general protection fault?

这个和上一个问题类似,如果设置 break pointDPL = 0 则会引发权限错误,由于这里设置的 DPL = 3 ,所以会引发断点

  • What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

这个机制有效地防止了一些程序恶意任意调用指令,引发一些危险的错误,所以我认为这个粒度的权限机制时十分必要的

Exercise 7

  • Add a handler in the kernel for interrupt vector T_SYSCALL.
// trap.c
// trap_dispatch()
struct PushRegs regs = tf->tf_regs;
switch (tf->tf_trapno) {
    case T_PGFLT:
        page_fault_handler(tf);
        break;
    case T_BRKPT:
        monitor(tf);
        break;
    case T_SYSCALL:
        tf->tf_regs.reg_eax = syscall(regs.reg_eax, regs.reg_edx, regs.reg_ecx, regs.reg_ebx, regs.reg_edi, regs.reg_esi);
        break;
    default:
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
}

还是和之前一样,分配相关的异常处理函数,这里需要将结果保存到 eax 寄存器中,记得在 IDT 中增加相应表项

// trapentry.S
TRAPHANDLER_NOEC(system_call, T_SYSCALL)

// trap.c
// trap_init()
extern void system_call();
SETGATE(idt[T_SYSCALL], 0, GD_KT, system_call, 3);

这里需要设置 syscallDPL = 3 ,接下来是 syscall.c 的部分

// syscall.c
// syscall()
switch (syscallno) {
    case SYS_cputs:
        sys_cputs((const char *) a1, a2);
        return 0;
    case SYS_cgetc:
        return sys_cgetc();
    case SYS_getenvid:
        return sys_getenvid();
    case SYS_env_destroy:
        return sys_env_destroy(a1);
    default:
        return -E_NO_SYS;
}

只需简单地根据 syscallno 调用不同的函数即可

Exercise 8

  • Add the required code to the user library, then boot your kernel.
// libmain.c
// libmain()
thisenv = &envs[ENVX(sys_getenvid())];

由于之前没有设置 thisenv 的值,所以运行到 hello 的第二句时会出现错误,这里根据 id 取出索引,然后找到相应 env

Exercise 9

  • Change kern/trap.c to panic if a page fault happens in kernel mode. Implement user_mem_check in that same file.Change kern/syscall.c to sanity check arguments to system calls.
// trap.c
// page_fault_handler()
if ((tf->tf_cs & 3) == 0) {
    panic("page fault in kern");
}

由于 cs 寄存器的低 2 位的值与 CPL 相等,所以可以根据 cs 寄存器判断是否在内核态

// pmap.c
// user_mem_check()
uintptr_t high = ROUNDUP((uintptr_t) va + len, PGSIZE);
for (uintptr_t low = (uintptr_t) va; low < high; low = ROUNDUP(low + 1, PGSIZE)) {
    pte_t *pte = pgdir_walk(env->env_pgdir, (void *) low, false);
    if (!pte || (~(*pte) & perm) || low >= ULIM) {
        user_mem_check_addr = low;
        return -E_FAULT;
    }
}
return 0;

通过页表找到相应的 pte ,然后判断是否具有权限,这里需要记录第一个出错的虚拟地址,所以一开始不能将 va 对齐,还有这种写法的 high 不能向下对齐,因为结果可能会比 low 还小

// syscall.c
// sys_cputs()
user_mem_assert(curenv, s, len, 0);

利用刚才的函数检查这一段地址

// kdebug.c
// debuginfo_eip()
// 第一处
if (user_mem_check(curenv, (void *) USTABDATA, sizeof(struct UserStabData), 0) < 0) {
    return -1;
}

// 第二处
if (user_mem_check(curenv, (void *) stabs, stab_end - stabs, 0) < 0 || user_mem_check(curenv, (void *) stabstr, stabstr_end - stabstr, 0) < 0) {
    return -1;
}

同理,检查相应的地址
关于最后一个问题,在调用 backtrace 后显示如下

K> backtrace
Stack backtrace:
 ebp efffff20 eip f0100c79 args 00000001 efffff38 f0198000 00000000 f0175840
     kern/monitor.c:217: monitor+276
 ebp efffff90 eip f01039a0 args f0198000 efffffbc 00000000 00000082 00000000
     kern/trap.c:192: trap+199
 ebp efffffb0 eip f0103aa8 args efffffbc 00000000 00000000 eebfdfd0 efffffdc
     kern/trapentry.S:80: <unknown>+0
 ebp eebfdfd0 eip 00800073 args 00000000 00000000 eebfdff0 00800049 00000000
     lib/libmain.c:26: libmain+58
 ebp eebfdff0 eip 00800031 args 00000000 00000000Incoming TRAP frame at 0xeffffea4
kernel panic at kern/trap.c:261: page fault in kern

这里引发页错误的原因是访问到了用户栈顶以上,可以看到, libmain 的两个参数都是 0 ,回想一下,在 lib/entry.S 中,系统在 USTACKTOP 执行了两次 pushl $0 ,所以当往上找第三个参数时就到达了上面的空内存,所以引发了页错误

Exercise 10

  • Boot your kernel, running user/evilhello. The environment should be destroyed, and the kernel should not panic.

这里只要完成了 Exercise 9 就可以通过这题,即 make grade 全部通过

猜你喜欢

转载自blog.csdn.net/scnu20142005027/article/details/51485616