MIT6.828学习之Lab3_Part B: Page Faults, Breakpoints Exceptions, and System Calls

实验过程

现在由于你的内核有了基本的异常处理能力,你会继续改进它,以提供依赖于异常处理的重要操作系统原语。

Handling Page Faults

The page fault exception, interrupt vector 14(T_PGFLT), 是特别重要的一个,我们将在这个和下一个lab中大量练习它。当处理器收到一个page fault,它将导致错误的linear(或virtual) address存在一个特别的处理器控制寄存器CR2中。在trap.c我们已提供了一个处理页面错误异常的特别函数page_fault_handler()的开始部分。

Exercise 5.修改trap_dispatch()dispatch(报道?派遣?)一个page fault exceptions到page_fault_handler()。
你现在应该能在faultread, faultreadkernel, faultwrite, and faultwritekernel测试中make grade成功,如果其中任何一个没成功,找到原因并解决它。
记住你可以用'make run-x''make run-x-nox'启动JOS到一个特别的用户程序。如make run-hello-nox 运行 hello用户程序.

你会在下面更近一步改进页面错误处理程序的,当你完成system calls时。

//trap_dispatch()
if(tf->tf_trapno == T_PGFLT)
	page_fault_handler(tf);

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)
	/*{//我居然想着内核页面错误了就插一个页面继续运行。傻了,kernel mode下根本不能页面错误,会terminate
		struct PageInfo *pp=page_alloc(0);
		if(!pp)
			panic("out of memory");
		page_insert(kern_pgdir,pp,(void *)fault_va,PTE_U|PTE_W);
	}else*/
		panic("a page fault happened in kernel mode!\n");

	
	// We've already handled kernel-mode exceptions, so if we get here,
	// the page fault happened in user mode.
	// 为什么??因为这是用户程序hello调用lib/cprintf()里的系统调用lib/sys_cputs陷入的
	// 如果页面错误发生在kernel mode,那就会产生triple fault,在上面panic了
	// 因为内核绝不能发生页面错误

	//用户程序发生页面错误就得直接destroy掉吗?
	// 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);
}

make grade 成功了

The Breakpoint Exception

The breakpoint(断点) exception, interrupt vector 3 (T_BRKPT),常被用于允许debuggers(调试器)插入一个断点在程序代码中,通过用特别1-byte int3软件中断指令临时取代相关程序指令。在JOS中,我们会轻微的滥用断点异常,通过将他变成一个能被任何user environment使用去调用JOS kernel monitor(监视器)的原始的pseudo-system call(伪系统调用)。这个用法实际上很适合如果我们把JOS kernel monitor想成一个基本的debugger。例如,在lib/panic.c的panic()中user-mode的实现,执行了一个int3在展示它的panic信息后。

Exercise 6.修改trap_dispatch()去让断点异常调用the kernel monitor。你应该现在能够在'breakpoint test'中make grade成功

//trap_dispatch()
if(tf->tf_trapno == T_BRKPT)
	monitor(tf);
Questions
  1. 断点测试案例也会产生一个断点异常或者一个general protection fault(通用保护错误),这取决于你是如何初始化IDT中断点入口地址的(i.e. 你在trap_init中调用SETGATE),why?你需要怎么去设置它使断点异常能像上面指定好的那样工作?什么不正确的设置会导致它去触发一个general protection fault?
    答:SETGATE(idt[T_BRKPT], 1, GD_KT, brkpt_handler, 3);中如果最后一个参数dpl设为3就会产生一个breakpoint exception,如果设为0就会产生一个general protection fault。这也是由于特权级别影响的。breakpoint test程序的特权级别是3,如果断点异常处理程序特权设为3那就可以是断点异常,如果设为0就产生保护错误。

  2. 你认为这些机制的重点(point)怎么样?尤其是根据user/softint test程序做了什么来判断。
    里面是这条代码asm volatile("int $14");本来想中断调用页面错误处理,结果因为特权级别不够而产生一个保护异常,所以重点应该是要分清特权级别吧。要区分$14$0x30

System calls

User processes(用户进程)通过调用system calls要求内核为它们做事。当用户进程调用一个系统调用,处理器会进入kernel mode,处理器和内核合作去报错用户进程的状态,内核执行正确代码去执行系统调用,然后返回用户进程。用户进程如何获得内核的注意以及它怎么指定它想执行哪个系统调用的准确细节因系统而异。

在JOS内核中,我们会使用会导致处理器中断的int instruction,尤其是,我们会使用int $0x30作为系统调用中断。我们以及定义了constant(常数) T_SYSCALL是48(0x30)。你必须设置interrupt descriptor去允许用户进程引起中断。注意中断0x30不能由硬件产生,因此用户代码去产生它不会导致歧义。

Application(应用程序)会在寄存器中传递系统调用号和系统调用参数。因此,内核不需要到用户环境栈或者指令流中去挖掘它们。The system call number(不是trapno,也不是中断向量,是系统调用函数的index)会在%eax中,参数会分别在%edx、%ecx、%ebx、%edi和%esi中。然后内核会将返回值存入%eax。调用系统调用的汇编代码已经写给你了,在lib/syscall.c的syscall()函数中。你应该读懂它,确保自己理解发生了什么。

Exercise 7. 在内核中为中断向量T_SYSCALL添加一个handler。你必须编辑 kern/trapentry.S和kern/trap.c的trap_init().
你也需要改变trap_dispatch()去处理系统调用中断,通过正确的参数调用'kern/syscall.c'中的syscall(),然后在'%eax'中安排返回值返回给用户进程。
最后你得完成syscall(),确保当系统调用号无效的时候它返回'-E_INVAL'.
你应该读懂lib/syscall.c,尤其是里面的'the inline assembly routine'(内联汇编代码),为了正式你确实理解系统调用接口了。
处理所有inc/syscall.h列出的系统调用,通过为每一个call调用合适的内核函数

在你的内核下运行user/hello程序(通过'make run-hello')。它应该在console打印"hello world"然后在user mode下导致一个页面错误。
如果没有发生,可能意味着你的'system call handler'不对。你应该也能是testbss测试make grade成功

要明白内联汇编代码

// Generic system call: pass system call number in AX,
	// up to five parameters in DX, CX, BX, DI, SI.
	// Interrupt kernel with T_SYSCALL.
	//
	// The "volatile" tells the assembler not to optimize(优化)
	// this instruction away just because we don't use the
	// return value.
	//
	// The last clause(子句) tells the assembler that this can
	// potentially change the condition codes and arbitrary
	// memory locations(任意内存位置).
	asm volatile("int %1\n"
	     : "=a" (ret)
	     : "i" (T_SYSCALL),
	       "a" (num),
	       "d" (a1),
	       "c" (a2),
	       "b" (a3),
	       "D" (a4),
	       "S" (a5)
	     : "cc", "memory");
	     
//看懂内联汇编只要看懂下面这个用法已经再下面那个图就行
 __asm__ __volatile__("InSTructiON List" : Output : Input : Clobber/Modify);

在这里插入图片描述
谢谢 找不到工作 的图
实验代码如下:

//trap_dispatch()
else if(tf->tf_trapno == T_SYSCALL){
	int32_t result = 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 = result;
}

//kern/syscall.c/syscall() 注意这里是kern文件下。因为在内核态下调用的,不需要再包含系统调用了
switch (syscallno) {
	case (SYS_cputs):
		//实在不懂这里的参数。EDI与ESI才是指向要处理内存的吧
		//好吧我傻了,在lib/syscall.c的sys_cputs()那里已经告诉我了。。。
		//lib/syscputs()里 syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);第二个参数是check,忽略掉
		sys_cputs((const char *)a1, a2);
		return 0;
	case (SYS_cgetc): 
		return sys_cgetc();
	case (SYS_getenvid):
		return sys_getenvid();
	case (SYS_env_destroy): 
		//这个在lib/syscall.c/sys_env_destroy()也有提示...
		return sys_env_destroy(a1);
	default:
		return -E_INVAL;
}

testbss:OK

User-mode startup

A user program开始运行在lib/entry.S的顶部。在一些设置之后,这里的代码会调用lib/libmain.c/libmain()。你应该修改libmain()去初始化global pointer(全局指针)thsienv,使其指向在envs[]数组里的这个环境(指这个user program)的struct Env。注意lib/entry.S已经定义了指向你在Part A设置的UENVS映射的envs。提示:看下inc/env.h然后使用sys_getenvid

