Part B: Page Faults, Breakpoints Exceptions, and System Calls
现在,JOS具有了基本的异常处理功能,我们将对其进行优化,以提供用于处理异常或中断的重要操作系统原语。
处理页面故障
页面错误异常(T_PGFLT)是一个特别重要的异常,当处理器发生页面故障时,引起故障的线性(即虚拟)地址会被存储在特殊的处理器控制寄存器CR2中。在trap.c中,我们提供了一个函数page_fault_handler()(仅编写了开头的代码),用于处理页面错误异常。
练习5:修改trap_dispatch()来将页面错误异常调度到page_fault_handler()。修改后,makegrade测试一下,看看是否通过了faultread, faultreadkernel, faultwrite, and faultwritekernel的检查。如果其中任何一个不起作用,找出原因并修复。我们可以使用make run-x或make run-x-nox将JOS引导到特定的用户程序中。 例如,make run-hello-nox运行hello用户程序。
断点异常
断点异常(T_BRKPT)通常用于允许调试器通过用特殊的int 3软件中断指令临时替换相关的程序指令,从而在程序代码中插入断点。
练习6:修改trap_dispatch()使断点异常能够调用内核监视器。要求:通过breakpoint的测试。
上面两个练习都比较简单,代码如下:
成功通过测试:
练习6还有个挑战,要求修改jos的内核监视器代码来实现单步调试功能,尝试写了一下,还是有点问题,这
里先留个坑,相关参考资料:x86—EFLAGS寄存器详解。
问题3:断点测试用例将生成断点异常或常规保护错误,具体生成哪个错误取决于您如何初始化IDT中的
中断描述符(就是trap_init调用SETGATE那一段代码),这是为什么? 为了使断点异常按上述规定工作,您需要如何对其进行设置?什么不正确的设置将导致它触发常规保护故障?
答:在trap_init中,我们会调用SETGATE函数来设置各个中断描述符,而设置的DPL位为描述符特权等级(
即访问该描述符对应的中断处理程序应有的特权等级),当我们将断点异常对应的DPL位设置为0时,用户
程序由于特权等级低,访问断点异常处理程序则会产生保护错误,而DPL设置为3时,会正确生成断点异常。
问题4:尤其考虑到user/softint测试程序,你认为这些机制的要点是什么?
答:通过设置DPL来将代码分级,实现一个隔离保护的作用,参考资料:详解 RPL、DPL、CPL 的关系。
系统调用
用户进程通过系统调用请求内核服务,当用户进程使用系统调用时,处理器进入内核模式,处理器和内核
会共同保存用户进程的状态,然后会执行内核代码来为用户进程提供服务,执行完后,恢复环境,返回到用
户进程,用户进程请求系统调用的方法和细节因系统而异。
在JOS内核中,我们将使用Int指令,该指令会产生一个处理器中断,特别地,我们将用int $30作为系统调
用中断,inc/trap.h中已经定义了T_SYSCALL,我们现在要设置该中断描述符以允许用户进程产生该中断,
注意:中断0x30不能由硬件产生,因此不会因为允许用户代码产生中断而有歧义。
应用程序使用寄存器来传递系统调用号和系统调用参数,因此内核无需搜索用户进程的堆栈或指令流.系统调用号保存在eax中,参数(最多五个)将分别保存在edx,ecx,ebx,edi,esi中,系统调用完成后,其返回
值也会保存在eax中,请求系统调用的汇编代码已经在lib/syscall.c中的syscall()函数中编写好,阅读
并理解这段源码。
练习7:
为中断向量T_SYSCALL在内核中添加一个中断处理函数,需要编辑 kern/trapentry.S及kern/trap.c中的
trap_init(),还需要更改trap_dispatch()来处理系统调用中断,方法是用适当的参数调用syscall()(在kern/syscall.c中定义),然后在%eax中将返回值传递回用户进程。最后,需要在kern/syscall.c中实现syscall()。阅读并理解lib/syscall.c(尤其是内联程序集例程),通过为每个请求调用相应的内核函数来处理inc/syscall.h中列出的所有系统调用。
要求:
在内核下运行user/hello程序(make run-hello)。它应该在控制台上打印“hello world”,然后在用户模式下导致页面错误。如果这种情况没有发生,可能意味着编写的系统调用处理程序不太正确。make grade也要通过testbss的测试.
lib/syscall.c解析:
有了PartA在JOS中设置IDT,中断入口及前面做的在XV6中添加系统调用的HomeWork的积累,这个练习做起来就很简单了。贴上修改后的kern/syscall.c和kern/trap.c的核心代码:
trap.c/trap_dispatch():
syscall.c/syscall():
测试通过:
最后以sys_cputs为例走一遍用户程序请求系统调用的流程:
lib/syscall.c/sys_cputs(s,len)-> lib/syscall.c/syscall(SYS_cputs, 0, (uint32_t)s, len, 0,
0, 0)->触发48号中断->kern/trap.c/trap(tf)->kern/trap.c/trap_dispatch(tf)的T_SYSCALL分支->
kern/syscall.c/syscall(eax,edx,ecx,ebx,edi,esi)的SYS_cputs分支->kern/syscall.c/sys_cputs
((char)(a2),a3)*;
用户模式启动
一个用户程序从lib/entry.S的顶端开始运行,在做完一些设置之后,会调用lib/libmain.c中的libmain函数,修改libmain()来初始化全局指针thisenv,使其指向当前进程的env结构体,提示: 在inc/env.h中查找并使用sys_getenvid,然后libmain()调用umain,在hello程序的情况下,它位于user/hello.c中。注意,在打印“hello,world”之后,它会尝试访问thisenv->env_id。这就是出现页面故障的原因。如果正确地初始化了这个环境,那么应该不会出错。
阅读inc/env.h的源码和注释:
可以知道envid分为三部分,最高位为0,低十位为其在envs数组中的下标,中间21位用来唯一标志当前进
程,宏函数ENVX通过保留envid的低十位来获得其在envs数组中的下标。那么要初始化全局指针thisenv。
添加如下代码即可:
测试通过:
页面错误和内存保护
内存保护是操作系统的一个重要功能,它能保证发生BUG的程序不会影响到其他程序和操作系统。操作系统通常依赖硬件的支持来实现内存保护,操作系统使硬件知道哪些虚拟地址有效,哪些无效。 当程序尝试访问无效地址或没有权限的地址时,处理器会在导致错误的指令处停止程序,然后使用有关信息陷入内核。如果该错误可修复,则内核会对其进行修复,并让程序继续运行。 如果故障无法修复,则程序将无法运行下去。可修复故障的示例典型是自动扩展堆栈。 在许多系统中,内核最初分配一个堆栈页面,如果程序在访问堆栈中更远的页面时出错,则内核将自动分配这些页面并让程序继续。 这样,内核仅分配程序所需的堆栈内存,但从程序角度看,它拥有任意大的堆栈。
系统调用引出了一个有趣的内存保护问题。 大多数系统调用接口都允许用户程序将指针传递给内核
,这些指针指向要读取或写入的用户缓冲区。 然后,内核在执行系统调用时解引用这些指针。 这有两个问题:
1.内核中的页面错误可能比用户程序中的页面错误严重得多。如果内核在处理自己的数据结构时出现页面错误,那就是内核错误,错误处理程序会使内核(以及整个系统)死机。因此,当内核解引用用户程序传递过来的指针时,它需要记住这个错误属于用户程序。是来自用户进程。
2.内核通常比用户程序具有更多的内存权限。 用户程序可能会传递一个指向系统调用的指针,该指针指向内核,可以读取或写入用户程序无法读取的内存。 内核需要小心不被欺骗去解引用这样的指针,因为这可能会泄露私有信息或破坏内核的完整性。
由于这些原因,内核在处理用户程序提供的指针时必须格外小心。
现在,我们将通过一种机制来仔细检查这两个问题,该机制将仔细检查从用户空间传递到内核的所有指针。 当程序将指针传递给内核时,内核将检查该地址是否位于地址空间的用户部分中,以及页表是否允许进行内存操作。因此,内核将永远不会由于解引用用户程序提供的指针而导致页面错误,如果内核出现页面错误,则它应该崩溃并终止。
练习9:
修改trap.c,如果是在内核态下发生页面错误,应该调用panic,阅读kern/pmap.c中的user_mem_assert并在该文件中实现user_mem_check函数,修改一下 kern/syscall.c 去检查系统调用的输入参数。启动内核后,运行 user/buggyhello 程序,用户环境应该被销毁,内核不会panic,应该会打印出如下信息:
user_mem_check函数(根据注释中的提示实现即可):
修改syscall.c的sys_cputs函数:
运行buggyhello结果:
最后,修改kern/kdebug.c文件中的debuginfo_eip函数调用user_mem_check检查usd,stabs,stabstr,修
改后运行user/breakpoint,应该能够从kernel monitor运行backtrace,并在内核因页面错误而死机之前看到回溯遍历到lib/libmain.c,为什么发生了页面错误呢?
答:在kern/monitor.c的mon_backtrace函数中调用了debuginfo_eip函数,在执行user_mem_check发生了页面错误,由于backtrace本身位于内核中,故在内核态下发生页面错误导致了内核死机。
练习10: 启动你的内核,运行user/evihello.这个环境应该会被销毁,而内核不会panic。
你会见到:
user/evihello运行结果: