MIT6.828 32位操作系统笔记(12)----进程管理与中断 LAB3下

本文围绕MIT6.828 32位操作系统实验展开,介绍了缺页中断、断点异常、系统调用等内容。阐述了处理这些中断和异常的方法,如完善相关函数、注册中断处理函数等。还提及用户模式开启及内存保护,通过实验操作完成LAB 3,帮助理解操作系统原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MIT EDU 6.828 实验源代码

分类 MIT6.828 32位操作系统实验笔记

实验完善代码 LAB2-4下载链接 提取码:79t8

1、缺页中断

  缺页中断是一个非常重要的中断,因为我们在后续的实验中,非常依赖于能够处理缺页中断的能力。当缺页中断发生时,系统会把引起中断的线性地址放到控制寄存器CR2 中。 在trap.c 文件中,已经提供了一个能够处理这种缺页异常的函数page_fault_handler()。

  • Exercise 5
    在这里插入图片描述
      完善trap_dispatch(),使系统能够将缺页异常引导到page_fault_handler()上执行。在修改完成之后,在lab 目录下运行 make grade ,此时应该能成功运行 faultread, faultreadkernel, faultwrite,这些函数,如果出错了仔细检查下前面的步骤有没有错误。
static void
trap_dispatch(struct Trapframe *tf)
{
	// Handle processor exceptions.
	// LAB 3: Your code here.
	switch(tf->tf_trapno)
	{
		case (T_PGFLT):
			page_fault_handler(tf);
			break;		
		default:
	// Unexpected trap: The user process or the kernel has a bug.
			print_trapframe(tf);
			if (tf->tf_cs == GD_KT)  //GD_KT表示内核TEXT
				panic("unhandled trap in kernel");
			else {
				env_destroy(curenv);
				return;
			}
	}
}

在这里插入图片描述

2、断点异常

  断点异常,异常号为3,这个异常可以让调试器能够给程序加上断点。加断点的基本原理就是把要加断点的语句用一个INT 3 指令替换,执行到INT3 的时候,会触发软中断。在JOS 中,我们将通过这个异常转换成为一个伪 系统调用,这样的话任何用户环境都可以使用这个伪系统调用来触发JOS kernel monitor,例如在lib/panic 中函数panic() 就是在用户模式下进行伪系统调用来输出信息的。

  • Exercise 6

在这里插入图片描述

static void
trap_dispatch(struct Trapframe *tf)
{
	// Handle processor exceptions.
	// LAB 3: Your code here.
	switch(tf->tf_trapno)
	{
		case (T_PGFLT):
			page_fault_handler(tf);
			break;
		case (T_BRKPT):
			monitor(tf);
			break;
		default:
	// Unexpected trap: The user process or the kernel has a bug.
			print_trapframe(tf);
			if (tf->tf_cs == GD_KT)  //GD_KT表示内核TEXT
				panic("unhandled trap in kernel");
			else {
				env_destroy(curenv);
				return;
			}
	}
}
  • Questions在这里插入图片描述
    问题3:在上面的break point exception测试中,如果你在设置IDT 时,对break point exception 采用不同的方式进行设置,可能互产生触发不同的异常,有可能是break point exception,也有可能是general protection exception。这是为什么?

通过实验发现出现这个现象的问题就是在设置IDT表中的break point exception 的表项时,如果将表项中的DPL 字段设置为3,则会触发break point exception,如果设置为0 ,则会触发 general protection exception。首先需要弄清楚这里DPL 字段的含义:段描述符优先级,如果我们想要执行的程序能够跳转到这个描述符所指向的程序那里运行的话,有个要求就是当前程序运行的优先级(用CPL 表示)和RPL 的最大值需要小于等于DPL(也就是当前程序运行的优先级需要低于异常处理代码的级别),否则就会出现优先级低的代码试图去访问优先级高的代码的情况,就会触发 general protection exception。那么我们的测试程序会首先运行在用户态,它的CPL = 3 , 当异常发生时,它希望执行int3 指令,这是一个系统级别的指令,用户态命令的CPL 一定大于int3 的DPL ,所以就会触发 general protection exception,但是如果将idt 这个表项的DPL设置为3时,就不会出现这样的现象了,这时如果再出现异常,肯定是编写的break point exception程序的问题。