libmain()然后会calls umain,在这个hello程序的例子中,umain是在user/hello.c。注意在打印"hello, world"之后,它会试着去access(访问) thisenv->env_id。这是为什么它之前错误的原因。由于你已经正确初始化thisenv,它不应该会再错。如果它还是错了,你可能还没有映射UENVS区域的user-readable(用户可读)部分。(回到Part A的pmap.c中,这是我们实际上第一次使用UENVS area)

Exercise 8.添加所要求的代码到user library(用户库),然后启动你的内核。你应该看到user/hello打印"hello, world",然后打印 “i am environment 00001000”。user/hello然后尝试通过调用sys_env_destroy()"exit"(见lib/libmain.c和lib/exit.c)。因为内核目前只支持一个user environment,它应该报告它已经destroyed这唯一的environment,然后转到kernel monitor。你应该能够让hello test成功make grade

// LAB 3: Your code here.
	
	// 这个地方一定要看懂envid_t的结构,用ENVX()取后10bit做索引值就行
	// 也要注意这里有kern/syscall.c文件与lib/syscall.c
	// kern/syscall.c是在内核态下调用的,本身在内核态,所以不包括系统调用
	// lib/syscall.c是在用户态下调用的,想让内核帮忙,就得系统调用
	// 根据头文件判断这里调用的是lib/syscall.c,根据这里是用户程序,也可以判断是lib/syscall.c
	
	thisenv = &envs[ENVX(sys_getenvid())];

hello: OK

Page faults and memory protection

Memory protection是操作系统一个至关重要的特性,确保一个程序的bugs不会破坏其他程序或者破坏操作系统自身。

操作系统经常依赖于hardware support(硬件支持)去实现内存保护。OS会通知硬件哪些虚拟地址有效,哪些无效。当一个程序试着去access(访问)一个无效地址或者一个它没有权限访问的地址的时候,处理器会停止这个程序再这条导致fault的指令上,然后带着这尝试操作的信息陷入内核。如果这个fault是可修复的,内核会fix它然后让程序继续运行。如果这个fault是不可修复的,那这个程序就不能再继续了,因为它永远通不过这条导致错误的指令。

作为一个fixable fault的例子,考虑一个自动扩展栈(automatically extended stack)。在很多系统中,内核初始分配一个single stack page(单一的栈页),然后如果一个程序错误地访问栈下的页面,内核将自动分配这些页面并让程序继续运行。通过这样做,内核只分配程序所需要的栈内存,但是程序可以工作在以为自己有任意大的栈的错觉中。

系统调用为内存保护present(提出)了一个有趣的问题。大多数系统调用接口让用户程序向内核传递指针。这些指针指向将被读或者写的user buffers(用户缓冲区)。然后当内核执行这些系统调用时,它dereference(间接引用?解引用?)这些指针。这样有两个问题:

  1. 一个内核的page fault可能比用户程序的page fault严重的多。如果内核操作它自己的数据结构时发生页面错误,那就是一个内核bug,那么错误handler会panic内核(甚至整个系统)。但是当内核在间接引用由用户程序给定的指针的时候,它需要一种方法去记住,这些间接引用导致的任何页面错误实际上都代表用户程序
  2. 内核典型比用户程序拥有更多内存权限。用户程序可能给system call传递一个指向内核可以读写但用户不能读写的内存的指针。内核必须小心,不要被骗去间接引用一个这样的指针,因为这可能会泄露私有信息或破坏内核的完整性。

由于这些理由,当处理由用户程序提供的指针时内核必须小心。

现在你将使用一种机制来解决这两个问题,该机制仔细检查所有从用户空间传到内核的指针。当一个程序向内核传一个指针的时候,内核会检查这个地址是不是在地址空间的用户部分,并且页表是否允许这个内存操作。

因此,内核永远不会由于间接引用一个用户提供的指针出现页面错误。如果内核自己出现页面错误了,它会panic然后terminate(终止)。

