最近做操作系统的实验,本来挺好的,结果被那个challenge给折磨了好久,真的想说challenge的题目描述实在让人无力吐槽。
扩展proj4,增加syscall功能,即增加一用户态函数(可执行一特定系统调用:获得时钟计数值),当内核初始完毕后,可从内核态返回到用户态的函数,而用户态的函数又通过系统调用得到内核态的服务。需写出详细的设计和分析报告。完成出色的可获得适当加分。
提示:规范一下 challenge 的流程。
kern_init 调用 switch_test,该函数如下:
static void switch_test(void) {
print_cur_status(); // print 当前 cs/ss/ds 等寄存器状态
cprintf(“+++ switch to user mode +++n”);
switch_to_user(); // switch to user mode
print_cur_status();
cprintf(“+++ switch to kernel mode +++n”);
switch_to_kernel(); // switch to kernel mode
print_cur_status();
}
switch_to_* 函数建议通过 中断处理的方式实现。主要要完成的代码是在 trap 里面处理 T_SWITCH_TO* 中断,并设置好返回的状态。
就这样的描述,反正从头到尾我也没能弄清楚他是想要让我们做什么,上来就说让我们增加syscall功能,可最后却check的是内核态与用户态的切换。
然后,最让我不能理解的还是,为什么他要让我们通过中断做一个从用户态到内核态的切换,用户态自己随便调用一个中断,就能把权限提升为内核权限,这样的操作系统真的还有安全性可言么,实在是不能理解!!!(PS:后听一同学说是内核启动第一个用户程序的时候,需要从用户态切回内核态,完全初始化好后再关掉此中断即可)
不过,既然让做,咱就来做吧:
首先是内核态切到用户态的函数:
题目建议通过中断处理实现,于是乎在trap.h中查找中断号,发现:
#define T_SWITCH_TOU 120 // user/kernel switch
#define T_SWITCH_TOK 121 // user/kernel switch
然后,查看中断处理函数入口(vector.S),会发现所有中断处理函数都是trapentry.S中的__alltraps函数:
而__alltraps就是将各种寄存器压栈保存,然后调用trap.c中的trap函数,最后恢复各寄存器并返回,在trap.h中可发现压栈保存的数据结构:
/* registers as pushed by pushal */
struct pushregs {
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
};
struct trapframe {
struct pushregs tf_regs;
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
于是可发现,我们想要做特权级的切换,就是要修改cs、ds、es等段寄存器,而通过中断修改,最好的办法就是直接将保存在栈中这些数据修改,等返回时就会将寄存器调整为我们需要的值,实现权限切换。
然后需要注意的是,在由内核态切换成用户态的时候,一开始调用中断时,由于是从内核态调用的,没有权限切换,故ss、esp没有压栈,而iret返回时,是返回到用户态,故ss、esp会出栈,于是为了保证栈的正确性,需要在调用中断前将esp减8以预留空间,中断返回后,由于esp被修改,还需要手动恢复esp为正确值。
这样之后,系统特权级已经成功切换,但是由于切换到了用户态,导致IO操作没有权限,故之后的printf无法成功输出,为了能够正常输出,我们需要将eflags中的IOPL设成用户级别,即3,同样也是通过修改栈中值来达到修改的目的。
这个IOPL确实是让人无语至极,实验给的资料里完全没有提及过这个东西,但是没有设置这个,会导致在printf的时候走入自动中断,简直看不出来是什么原因导致的!
IOPL(EFLAGS寄存器的bits 12 and 13) [I/O privilege level field]:指示当前运行任务的I/O特权级(I/O privilege level),正在运行任务的当前特权级(CPL)必须小于或等于I/O特权级才能允许访问I/O地址空间。这个域只能在CPL为0时才能通过POPF以及IRET指令修改。
接下来是从用户态切换到内核态,此时由于调用中断时,是从用户态调用的,故会将ss、esp压栈,但是iret返回时,是返回到内核态,故ss、esp不会出栈,所以此时需要手动出栈,而ss由于维持了中断内部的ss,即已经是kernel,无需修改,故直接出栈esp即可。最后,为了能够让用户态的时候可以调用此中断,需要在idt的初始化函数中将该中断的DPL改成USER级别。
最后,再实现一下syscall,获得时钟计数值,syscall number采取255,于是便可以得到:
static int get_ticks(void) {
asm("mov $0xff, %eax");
asm("int $0x80");
}
而对于中断内部的处理代码就更简单了,直接在trap_dispatch的switch里面增加一个分支返回ticks即可。
最后在lab1_switch_test里面的kernel切到user后,再切回kernel前调用此函数进行测试,发现可正确获取ticks。
最后,附上整个lab1的实验报告和修改的代码。
lab1_result.zip