3、系统调用

  用户程序会要求内核帮助它完成系统调用。当用户程序触发系统调用,系统进入内核态。处理器和操作系统将保存用户程序的当前上下文状态,然后由内核执行正确的代码完成系统调用,然后回到用户程序继续执行。而用户程序到底是如何进行使得系统进入内核态,并准确调用想要调用的系统程序的。

  在JOS 内核,我们用int 指令来触发一个处理器的中断,特别地,我们用int $0x30来表示系统调用中断,注意,中断0x30并不是通过硬件产生的,也就是我们在异常分类这里所述的陷阱。

  • Exercise 7
    在这里插入图片描述

(1)在trapentry.S添加下面的代码,声明SYSCALL 的中断处理函数。

//////////////////trapentry.S/////////////////////////
TRAPHANDLER_NOEC(Trap_syscall, T_SYSCALL);//40

(2)在trap_init() 函数注册T_SYSCALL的中断处理函数。

///////////// kern/trap.c trap_init()////////////////
SETGATE(idt[T_SYSCALL], 0, GD_KT, Trap_syscall, 3  );

(3)在trap_dispatch() 分发 T_SYSCALL 到相应的handler。

应用程序会把系统调用号以及系统调用的参数存放到寄存器中。通过这种方法,内核就不需要去查询用户程序的堆栈了。系统调用号存放在%eax中,参数则存放在 %edx,%ecx,%ebx,%edi 和 %esi 中。内核会把返回的值存在 %eax中。在 lib/sysycall.c 中已经写好了触发一个系统调用的代码。

/////////////// kern/trap.c trap_dispatch()/////////
		case (T_SYSCALL):
			
			ret = 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 = ret;			
			break;

(4)在 kern/syscall.c 中完善中断向量T_SYSCALL 的中断处理函数syscall()。

int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
	// Call the function corresponding to the 'syscallno' parameter.
	// Return any appropriate return value.
	// LAB 3: Your code here.

	//
	int32_t ret = 0;

	switch (syscallno) {
		case(SYS_cputs):
			sys_cputs((const char *)a1, (size_t)a2);
			break;
		case(SYS_cgetc):
			ret = sys_cgetc();
			break;
		case(SYS_getenvid):
			ret = sys_getenvid();
			break;
		case(SYS_env_destroy):
			ret = sys_env_destroy((envid_t)a1);
			break;		
		default:
			return -E_INVAL;
	}
	return ret;
	//panic("syscall not implemented");
}

(5)make grade
在这里插入图片描述

4、用户模式的开启

  在操作系统中最终是要运行用户进程的,在JOS 中,用户进程是lib/entry.S 开始运行的,然后转到函数libmain(),这部分唯一需要注意的事情是,在lib/libmain.c 中对于用户进程结构体env 的设置,在entry.S 中env 指向 UNEV ,现在需要更正为指向当前进程控制块的地址,对于全局数组envs[] , 我们只要根据当前进程的进程编号在envs[] 里面进行搜索就可以找到当前的进程控制块。在进行搜索时,要用宏ENVX 对当前进程号进行搜索,该宏的作用是保留进程号的低10位,以防在不同的进程控制块分配回收后进程编号超过了NENV,造成数组的越界访问

  注意:内存保护可以确保用户进程中的bugs 不能破坏其他进程或者内核。当前用户进程试图访问一个无效的或者没有权限的地址时,处理器就会中断进程陷入内核,如果错误可以修复,内核就会修复它并让用户进程继续执行;如果无法修复,那么用户进程就不能继续执行。而在系统调用中导致了内存保护。许多系统接口运行把指针传递给kernel ,这些指针指向用户buffer ,这就要防止一些恶意用户程序破坏内核,需要内核对用户传递的指针进行权限检查。对每个指针进行检查,由 user_mem_check() 和 user_mem_assert() 实现。检查用户进程的访问权限,并检查是否越界。

  • Exercise 8
    在这里插入图片描述
    (1) 完善 lib/libmain.c() 函数,初始化一个指针(thisenv )指向位于 envs[] 数组中的表示 该进程的struct Env 。(提示:用宏ENVX 和 系统调用 sys_getenvid())
//////////////// lib/libmain.c///////////////
void
libmain(int argc, char **argv)
{
	// set thisenv to point at our Env structure in envs[].
	// LAB 3: Your code here.  sys_getenvid
	thisenv = envs + ENVX(sys_getenvid());

	// save the name of the program so that panic() can use it
	if (argc > 0)
		binaryname = argv[0];

	// call user main routine
	umain(argc, argv);

	// exit gracefully
	exit();
}

