zoukankan      html  css  js  c++  java
  • MIT_JOS_Lab3_PartB

    Handling Page Faults

    产生却也中断的时候, 需要保留当前的虚拟地址到寄存器 CR2,

    Exercise 5. Modify trap_dispatch() to dispatch page fault exceptions to page_fault_handler(). You should now be able to get make grade to succeed on the faultread, faultreadkernel, faultwrite, and faultwritekernel tests. If any of them don't work, figure out why and fix them. Remember that you can boot JOS into a
    particular user program using make run-x or make run-x-nox. For instance, make run-hello-nox runs the hello user program.

    需要明白的是 trap 的过程, 当遇到中断的时候去访问中断向量表, 中断向量表的结构是 SETGATE(idt[T_PGFLT], 0, GD_KT, &th_pgflt, 0); 这样定义得到了, 那么就会去访问函数 th_pgflt, 在 trapentry.S 中, 我们又使用了 #define TRAPHANDLER(name, num) 将 name 与 函数对应起来, 所以查表的时候就会调用 TRAPHANDLER 函数, 然后会在下面这部分代码:

    .global _alltraps
    _alltraps:
        pushl %ds
        pushl %es
        pushal
        movw $GD_KD, %ax
        movw %ax, %ds
        movw %ax, %es
        pushl %esp
        call trap
    

    手动实现调用 trap 函数的过程, 参数与 %esp 寄存器的入栈都是手动的.

    在源码中我并没有找到发生中断的时候, 跳转到中断向量表的跳转指令, 这个过程同时需要将 CPU 切换到内核态, 切换之前要先保存 SS 寄存器与 ESP 寄存器, 所以栈中应该是下面的结构:

     					 +--------------------+ KSTACKTOP             
                         | 0x00000 | old SS   |     " - 4
                         |      old ESP       |     " - 8
                         |     old EFLAGS     |     " - 12
                         | 0x00000 | old CS   |     " - 16
                         |      old EIP       |     " - 20 <---- ESP 
                         +--------------------+    
    

    将栈切换到TSS的SS0和ESP0字段定义的内核栈中,在JOS中两个值分别是GD_KD和KSTACKTOP。 这样就完成了切换到内核栈, 然后就可以查找出 handler, 在handler的末尾就会跳转到 trap() 函数,

    // Trapframe 在 Env 中的定义是保存当前环境, 这里传入的参数就是新的运行环境
    // 表示陷入内核执行的过程, 在内核执行的过程
    void trap(struct Trapframe *tf)
    {
    	// The environment may have set DF and some versions
    	// of GCC rely on DF being clear
    	asm volatile("cld" ::: "cc");
    
    	// Check that interrupts are disabled.  If this assertion
    	// fails, DO NOT be tempted to fix it by inserting a "cli" in
    	// the interrupt path., 进入内核之后要先关中断, 不允许其他中断
    	assert(!(read_eflags() & FL_IF));
    	// 输出了 trap 的信息, 
    	cprintf("Incoming TRAP frame at %p
    ", tf);
    
    	if ((tf->tf_cs & 3) == 3) {
    		// Trapped from user mode.
    		assert(curenv);
    		// Copy trap frame (which is currently on the stack) into 'curenv->env_tf',
    		//  so that running the environment will restart at the trap point.
            // 这个过程就是保存了 tf, 也就是进入内核之前的状态
    		curenv->env_tf = *tf;
    		// The trapframe on the stack should be ignored from here on.
    		tf = &curenv->env_tf;
    	}
    	
    	// Record that tf is the last real trapframe so
    	// print_trapframe can print some additional information.
    	last_tf = tf;
    
    	// Dispatch based on what type of trap occurred, 为 tf 分配一个 handler, 并进行了 trap 处理
    	trap_dispatch(tf);
    	// 在 trap_dispatch 函数的末尾, 使用了 env_destroy(curenv); 
    	// Return to the current environment, which should be running.
    	assert(curenv && curenv->env_status == ENV_RUNNING);
    	// 返回到用户进入内核之前执行的指令
    	env_run(curenv);
    }
    

    Exercise 6. Modify trap_dispatch() to make breakpoint exceptions invoke the kernel monitor. You should now be able to get make grade to succeed on the
    breakpoint test

    对于 Exercise 5和6 都是分配中断, 直接修改 Trap_dispatch 就可以了,

    static void trap_dispatch(struct Trapframe *tf)
    {
    	// Handle processor exceptions.
        switch (tf->tf_trapno) {
    		// 这里是判断 trap 的类型, 对于不同的 trap, 有不同的处理函数
        case T_PGFLT: page_fault_handler(tf); return;
    	case T_BRKPT: monitor(tf); return;
    
        default:
    		// 未知的 trap
            // Unexpected trap: The user process or the kernel has a bug.
            print_trapframe(tf);
            if (tf->tf_cs == GD_KT)
                panic("unhandled trap in kernel");
            else {
                env_destroy(curenv);
                return;
            }
        }
    }
    

    对于两个问题:

    1. 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?

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

    System calls

    在 JOS 中实现系统调用的方法是使用中断的方式, 所以需要构建中断向量表与中断描述符, 还有在 trapentry.S 文件中添加中断处理函数, 就相当于添加了一个中断, 系统调用对于 trap 来说使用的参数是相同的, 所以判断不同的系统调用判断的参数在 syscall 函数中, 也就是在 trap_dispatch 的时候会分配参数, 这里的情况是:

    case T_SYSCALL:
    		tf->tf_regs.reg_eax = 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);
    		return;
    

    在系统调用发生的过程中, 用户程序将在寄存器中传递系统调用号和系统调用参数。这样,内核就无需在用户环境的堆栈或指令流中操作。系统调用号将以 %eax 开头,参数(最多五个)将分别以 %edx,%ecx,%ebx,%edi和%esi开头。内核将返回值传回%eax。在lib/syscall.c 中的 syscall() 中已编写了用于调用系统调用的汇编代码. 这部分代码是系统调用的通用代码, 就是

    // 将系统调用的参数放入 CPU 寄存器中
    static inline int32_t
    syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
    {
    	int32_t ret;
    	/*系统调用
    	// Generic system call: pass system call number in AX,
    	// up to five parameters in DX, CX, BX, DI, SI.
    	// Interrupt kernel with T_SYSCALL.
    	*/
    	asm volatile("int %1
    "
    		     : "=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;
    }
    
    void sys_cputs(const char *s, size_t len)
    {
        // 这里的 syscall 函数就是上面的 /lib/syscall.c 中的 syscall 函数
    	syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
    }
    

    注意上面的代码是在内核模式下执行的, 而在 Trap_dispatch 中的 syscall 函数是 kern/syscall.c 中的 syscall() 函数, 这里的 syscall() 函数与 lib/syscall.c 下面的 syscall() 函数有什么区别呢? 我们需要知道这两个函数之间的关系, 我们先补全 kern/syscall.c 函数中的 syscall() 的代码,

    // Dispatches to the correct kernel function, passing the arguments.
    // 系统调用的参数是以系统调用号为开头的, 第一个参数是系统调用号
    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.
    	// panic("syscall not implemented");
    	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_INVAL;
    	}
    }
    

    举个例子, static void sys_cputs(const char *s, size_t len) 这个函数中调用了 cprintf("%.*s", len, s); 函数, 我们知道 cprintf() 函数是调用了下面这个函数, 也就是:

    int vcprintf(const char *fmt, va_list ap)
    {
    	struct printbuf b;
    	b.idx = 0;
    	b.cnt = 0;
    	vprintfmt((void*)putch, &b, fmt, ap);
        // 这里调用了 lib/syscall.c 里面的 syscall() 函数, 所以最终还是调用了 lib/syscall.c 下面的系统调用函数
    	sys_cputs(b.buf, b.idx);
    	return b.cnt;
    }
    

    User-mode startup

    操作系统启动的时候是在内核模式下启动的, 现在要运行用户程序, 所以需要启动用户模式. 用户模型开始执行的位置是 /lib/entry.S, 这个位置我猜想是操作系统决定的,这与 /kern/entry.S 的内容不太一样, 在一些设置之后代码调用 lib/libmain.c 中的 libmain() 函数, 现在需要修改 libmain() 函数去初始化 thisenv 指针使其指向当前进程在 struct Env in the envs[] array 结构体数组的位置, 我们知道, 在此之前, envs 虚拟地址已经与 ENVS 映射过了. 初始化 thisenv 指针的过程就是:

    // thisenv 指向将当前正在执行的进程
    	thisenv = &envs[ENVX(sys_getenvid())];
    

    然后 libmain() 函数就会调用 umain,这个 umain 程序恰好是 user/hello.c 中的main函数, 也就是我们通产的 main 函数。在之前的实验中我们发现,hello.c程序只会打印 "hello, world" 这句话,然后就会报出 page fault 异常,原因就是在 hello.c 中的 thisenv->env_id 这条语句。现在你已经正确初始化了这个 thisenv的值,再次运行就应该不会报错了。

    上述就是用户模型启动的主要过程,可以总结为, 从 kernel/init.c 中的 env_run(&envs[0]); 开始执行用户程序, 这个程序是编译好了,通过创建进程时构建的 environment, 然后用户进行运行的步骤还受限于操作系统, 也就是说操作系统决定用户程序的入口. 所以windows下编译的C程序显然不能在linux下运行.

    Page faults and memory protection

    内存保护是操作系统中十分重要的一个部分, 能够保证一个程序的bug 会引起另一个程序的bug, 也不会导致整个程序的崩溃, 操作系统通常需要硬件的支持来实施内存保护, JOS 告诉操作系统哪些虚拟地址是有效的, 哪些是无效的, 这里的虚拟地址就是内存访问的虚拟地址, 当一个程序试图访问无效的地址或者没有权限的地址的时候就会停止当前导致错误的程序, 如果这个错误是可以修复的, 那么内核程序就会试图去修复他, 然后继续运行之前的程序, 如果无法修复, 程序就不能继续执行, 对于可修复错误的例子, 比如说栈的访问, 如果当前内核给用户栈只分配了一页, 而用户访问了栈底的下面, 那么就会导致内存错误, 这是, 只需要 trap 进入内核, 为用户分配足够的空间即可.

    系统调用给内存保护带来一个有趣的问题, 许多系统调用的接口将用户程序的指针传递给内核, 这些指针指向用户读或者写的缓冲区, 而内核在执行系统调用的时候将解引用这些指针, 也就是说读或者写该指针指向地址的内容, 这会导致内核在判断缺页时的问题:

    1. 缺页是由谁造成的问题: 因为内核对用户指针进行了解引用, 内核也会去访问这一段内存, 因为内核本身也可能发生缺页, 如果这时候发生了缺页是谁造成的呢? 所以内核需要记录解引用访问的时候是谁造成了缺页问题
    2. 内核修改内存内容问题: 内核拥有比用户程序更高的权限, 那么在缺页的时候, 内核会改变内存的内容, 但是这时不能改变用户的内容, 否则会造成用户的保护信息收到侵犯.

    现在你需要通过仔细检查所有由用户传递来指针所指向的空间来解决上述两个问题。当一个程序传递给内核一个指针时,内核会检查这个地址是在整个地址空间的用户地址空间部分,而且页表也运行进行内存的操作。

    Exercise 9. Change kern/trap.c to panic if a page fault happens in kernel mode. Hint: to determine whether a fault happened in user mode or in kernel mode, check the low bits of the tf_cs.

    这一步就是判断缺页中断是内核进程缺页还是用户进程缺页:

    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.
        // tf_cs 用来表示优先级
    	if ((tf->tf_cs & 0x3) == 0) {
            panic("page fault in kernel mode!");
        }
    	// 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
    ",
    		curenv->env_id, fault_va, tf->tf_eip);
    	print_trapframe(tf);
    	env_destroy(curenv);
    }
    

    Read user_mem_assert in kern/pmap.c and implement user_mem_check in that same file.

    int user_mem_check(struct Env *env, const void *va, size_t len, int perm)
    {
    	// LAB 3: Your code here.
    	uint32_t* start = ROUNDDOWN((void*)va, PGSIZE);
    	uint32_t* end = ROUNDUP((void*)(va+len), PGSIZE);
    	// 初始化地址
    	uint32_t addr = (uint32_t)va;
    	pte_t *temp = NULL;
    	while( start < end) {
    		temp = pgdir_walk(env->env_pgdir, (void *)start, 0);
    		// 1. 访问到内核空间   2. 页表缺页       3. 页表的页表项无效     4. 页表项内容的权限不一致
    		if (start >= ULIM || temp == NULL || !(*temp | PTE_P) || (*temp & perm) != perm) {
    			// 缺页错误的首地址, 表示从某处开始页表访问错误
                user_mem_check_addr = (start < addr) ? addr : start;
                return -E_FAULT;
            }
            start += PGSIZE;
    	}
    	return 0;
    }
    

    Change kern/syscall.c to sanity check arguments to system calls

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

    Finally, change debuginfo_eip in kern/kdebug.c to call user_mem_check on usd, stabs, and stabstr. If you now run user/breakpoint, you should be able to run
    backtrace from the kernel monitor and see the backtrace traverse into lib/libmain.c before the kernel panics with a page fault. What causes this page fault? You don't need to fix it, but you should understand why it happens.

    这一部分的代码就是确保符号表的正常访问,

    		// Make sure the STABS and string table memory is valid. 用户是可以读符号表的
    		// LAB 3: Your code here.
    		if (user_mem_check(curenv, stabs, stab_end - stabs, PTE_U) < 0) {
    			return -1;
    		}
    		if (user_mem_check(curenv, stabstr, stabstr_end - stabstr, PTE_U) < 0) {
    			return -1;
    		}
    

    至此完成了 Lab3的全部内容, 这一部分主要是进程的创建, 进程表的构建, 启动进程, 导入二进制文件, 执行程序, 还有中断与异常的操作需要 trap 进入内核, 在编译的过程我们可以得到 Lab3 测试的整个流程:

    + as kern/entry.S    // boot_loader 之后内核开始执行
    + cc kern/entrypgdir.c		// 静态页目录初始化
    + cc kern/init.c		// 内核开始
    + cc kern/console.c		// 初始化控制台, 与输入输出
    + cc kern/monitor.c		// 检测装置
    + cc kern/pmap.c		// 内存初始化
    + cc kern/env.c			// 进程初始化
    + cc kern/kclock.c
    + cc kern/printf.c
    + cc kern/trap.c		
    + as kern/trapentry.S
    + cc kern/syscall.c
    + cc kern/kdebug.c
    + cc lib/printfmt.c
    + cc lib/readline.c
    + cc lib/string.c
    + cc[USER] lib/console.c
    + cc[USER] lib/libmain.c
    + cc[USER] lib/exit.c
    + cc[USER] lib/panic.c
    + cc[USER] lib/printf.c
    + cc[USER] lib/printfmt.c
    + cc[USER] lib/readline.c
    + cc[USER] lib/string.c
    + cc[USER] lib/syscall.c
    + ar obj/lib/libjos.a
    ar: creating obj/lib/libjos.a
    + cc[USER] user/hello.c			// 这里执行 hello.c 之后进入 entry.S
    + as[USER] lib/entry.S
    + ld obj/user/hello				// 二进制文件, 执行
    + cc[USER] user/buggyhello.c
    + ld obj/user/buggyhello
    + cc[USER] user/buggyhello2.c
    + ld obj/user/buggyhello2
    + cc[USER] user/evilhello.c
    + ld obj/user/evilhello
    + cc[USER] user/testbss.c
    + ld obj/user/testbss
    + cc[USER] user/divzero.c
    + ld obj/user/divzero
    + cc[USER] user/breakpoint.c
    + ld obj/user/breakpoint
    + cc[USER] user/softint.c
    + ld obj/user/softint
    + cc[USER] user/badsegment.c
    + ld obj/user/badsegment
    + cc[USER] user/faultread.c
    + ld obj/user/faultread
    + cc[USER] user/faultreadkernel.c
    + ld obj/user/faultreadkernel
    + cc[USER] user/faultwrite.c
    + ld obj/user/faultwrite
    + cc[USER] user/faultwritekernel.c
    + ld obj/user/faultwritekernel
    + ld obj/kern/kernel
    
  • 相关阅读:
    访问控制模型+强制访问控制
    防火墙体系结构
    信息安全工程师手记
    关于PHPSESSIONID的一些发现
    中级测评师10-物联网
    WAPI学习记录
    1. Jenkins 入门使用
    Springboot druid监控配置
    Springboot 添加数据源报错
    mysql function 查询子级机构
  • 原文地址:https://www.cnblogs.com/wevolf/p/12778647.html
Copyright © 2011-2022 走看看