MIT6.828-Lab3-总结
Lab3的过程记录放在了我自己的博客上MIT6.828-Lab3,总结放在这里。
用户态环境的建立与管理以及异常处理机制
用户环境
JOS使用一个数组管理所有的环境(或称进程),最多有1024个struct Env
,其使用链表记录所有free
的env
,并使用currenv
来记录当前正在运行的环境。当前的物理内存布局如下:
![](/2024/03/09/MIT6-828-Lab3-%E6%80%BB%E7%BB%93/pic1.png)
接下来介绍一下JOS中第一个用户环境的创建流程:此处我觉得最妙的是在env_init
到env_pop_tf
的这段过程中,我们使用Trapframe
模拟了从用户态进入内核态时的栈的内容(因为第一个用户进程,不存在从用户态进内核态的过程,所以我们需要模拟),随后我们将tf
的首地址赋值给esp
寄存器(发生在env_poptf
),随后便是convention when switching from kernel to user mode
![](/2024/03/09/MIT6-828-Lab3-%E6%80%BB%E7%BB%93/pic2.png)
异常处理机制
在我们做完了上面的事情后,我们会创建一个hello的用户进程,其尝试使用int $0x30
来invoke一个系统调用的中断,接下来我们做的事情便是为了使得JOS可以处理CPU内部异常(类似于divezero,pagefault等等CPU定义的异常)和软中断(用户程序可以发起的系统调用)。
中断处理过程
![](/2024/03/09/MIT6-828-Lab3-%E6%80%BB%E7%BB%93/pic.png)
首先中断肯定是通过
int $num
来调用的,所以CPU会知道中断向量号,此中断向量号作为一个索引并结合IDTR
寄存器中保存的IDT表首地址,来查询此中断对应的中断门描述符(一般称Interrupt Gate/ Trap Gate/ Task Gate)。其中Interrupt Gate和Trap Gate的唯一区别即Interrupt Gate通过将EFLAGS寄存器中的IF标志位置0使其不可屏蔽,而Trap Gate则无此特性。然后根据门描述符的段选择子加上偏移便可找到中断程序所在的位置
中断栈帧
我们都知道,当操作系统call一个函数时,会把参数,eip,ebp入栈使得函数结束时得以回到原始位置。同样的,当操作系统invoke一个interrupt时,也会建立一个栈帧,用于保存上下文。
下面是一个常见的中断栈帧:图源
![](/2024/03/09/MIT6-828-Lab3-%E6%80%BB%E7%BB%93/pic4.png)
- 我们需要明白的是只有从用户态进入内核态时,才会push ss和esp用于保存用户栈位置,接着会将TSS中的ss0字段和esp0字段赋值给ss和esp寄存器。
- error code和trap no是由入口程序压入的(即trapentry.S)
- 在_alltrap中最后压入esp是用来将其作为函数参数传递给trap,*需要注意的是上图有点小问题,蓝色部分esp(tf)应该改成指向edi的旧esp
中断返回
1 |
|
tips
- 当用户程序想要使用
int $14
来invoke page fault时,实际上kernel会执行general protection fault,以防止对os恶意的操作 - 关于如何设置段描述符的dpl,出发点为是用户
int $num
还是内核做这件事,比如系统调用,用户态都是进到lib/syscall.c
中的syscall
函数然后int $0x30
,即是通过用户态发起中断请求, 所以系统调用的中断描述符的dpl必须设置为3
Page Faults, System Calls and mem protection
缺页中断
- 当产生一个缺页中断的错误时,出错的线性地址会保存在cr2寄存器中
- kernel mode下的缺页中断是一种bug,可以通过Trapframe的cs字段判断其特权级来判断是kernel还是user mode
系统调用
Linux中有超过256个的系统调用,而中断向量号由于历史兼容性是8位的无符号整数,所以JOS采用将syscallno(用于区分不同系统调用)通过硬件保存在eax寄存器中,其余5个系统参数保存在edx,ecx,ebx,edi,esi寄存器中。
1 |
|
内存保护
为了防止用户程序通过系统调用访问内核空间,我们需要在系统调用函数中加上内存检查(比如传递过来的参数是否超出ULIM,对应的pte的权限部分是否允许用户读或写等等)
1 |
|
关于系统调用还需再提一嘴
在内核模式和用户模式下调用系统调用的过程是不一样的。
就拿cprintf
函数来说吧,下面是用户模式下调用cprintf
的过程:
1 |
|
而如果是在内核模式下调用cprintf
的话,就直接从cprintf in kernel mode
开始即可