MIT6.828LAB3 (3)

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的主要内容就是中断异常的处理。经过学习对这部分的底层有了一定的了解,但也有很多地方还不太明白,后续会继续学习。

  • 48
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值