9
LAB3代码已经上传。
最近忙于打WOWTCG,早就做完了一直没腾出时间写博客。
LAB3第二部分主要是处理系统调用。
第一部分我们已经让第一个env运行了起来,接着这个env执行一个cprintf,这个cprintf是一个系统调用,因为os暂时没有实现系统调用,所以系统崩溃。
在lab2我们就要完成各种系统调用以及exception和trap等的实现。
handout地址:http://pdos.csail.mit.edu/6.828/2011/lec/x86_idt.pdf
博文中的很多说明都引用了handout里面的图。
一、原理
系统调用、中断以及异常os的处理方式都一样,他们的区别只在于陷入内核的方法不同。
interrupt和exception略有差异,但总体可以认为是某些硬件机制导致我们陷入内核,而系统调用是我们在用户态程序中执行int48进行调用,用户态的调用工作在lib下的syscall.c中。之所以是int 48,是因为T_SYSCALL常量定义为48,理论上定义32--255之间的任何值都可以,0--31规系统使用。详细信息可见handout。
下面详细分析系统调用陷入内核的过程。
首先系统调用的入口在lib下的syscall.c,可以看到,函数syscall使用了嵌入式汇编,先将各个参数分别赋值给eax,ebx,ecx,edx,edi,esi,然后约定将返回值放入eax中(把返回值放入eax的过程是我们需要在内核中实现的),接着使用int 48陷入内核。
剩下的部分中断、异常、陷阱、系统调用都一样(虽然我也分不太清这几个概念),因此下文中除非特殊说明,“中断”一词代表着中断、异常、陷阱、系统调用这4个概念,但唯独中断号和系统调用号进行区分,中断号是指idt表中的索引,系统调用号是指不同的系统调用函数的标识符。
int指令是一个较为复杂的指令,其做了很多事情,按顺序包括以下几步:
1、查找idtr里面的idt地址,根据这个地址找到idt,然后根据idt找到中断向量的表项。
2、检查cpl和表项的dpl,如果cpl>dpl产生保护异常,否则继续
3、根据tssr寄存器里的tss地址找到tss在内存中的位置,读取其中的ss和esp并装载(tss结构是x86定义好的,其内存中存放的位置需要os去决定,并对其中内容的赋值也要os实现,这部分内容在trap.c的trapinitpercpu函数中,同时还包括着加载idt的逻辑)。
4、如果是一个用户态到内核态的陷入操作,则像堆栈中压入ss和esp,注意这个ss和esp是之前用户态的数据,而不是新装载的数据
5、压入esp,eflags,eip
6、修改eflags中的某些位(比如关中断)
7、如果有必要,压入errorcode,某些中断需要errorcode以及不同中断的errorcode含义可查看handout。
8、根据idt表项设置cs和eip,也就是跳转到处理函数执行。idt内容相关可查看handout。
压入后的堆栈就应该是这个样子的,跳转到相应中断处理函数的时候我们面对的就是这样一个堆栈。接着中断处理函数处理相应操作,然后根据目前堆栈里有的内容和当前寄存器的内容恢复现场,继续程序执行。
二、实验
大概弄懂了原理,接下来解析具体的实验。
1、完成trapentry.S和trap.c的部分内容,使之能正确的调用trap函数,并将一个正确的trapframe结构指针当做函数的参数
在这个实验时,还需要完成很多操作才能进行系统调用。因为加载idt的工作JOS已经帮我们做了,我们需要做的就是初始化idt,给不同的中断分配不同的处理函数。
简单分析JOS的逻辑可知,JOS是先将所有中断都跳转到trap函数,再在trap函数里调用trap_dispatch来进行分发,再在trap_dispatch中调用具体的处理函数,虽然这个过程复杂繁琐且个人认为完全没有必要,但至少这意味着idt里指向的函数只需要调用trap函数就可以了。
在trapentry.S中,根据定义的两个宏,参考handout里面errorcode所对应的中断号,可以为不同的中断号定义处理函数,名字随便取,然后再在trap.c中声明这些函数,并获取函数地址填充进idt中(使用setgate宏,使用GD_KT段,也就是OS的代码段),DPL我参考了linux一些信息,将breakpoint,overflow,以及system call设置为3,其余都是0。
此时idt初始化代码就完成了,发现所有中断处理函数跳转到_alltrap处,在trapentry.S中定义_alltrap符号,接着往堆栈里压入某些值,使堆栈看起来像是一个trapframe,此时堆栈栈顶的指针就是指向这个trapframe的指针(结构体内存从低向高增长),将这个指针也就是esp压入堆栈,调用trap函数,进入函数时,会取栈顶元素当做参数,正好就是这个trapframe,这样就完成了我们想要的功能。
接下来考虑如何压栈才能让堆栈看起来像trapframe。
tramframe看起来应该是这个样子的:
在每一个中断处理函数里,已经压入了trapno,所以在_alltrap处,还需要压入trapno以下的所有内容。接着根据要求加载内核数据段,最后再压入esp,执行call trap即可转入c执行。至此exercise 4完成。
2、完成部分trap_dispatch逻辑
这部分没啥可说的,根据中断号把page fault分发到trap.c里的trap_fault_handler函数,然后发现函数里把这个唯一的env给毁掉了,也就是说用户程序出了page fault直接销毁,大概是这么个逻辑。这样exercise 5就完成了。
3、完成breakpoint的中断处理
自己建个函数,然后在trap_dispatch里根据中断号跳转到此函数,在此函数中调用monitor(tf)即可,如果想让到达断点的程序继续执行,可在monitor中多加个命令,退出monitor中的那个死循环,然后trap函数就会根据trapframe里的内容恢复现场继续执行下去。
编程5年总算是知道这个断点是怎么实现的了,囧。
4、完成内核区的系统调用
根据trapframe内的系统调用号(放在eax里)完成相应的系统调用,返回结果要放在eax里,这样经过iret返回时才能得到正确的结果。
5、完成libmain
在libmain用刚才写好的系统调用取得该env的envid,然后根据envid得到Env结构(使用ENVX宏获取UENVS数组的索引),简单的很,也没啥可说的。
6、如果在内核态发生page fault,则panic
在page_fault_handler中判断是否在内核态产生page_fault,判断的方法是查看传入trapframe的cs中的DPL,如果是0,即为内核态。
7、系统调用中的参数检查
在系统调用中有一个是向控制台输出信息的函数sys_cputs,在此函数中要去检查用户传入的字符串所处的内存是否有映射以及是否有read权限,方法也很简单,遍历page table,查看其页表项的PTE_P和PTE_U位即可,如果通不过检测,就把这个envpanic掉。
好了,到此为止应该能通过绝大多数testcase,只有一个testbss除外。
bss里存放着未初始化的变量,和data段不同的是,data段存放变量及其值,而bss段只存放变量及其所占空间大小的信息,所以bss段在elf中所占用的空间要小的多。
现在的问题是,在load_icode时,我们并没有加载bss段。通过objdump来看,按JOS的逻辑,让我们在program header表中只加载ELF_PROG_LOAD类型的段是不会加载bss段的,bss段存放于elf的section表中,且其类型的id是8,因此我们必须写点代码把这个bss段加入内存才行。
但问题是实验中并没有给出这些内容相关的提示,或许是我阅读的材料不够(我没阅读所有相关材料),也许是实验的设计者压根就忘了,反正加载bss段之后就能通过测试了。
代码以上传,因版面问题,就不贴在这里了。