(2)对比这个实验前后运行结果
在这里插入图片描述
现在thisenv 被重新正确设置,不会报page fault 错误。
在这里插入图片描述
测试下分数:make grade , 可以看到这个hello 已经正常中断了。
在这里插入图片描述

5、阶段小结

这里有几个很重要得概念需要整理一下,按照中断异常处理的思路来。

  • 系统要能处理中断和异常就需要初始化trap,设置对应的中断/异常码 的回调,当发生中断/异常的时候,根据已经注册的信息转移到相应的中断/异常处理函数。
    • 首先,在trap_init() 中注册不同类型的中断/异常对应的处理函数(handler),这里的初始化主要用的是SETGATE 宏定义,针对这个宏定义,#define SETGATE(gate, istrap, sel, off, dpl) ... , 这个宏的具体内容这里没有贴出来,因为主要是针对宏参数进行分析的
    • 参数gate 表示的是门, 因为每个中断/异常都对应了一个中断向量,中断向量被CPU 用来作为IDT 的索引访问对应的门描述符,如图所示,因此第一个参数用idt[T_DIVIDE]可以选出对应的门描述符。在这里插入图片描述
    • 参数 istrap ,如果为 1 表示 trap (= exception) 门,为 0 表示 interrupt gate。这个参数的意义是:因为门有type(STS_{TG(任务门),IG32(中断门),TG32(陷阱门)}) 这三种门,这个参数可以确定门类型。
    • 参数sel 表示代码段选择,用宏定义 GD_KT 表示内核.text 段。
    • 参数 off 表示偏移地址,这里的偏移地址,结合上一个参数的段选择符号,可以确定中断处理函数在内存中的地址。
    • 参数 dpl 表示描述符优先级,简单点说就是这个handler 程序运行的权限。这个参数很重要,因为这里涉及到CPL、RPL 和 DPL ,首先解释下,什么是CPL ?CPL 表示的是当前执行的程序或者任务的优先级,被存储在cs 和 ss 的第0 位和第1 位上。RPL 是代码中根据不同段跳转而确定,以动态刷新cs 中的CPL ,存储在代码段选择符中。DPL 表示段或者门的 优先级,被存储在段描述符或者门描述符的DPL 字段中。我们这里的这个参数 dpl 表示的是目的代码段描述符的DPL 。

通过调用门进行程序的转移控制,cpu 会检查几个字段:当前代码段的CPL,调用门描述符中的DPL,调用门描述符中的RPL,目的代码描述符的DPL,目标代码段描述符中的一致性。
对于call 指令,需要满足当前的CPL <= 调用门描述符的DPL , RPL <= 调用门描述符的DPL。

如果在 SETGATE 中设置的DPL 的值 小于 当前运行程序的CPL的值会怎么样呢? 当前CPL >= 目的代码段描述符DPL; (也就是优先级低 的用户企图执行优先级高的代码段)
这时候,会出现general protection fault,稍后会实验验证。
PS:值越小,优先级越高,内核优先级用0 表示,用户一般是3。

  • 其次,需要在trapentry.S中声明中断处理函数,这个函数中的TRAPHANDLER 定义了一个全局可见的 中断/异常处理,压入一个中断/异常号 到特定的堆栈中。这个文件的内容主要是将寄存器的内容压入 堆栈。所有的中断/异常 首先会在这里先将现在运行的程序的信息压入堆栈(内核堆栈,即TSS中ES0 和 EP0 所指的堆栈),然后调用trap()。

(1)分析当一个中断/异常发生的时候系统的处理过程具体的流程:

  • 每个中断/异常对应一个中断向量,例如:除零中断,其指令为int $0x0, 操作系统执行int 指令时候,发生了堆栈的切换:堆栈从用户空间的堆栈切换到TSS 中SS0 和 ESP0 所指向的内核堆栈,然后内核将产生中断/异常的用户的重要的信息压入堆栈(包括 SS,ESP,EFLAGS,CS,EIP),然后将中断号(tf_err)和这个中断号在IDT 表中的偏移(tf_trapno)压入堆栈,接下来还要将剩下的信息(按照trapframe 中的格式)压入堆栈(包括 ds, es , tf_regs)。
  • 然后将内核对应的.data 段的分别赋值给 ds 和 es 寄存器,切换到内核.data 段,将 esp 压入内核堆栈(从这个指针所指位置开始往上的堆栈中存储的信息刚好和trapframe 完全对应!)。
  • 调用trap() 函数,trap() 函数根据 分配中断/异常 号对应的处理程序进行处理。

