MIT 6.828 学习笔记5 Lab3实验报告
Posted 丶Hiroshi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MIT 6.828 学习笔记5 Lab3实验报告相关的知识,希望对你有一定的参考价值。
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, NENV * sizeof(struct Env), 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);
e->env_pgdir[PDX(UENVS)] = kern_pgdir[PDX(UENVS)];
e->env_pgdir[PDX(UPAGES)] = kern_pgdir[PDX(UPAGES)];
e->env_pgdir[PDX(KSTACKTOP-KSTKSIZE)] = kern_pgdir[PDX(KSTACKTOP-KSTKSIZE)];
for (size_t i = PDX(KERNBASE); i != 1024; ++i) {
e->env_pgdir[i] = kern_pgdir[i];
}
初始化用户地址空间中内核部分的虚拟内存并设置一级页表,可以利用之前的 kern_pgdir
将相同的部分复制过去,这里应该也可以直接使用 menset
函数,注意需要增加页面的引用
// 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
二进制文件读入用户地址空间中,仿照 bootloader
中 main.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 != NULL) {
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 8
的 handouts
,里面含有相关信息
// 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
指令会按顺序将 eax
到 edi
压入栈中,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 = 3
, SETGATE
的定义在 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 14
在 IDT
内描述符的 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:
if (tf->tf_cs == 0) {
panic("page fault in kernel");
}
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:
if (tf->tf_cs == 0) {
panic("page fault in kernel");
}
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 point
的 DPL = 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:
if (tf->tf_cs == 0) {
panic("page fault in kernel");
}
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);
这里需要设置 syscall
的 DPL = 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()
pde_t *pgdir = env->env_pgdir;
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(pgdir, (void *) low, false);
if ((~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
全部通过
以上是关于MIT 6.828 学习笔记5 Lab3实验报告的主要内容,如果未能解决你的问题,请参考以下文章
《MIT 6.828 Lab 1 Exercise 12》实验报告
《MIT 6.828 Lab1: Booting a PC》实验报告