LAB3_Part B Page Faults, Breakpoints Exceptions, and System Calls
文章目录
前言
记录一下自己的学习过程
实验内容翻译:
https://gitee.com/cherrydance/mit6.828
该翻译仅供参考
练习5
请修改trap_dispatch()函数,将页面故障异常分派给page_fault_handler()函数。现在,您应该能够通过make grade命令成功完成faultread、faultreadkernel、faultwrite和faultwritekernel测试。如果其中任何一个不起作用,请找出原因并进行修复。请记住,您可以使用make run-x或make run-x-nox将JOS引导到特定的用户程序。例如,make run-hello-nox可以运行hello用户程序。
直接根据tf的中断码tf_trapno来判断当前是什么异常。如果是T_PGFLT则调用page_fault_handler函数处理。代码如下:
switch(tf->tf_trapno){
case T_PGFLT:
page_fault_handler(tf);
break;
}
运行结果如图:
练习6
请修改trap_dispatch()函数,使断点异常调用内核监视器。现在,您应该能够通过make grade命令成功完成断点测试。
这个练习就是完成断点异常,与练习5类似。调用内核监视器就是使用monitor()函数。
case T_BRKPT:
monitor(tf);
break;
结果如下:
问题3:断点测试用例将根据您在IDT中初始化断点条目的方式(即,从trap_init中调用SETGATE)生成break point exception 或者general protection fault 。为什么会这样?为了使断点异常按照上述规定工作,您需要如何设置它?而哪种错误的设置会导致触发通用保护错误?
因为在SETGATE中最后一个参数dpl是用来表示特权级的,如果设置为3,则会发生break point exception。设置为0则会发生general protection fault。因为当前是用户态,如果设置为0会导致在用户态试图调用内核态指令,这就会发生general protection fault。
问题4:这些机制的目的是什么,特别是考虑到用户/softint测试程序的功能?
是为了保证在用户环境中不能随意调用访问内核,保护内核的不受用户程序的影响。
练习7
在内核中为中断向量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测试。
在有了前面练习的经历之后,这里照着写就行。
首先在tarpentry.S中加入TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)
。然后在trap.c中加入函数声明void t_syscall();
,之后在trap_init函数中加入SETGATE(idt[T_SYSCALL], 0, GD_KT, t_syscall, 3);
,因为是用户进行系统调用,自然该中断的优先级得是3。然后就是trap_dispatch函数。
练习告诉我们系统调用函数用到的参数该按下列方式给出:
系统调用号将存储在%eax寄存器中,参数(最多五个)将分别存储在%edx、%ecx、%ebx、%edi和%esi寄存器中。内核将返回值存储在%eax寄存器中。
代码如下:
case T_SYSCALL:
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);
break;
接下来查看lib/syscall.c中的syscall()函数:
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");
if(check && ret > 0)
panic("syscall %d returned %d (> 0)", num, ret);
return ret;
大概是执行指令int T_SYSCALL,返回值存储在eax寄存器并分配给ret用来判断。然后把num到a5存储在eax,edx,ecx,ebx,edi,esi寄存器,cc和memory表示会改变命令码和内存位置。
然后查看inc/syscall.h里面列出系统调用的集中分类:
/* system call numbers */
enum {
SYS_cputs = 0,
SYS_cgetc,
SYS_getenvid,
SYS_env_destroy,
NSYSCALLS
};
因此我们实现kern/syscall.c的syscall函数时,要根据上面两种代码来实现
int ret = 0;
switch (syscallno) {
case SYS_cgetc:
ret = sys_cgetc();
break;
case SYS_cputs:
sys_cputs((const char *)a1, (size_t)a2);
break;
case SYS_env_destroy:
ret = sys_env_destroy((envid_t) a1);
break;
case SYS_getenvid:
ret = sys_getenvid();
break;
default:
return -E_INVAL;
}
return ret;
关于其中sys_cputs和sys_env_destroy的参数可以查看lib/syscall.c中的代码:
void
sys_cputs(const char *s, size_t len)
{
syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}
练习7运行结果如下:
练习8
在用户库中添加所需的代码,然后启动您的内核。您应该看到user/hello打印出"hello, world",然后打印出"i am environment 00001000"。然后,user/hello尝试通过调用sys_env_destroy()函数来"退出"(参见lib/libmain.c和lib/exit.c)。由于内核当前只支持一个用户环境,它应报告已销毁唯一的环境,然后进入内核监视器。您应该能够通过make grade命令成功完成hello测试。
在lib/libmain.c函数中我们看到将thisenv设置为我们在envs数组中的Env。
// set thisenv to point at our Env structure in envs[].
这提示我们使用envs数组去获得当前用户环境,而在lib/exit.c中只有一个函数exit()。它调用了sys_env_destroy函数。
void
exit(void)
{
sys_env_destroy(0);
}
那也就是提示我们可以直接使用sys_getenvid()函数来获取当前用户环境在envs数组的位置。获取id之后我们还需要知道哪部分才是真正的数组索引,在inc/env.h文件中我们获得由ENVX(eid)可以获取最后的索引。
代码如下:
thisenv = 0;
thisenv = envs +ENVX(sys_getenvid());
运行结果如下:
练习9
请修改kern/trap.c文件,使得如果内核模式下发生页面故障时会引发紧急停机。
提示:要确定故障是在用户模式还是内核模式下发生的,请检查tf_cs的低位。
请阅读kern/pmap.c中的user_mem_assert函数,并在同一文件中实现user_mem_check函数。
接下来,请修改kern/syscall.c文件,对系统调用的参数进行合法性检查。
启动内核并运行user/buggyhello程序。环境应该会被销毁,而内核不应该发生紧急停机。您应该会看到以下输出:
最后,请修改kern/kdebug.c中的debuginfo_eip函数,在usd、stabs和stabstr上调用user_mem_check函数。如果您现在运行user/breakpoint程序,您应该能够从内核监视器中运行backtrace命令,并在内核在页面故障之前进入lib/libmain.c时看到回溯信息。是什么导致了这个页面故障?您不需要修复它,但应该理解为什么会发生这种情况。
首先要查看trap.c文件,使得如果内核模式下发生页面故障时会引发紧急停机。那我们怎么判断当前是在内核还是在用户呢?
要判断当前是在内核态还是用户态,可以通过检查处理器的当前特权级别(CPL)或当前代码段的特权级别来确定。CPL的值就是cs寄存器的低两位。当CPL为0时就是在内核,为3就是在用户。如何时在内核就使用panic停机。因此此处的代码为
if((tf->tf_cs & 0x11) == 0x00){
panic("page_fault in kernel mode, fault address is %d\n", fault_va);
}
然后查看kern/pmap.c文件下的user_mem_assert函数,该函数的作用是检查环境变量 ‘env’ 是否被允许以权限 ‘perm | PTE_U | PTE_P’ 访问内存范围 [va, va+len)。而需要我们实现的函数user_mem_check的作用是当前的用户态程序对内存[va,va+len)是否由perm|PTE_U的权限。
注释说明,va和len可能不是页对齐,此外对内存的要求是在ULIM一下且有权限。如果出现错误,将 ‘user_mem_check_addr’ 变量设置为第一个错误的虚拟地址。代码如下:
uint32_t start = (uint32_t)ROUNDDOWN(va, PGSIZE);
uint32_t end = (uint32_t)ROUNDUP(va + len, PGSIZE);//对齐
pte_t * pg_entry = NULL;
for(; start < end; start += PGSIZE){
pg_entry = pgdir_walk(env->env_pgdir, (const void *)start, 0);//获取页表项的地址
//三重判断,页表项存在,位置在ULIM以下,权限符合
if(!pg_entry || start > ULIM || (*(pg_entry) & perm) != perm){
if(start < (uint32_t)va){//错误地址存到user_mem_check_addr中
user_mem_check_addr = (uintptr_t)va;
}else{
user_mem_check_addr = (uintptr_t)start;
}
return -E_FAULT;
}
}
return 0;
接下来在kern/syscall.c中对参数的合法性进行检查,查看文件得到需要在sys_cputs中补充代码,如下,检查字符串s的内存是否是合法的内存。
user_mem_assert(curenv, (const void *)s, len, 0);
然后我们运行make run-buggyhello,得到以下结果说明代码正确:
最后,我们修改kern/kdebug.c中的debuginfo_eip函数。
if(user_mem_check(curenv, (const void *)usd, sizeof(struct UserStabData), PTE_U) < 0){
return -1;
}
if(user_mem_check(curenv, (const void *)stabs, sizeof(struct Stab), PTE_U) < 0){
return -1;
}
if(user_mem_check(curenv, (const void *)stabstr, stabstr_end - stabstr, PTE_U) < 0){
return -1;
}
练习10
启动内核并运行user/evilhello程序。环境应该会被销毁,而内核不应该发生紧急停机。您应该会看到以下输出:
如图:在前面的结果正确的基础上,这个练习不需要做就会自动完成。
最终结果:
未解决的关键点
在练习9的最后说运行make run-breakpoint然后使用backtrace。然而在我完成lab1忘记的最后一步backtrace命令后出现了重大错误。我写的代码如图:
在添加这一行之后运行make qemu出现如图的错误
错误代码如下:
kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
memset(kern_pgdir, 0, PGSIZE);
kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
经过检查发现是因为memset的时候将kern_pgdir的值页改成了0。即&kern_pgdir的值处于memset(kern_pgdir, 0, PGSIZE);这一段被清零的空间,具体原因未查明,以后要是知道了原因会开一篇博客写。如果大家知道原因也欢迎私信或者评论帮我解答!!!!!
总结
完成了lab3,这个lab的主要内容就是中断异常的处理。经过学习对这部分的底层有了一定的了解,但也有很多地方还不太明白,后续会继续学习。