(2)关于trapentry.S 中 两种宏定义的区别
   trapentry.S 中定义了两种宏,分别针对是否有错误码。这里错误代码,如果是系统运行中产生的中断,根据不同的类型,切换完栈之后,处理器会向堆栈中放入一个错误代码。 如果是用户使用 int 指令手动调用中断,处理器是不会放入错误代码的!
对于那些不需要错误代码的中断/异常的时候,我们需要手动补齐这个空间,为了结构的同一。

现在分析一种情况:

一种类型的中断/异常(例如 14 号 T_PGFLT 缺页中断),这个中断本来是需要操作系统向堆栈中压入一个错误代码的,但是如果我们在用户程序中手动调用 int $0x0e, 那么会出现什么情况呢?(因为用户手动调用int 指令的话操作系统不会压入错误代码,我们来验证下。)

  • 前面已经分析过了,因为 14 号 T_PGFLT 的DPL 为 0 , 那么当用户直接用 int 指令来执行 的话,会导致 general protection fault 。
    在这里插入图片描述
  • 如果将 14 号中断的权限打开,使得用户程序可以用 softint 指令来执行(将 14 号中断对应的DPL 设置为 3),再验证试试。
    在这里插入图片描述

但是,不要高兴的太早。觉得事情就这么简单, 还记得我们题目中的 14 号中断只需要操作系统压入错误代码的!
现在我们用 softint 指令来执行,操作系统并没有向堆栈中压入错误代码!注意前面寄存器eip 的值,由于 没有压入错误码,导致后面的寄存器的值压入错位了!

  本来在中断/异常处理这里寄存器的压栈顺序是按照 trapframe 结构中的变量的顺序定义的,那么现在由于错位的原因,那么 在后面访问 trapframe 结构的最后一个字的时候(也就是访问 ss 寄存器的时候,肯定会访问到 KSTACKTOP 上的空间中去!!! ) 可是为什么上面的输出中还是打印了 ss 寄存器的值呢?这个值是哪里的?

	ts.ts_esp0 = KSTACKTOP;
	ts.ts_ss0 = GD_KD;
	ts.ts_iomb = sizeof(struct Taskstate);

  在inc/memlayout.h 中可以看到,KSTACKTOP 上的空间为VPT(系统页目录)。如果以页目录的虚拟地址来访问内存,会找到页目录中的第0个页表的物理地址,然后OFFSET为0,访问的就是第0个页表的第0个表项,也就是载入可执行文件的页面。因为JOS 在载入程序二进制文件时,会分配物理页面放到ELF 文件中,查看user/user.ld 中关于文件中的 stab 节的链接地址,可以看到这个程序的内容被映射到0x200000地址,实际上这段信息就是刚我们越过KSTACKTOP 后访问到的信息,如果我们尝试删除 user/user.ld 这段,重新编译运行JOS 的话,会出现下面的信息。出现了两次trap ,第一次是我们调用的 指令,第二次是因为去掉 user/user.ld 中用于调试的那一段之后,页目录找不到映射的页面了,因此发生了第二次缺页!
在这里插入图片描述

6、缺页和内存保护

确保自己已经完全消化了前面的内容,接下来我们继续完成LAB3!

  内存保护是操作系统的非常重要的一项功能,它可以防止由于用户程序的崩溃对操作系统带来的破坏与影响。
  操作系统通常依赖于硬件的支持来实现内存保护。操作系统可以让硬件能够始终知晓哪些虚拟地址是有效的,哪些是无效的。当程序尝试去访问一个无效地址,或者尝试访问一个超出其访问权限的地址时,处理器会将指令终止,并且触发异常,陷入内核态,与此同时将错误信息报告给内核。如果这个异常是可以修复的,那么内核会修复异常,然后让用户程序继续运行。如果异常无法修复,那么程序就被终止。
  作为一个可以修复的例子,让我们考虑一个可自动扩展的堆栈。在许多系统中,内核在初始情况下只会分配一个内核堆栈页,如果程序想要访问这个内核堆栈页以外的堆栈空间的话,就会触发异常,此时内核会自动在分配一些页给这个程序,程序就可以继续运行了。
  系统调用也为内存保护带来了问题,大部分系统调用接口让用户程序传递一个指针参数给内核。这个指针参数指向的是用户缓冲区。通过这种方式,系统在执行时,就可以解引用这些指针。但是这里有两个问题:

  • 在内核中的Page fault 要比在用户程序中的Page fault 更加严重。如果内核在操作自己的数据结构时出现Page fault , 这是一种内核的 bug , 而且异常处理程序会中断整个内核,但是当内核在解引用有用户程序传递来的指针时,它需要一种方法去记录此时出现的任何Page fault 都是由用户程序带来的。
  • 内核通常比用户程序用着更高的内存访问权限。用户程序很可能要传递一个指针给系统调用,这个指针指向的内核区域是内核可以进行读写的,但是用户程序不能。此时内核必须小心不要去解析这个指针,否则的话内核中总要的信息很可能被泄露。

  现在你需要通过仔细的阅读所有由用户传递来的指针所指向的空间,来解决上述两个问题,当一个程序传递给内核一个指针时,内核会检查这个地址是在整个地址空间的用户地址空间部分,而且确保指针指向的页面允许操作。
  这样操作系统将不会因为 解析用户进程传递进来的指针 而出现 Page fault 的情况。如果内核真的出现了 Page fault ,应该panic 并且终止程序。

  • Exercise 9
    在这里插入图片描述