Exercise 9. 改变kern/trap.c,当a page fault发生在kernel mode时panic
提示:通过检查tf_cs的低位去决定a fault发生在user mode还是kernel mode
Read user_mem_assert in kern/pmap.c and implement user_mem_check in that same file.
启动你的内核,运行user/buggyhello.这environment应该被destroyed,内核不会panic。你应该见到这个:
[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!
最后,change debuginfo_eip in kern/kdebug.c to call user_mem_check on usd, stabs, and stabstr。如果你现在运行user/breakpoint,你应该能够从内核monitor运行backtrace,并且在内核用a page fault来panic之前看见回溯遍历到了lib/libmain.c。是什么导致了这个page fault?你不需要fix它,但你得理解为什么它会发生。

//pmap.c
int user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
	// LAB 3: Your code here.
	//自己写出的代码也差不多,不知道为什么最多只能拿75,只好照着大神的改了下
	pte_t *pte;
	uint32_t start = (uint32_t)ROUNDDOWN(va, PGSIZE);//因为va跟va+len都没对齐的
	uint32_t end = (uint32_t)ROUNDUP(va+len, PGSIZE);
	for(uint32_t i=start; i<end; i+=PGSIZE){
		pte = pgdir_walk(env->env_pgdir, (void *)i, 0);
		//确实这里考虑不了这么周全
		if ((i >= ULIM) || !pte || !(*pte & PTE_P) || ((*pte & perm) != perm)){
			user_mem_check_addr = (i < (uintptr_t)va? (uintptr_t)va : i);
			return -E_FAULT;
		}
	}
	return 0;

}

//
// Checks that environment 'env' is allowed to access the range
// of memory [va, va+len) with permissions 'perm | PTE_U | PTE_P'.
// If it can, then the function simply returns.
// If it cannot, 'env' is destroyed and, if env is the current
// environment, this function will not return.
//
void user_mem_assert(struct Env *env, const void *va, size_t len, int perm)
{
	if (user_mem_check(env, va, len, perm | PTE_U) < 0) {
		cprintf("[%08x] user_mem_check assertion failure for "
			"va %08x\n", env->env_id, user_mem_check_addr);
		env_destroy(env);	// may not return
	}
}

//kern/syscall.c
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, PTE_U);
	// Print the string supplied by the user.
	cprintf("%.*s", len, s);
}

忘了修改debuginfo_eip了,直接运行breakpoint然后执行backtrace得到如下结果,改不改居然结果是一样的:
在这里插入图片描述
下面是我对kern/debuginfo_eip()的更改:

//kern/debuginfo_eip()
//Fill in the 'info' structure with information about the specified instruction address, 'addr'.

const struct UserStabData *usd = (const struct UserStabData *) USTABDATA;

// 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,(void *)usd, sizeof(struct UserStabData), 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,(void *)stabs, sizeof(struct Stab), PTE_U)<0)
	return -1;
if(user_mem_check(curenv,(void *)stabstr, strlen(stabstr), PTE_U)<0)
	return -1;

我也不太确定这个debuginfo_eip函数是干嘛的,按注释来说是把指定地址出的信息填入到info结构体中。然后我们可以发现,在kern/monitor.c/mon_backtrace()里居然调用过这个函数。在输出第三个参数的时候给出了缺页中断,个人感觉是用户栈?到顶了,因为libmain再回溯就只是内核态调用env_pop_tf了,由于backtrace本来就在内核态,发生缺页中断就panic咯

注意,您刚刚实现的相同mechanism(机制)也适用于malicious(恶意)用户应用程序(例如user/evilhello)。

Exercise 10. 启动你的内核,运行user/evihello.这个environment应该会被destroyed,内核不会panic。
你会见到:
[00000000] new env 00001000

[00001000] user_mem_check assertion failure for va f010000c
[00001000] free env 00001000

不懂为什么代码没做任何改变,但是连按几次make grade都可以输出不同的结果?之前这个ok的下次就不ok,之前不ok的后面又ok了。。。真是无数次make grade才出来个80分。。。
在这里插入图片描述

自问自答

1.系统调用结束后怎么回到用户态?
应该是在trap()的trap_dispatch()后到了env_run(),在env_run()的env_pop_tf()里回到用户态。

改一个地方的代码,可以引起几个test结果的变化。。。。

要注意很多函数在kern文件跟lib文件中都有,而且是非常不同的(比如lib中的syscall是用户态下调用系统调用,而kern中的syscall只是简单的输出之类的,不包括系统调用,因为本身就是内核态下),要注意到底调用的是哪个文件里的,一般可以通过原理去判断,实在不行就通过头文件判断吧

参考

这次大部分代码都是自己写的,开森,但是有些地方总是过不了,就参考着改了下
部分代码参考这位仁兄

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值