(1) 完善page_fault_handler() 函数,实验要求里面提示了用tf_cs的低位来验证,那么每个env 的这个tf_cs在哪里设置的?怎么用这个来验证呢?
  在 kern/pmap.c 文件中的 env_alloc() 函数为用户进程分配env 的代码中,我们可以发现,其中 宏定义GD_UT为0x18 在 memlayout.h 文件中定义,可以看出不管是内核还是用户进程, 段选择符的后三位都是0, 其中 最后面的两位用来作为权限检查的(因为权限级别只有0,1,2,3,因此两位就够了,我觉得之所以空出三位应该是预留的), 用户的进程的.text 段的后三位通过代码 GD_UT| 3 设置为3,也就是特权级别为3。那么在page_fault_handler() 检查curenv 的这个字段就可以知道缺页是内核还是用户进程了。
在这里插入图片描述

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 && 0x01 == 0)
	{
		panic("page_fault in kernel mode, fault address %d\n", fault_va);
	}
	// We've already handled kernel-mode exceptions, so if we get here,
	// the page fault happened in user mode.

	// 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);
}

(2)完善user_mem_check()函数

int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
	// LAB 3: Your code here.
	//perm  = perm | PTE_U| PTE_P;
	void * start = (void *)ROUNDDOWN(va,PGSIZE);
	void * end = (void *)ROUNDUP(va+len,PGSIZE);	
	pte_t *pg_entry = NULL; 	
	for(; start < end;start += PGSIZE)
	{
		pg_entry = pgdir_walk(env->env_pgdir,start,0); 
		if((uint32_t)start > ULIM || pg_entry == NULL || (*pg_entry & perm) != perm)  
		{
			if(start < va)
			{
				user_mem_check_addr = (uintptr_t)va;
			}
			else
			{
				user_mem_check_addr = (uintptr_t)start; start;				
			}
			return -E_FAULT;
		}
	}
	return 0;
}

(3)完善 sys_cputs() 函数

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_P);

	// Print the string supplied by the user.
	cprintf("%.*s", len, s);
}

(4)完善kern/kdebug.c 中的 debuginfo_eip()

		// Make sure this memory is valid.
		// Return -1 if it is not.  Hint: Call user_mem_check.
		// LAB 3: Your code here.
	//user_mem_check(struct Env *env, const void *va, size_t len, int perm)
		if(user_mem_check(curenv,usd,sizeof (struct UserStabData),PTE_U) < 0)
			return -1;
		stabs = usd->stabs;
		stab_end = usd->stab_end;
		stabstr = usd->stabstr;
		stabstr_end = usd->stabstr_end;
		// Make sure the STABS and string table memory is valid.
		// LAB 3: Your code here.
		if(user_mem_check(curenv,stabs,stab_end - sstabs,PTE_U) < 0 || user_mem_check(curenv,stabstr,stabstr_end - stabstr,PTE_U) < 0)
			return -1;
	}

在这里插入图片描述
  自此LAB 3 全部完成! 非科班自学越来越发现实验越来越难了,这个实验从开始做到现在完全写完,花了5天!速度是很慢,但是学到了很多东西!后期有空的话还会过来完善这个实验的,因为到目前为止challenge 的内容我都没有做过,先把整体的框架弄明白。
  如果文中有错误的话,欢迎到评论区留言。

分类 MIT6.828 32位操作系统实验笔记

本文参考文章 http://grid.hust.edu.cn/zyshao/OSEngineering.htm
推荐这位博主系列的文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值