MIT 6.828 Lab 03:User Environments ( Part B )
PartB :Page Faults, Breakpoints Exceptions, and System Calls
Handling Page Faults
缺页中断中断号是14,发生时引发缺页中断的线性地址将会被存储到CR2寄存器中。
Exercise 05
修改trap_dispatch(),将页错误分配给page_fault_handler()处理。在trap_dispatch()添加如下代码:
static void
trap_dispatch(struct Trapframe *tf)
{
// Handle processor exceptions.
// LAB 3: Your code here.
if(tf->tf_trapno == T_PGFLT)
{
page_fault_handler(tf);
return;
}
// 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;
}
}
Exercise 06
修改trap_dispatch(),使得当断点异常发生时调用内核的monitor。在trap_dispatch()继续添加如下代码:
if(tf->tf_trapo == T_BRKPT)
{
monitor(tf);
return;
}
Question
- 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项。为什么?】
如果设置其DPL为0,则会产生GPF,因为用户程序跳转执行内核态程序。如果我们想要当前执行的程序能够跳转到这个描述符所指向的程序哪里继续执行的话,有个要求,就是要求当前运行程序的CPL,RPL的最大值需要小于等于DPL,否则就会出现优先级低的代码试图去访问优先级高的代码的情况,就会触发general protection exception。此处,如果设置 break point
的 DPL = 0
则会引发权限错误,由于这里设置的 DPL = 3
,所以会引发断点。
- What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?【尤其考虑到
user/softint
测试程序,你认为这些机制的关键点是什么?】
DPL的设置,可以限制用户态对关键指令的使用,有效地防止了一些程序恶意任意调用指令,引发一些危险的错误。
System calls
注意硬件不能产生int 0x30
中断,需要程序自行产生此中断,并且没有二义性。
应用程序通过寄存器传递系统调用号和系统调用参数。 这样,内核就不需要在用户环境的堆栈或指令流中获取参数。系统调用号将存放在%eax中,参数(最多五个)将分别位于%edx,%ecx,%ebx,%edi和%esi中。内核通过%eax传递返回值。
inc/syscall.c片段
asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");
"volatile"表示编译器不要优化代码,后面的指令 保留原样,其中"=a"表示"ret"是输出操作数; "i"=立即数;
最后一个子句告诉汇编器这可能会改变条件代码和任意内存位置。memory
强制gcc编译器假设所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。
Exercise 07
需要我们做如下几件事:
-
为中断号T_SYSCALL添加一个中断处理函数
-
在trap_dispatch()中判断中断号如果是T_SYSCALL,调用定义在kern/syscall.c中的syscall()函数,并将syscall()保存的返回值保存到tf->tf_regs.reg_eax等将来恢复到eax寄存器中。
🈲 这里的syscall()不是lib/syscall.c的syscallb,不要搞错了!
kern/syscall.c 中的 syscall : syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5) lib/syscall.c 中的 syscall : syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
-
修改kern/syscall.c中的syscall()函数,使能处理定义在inc/syscall.h中的所有系统调用。
trap_dispatch()
static void
trap_dispatch(struct Trapframe *tf)
{
// Handle processor exceptions.
// LAB 3: Your code here.
if(tf->tf_trapno == T_PGFLT)
{
page_fault_handler(tf);
return;
}
if(tf->tf_trapno == T_BRKPT)
{
monitor(tf);
return;
}
if(tf->tf_trapnp == 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;
}
// 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;
}
}
kern/syscall.c中的syscall()函数
c
// 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.
uint32_t ret;
switch(syscallno)
{
case SYS_cputs:
sys_cputs((char *)a1, (size_t)a2);
ret = 0;
break;
case SYS_cgetc:
ret = sys_cgetc();
break;
case SYS_getenvid:
ret = sys_getenvid();
break;
case SYS_env_destroy:
ret = sys_env_destroy((envid_t)a1);
break;
default:
return -E_INVAL;
}
return ret;
//panic("syscall not implemented");
}
系统调用流程回顾
现在回顾一下系统调用的完成流程:以user/hello.c为例,其中调用了cprintf(),注意这是lib/print.c中的cprintf,该cprintf()最终会调用lib/syscall.c中的sys_cputs(),sys_cputs()又会调用lib/syscall.c中的syscall(),该函数将系统调用号放入%eax寄存器,五个参数依次放入in DX, CX, BX, DI, SI,然后执行指令int 0x30,发生中断后,去IDT中查找中断处理函数,最终会走到kern/trap.c的trap_dispatch()中,我们根据中断号0x30,又会调用kern/syscall.c中的syscall()函数(注意这时候我们已经进入了内核模式CPL=0),在该函数中根据系统调用号调用kern/print.c中的cprintf()函数,该函数最终调用kern/console.c中的cputchar()将字符串打印到控制台。当trap_dispatch()返回后,trap()会调用env_run(curenv);
,该函数前面讲过,会将curenv->env_tf结构中保存的寄存器快照重新恢复到寄存器中,这样又会回到用户程序系统调用之后的那条指令运行,只是这时候已经执行了系统调用并且寄存器eax中保存着系统调用的返回值。任务完成重新回到用户模式CPL=3。
Exercise 08
hello.c程序如下,到此,系统成功执行cprintf("hello, world\n");
但是,卡在了第二句,由于之前没有设置 thisenv
的值,所以运行到 hello
的第二句时会出现错误,所以这里根据 id
取出索引,然后找到相应 env
。
// hello, world
#include <inc/lib.h>
void
umain(int argc, char **argv)
{
cprintf("hello, world\n");
cprintf("i am environment %08x\n", thisenv->env_id);
}
inc/env.h
#define ENVX(envid) ((envid) & (NENV - 1))
在lib/libmain.c中添加1句即可
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
//thisenv = 0;
thisenv= &envs[ENVX(sys_getenvid())];
Page faults and memory protection
操作系统依赖处理器的来实现内存保护。当程序试图访问无效地址或没有访问权限时,处理器在当前指令停住,引发中断进入内核。如果内核能够修复,则在刚才的指令处继续执行,否则程序将无法接着运行。系统调用也为内存保护带来了问题。大部分系统调用接口让用户程序传递一个指针参数给内核。这些指针指向的是用户缓冲区。通过这种方式,系统调用在执行时就可以解引用这些指针。但是这里有两个问题:
- 在内核中的page fault要比在用户程序中的page fault更严重。如果内核在操作自己的数据结构时出现 page faults,这是一个内核的bug,而且异常处理程序会中断整个内核。但是当内核在解引用由用户程序传递来的指针时,它需要一种方法去记录此时出现的任何page faults都是由用户程序带来的。
- 内核通常比用户程序有着更高的内存访问权限。用户程序很有可能要传递一个指针给系统调用,这个指针指向的内存区域是内核可以进行读写的,但是用户程序不能。此时内核必须小心的去解析这个指针,否则的话内核的重要信息很有可能被泄露。
Exercise 9
知识补充:DPL,RPL,CPL 之间的联系和区别
CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。
当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}
下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
针对上述两种问题,做以下两件事情:
- 首先如果页错误发生在内核态时应该直接panic。
- 实现kern/pmap.c中的user_mem_check()工具函数,该函数检测用户环境是否有权限访问线性地址区域[va, va+len)。然后对在kern/syscall.c中的系统调用函数使用user_mem_check()工具函数进行内存访问权限检查。
※ CS寄存器的低2位是RPL;&
——and操作,计算的时候按位计算,&两边操作数对应位上全为1时,结果的该位值为1。否则该位值为0
- 如果页错误发生在内核态时应该直接panic。
kern/trap.c中
// LAB 3: Your code here.
if(tf->tf_cs &3 ==0)
{
panic("page_fault in kernel mode, fault address %d\n", fault_va);
}
- 完成user_mem_check()
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
cprintf("user_mem_check va: %x, len: %x\n", va, len);
uint32_t begin = (uint32_t) ROUNDDOWN(va, PGSIZE); //没有强求page-aligned 那么就需要用ROUNDDOWN/ROUNDUP
uint32_t end = (uint32_t) ROUNDUP(va+len, PGSIZE);
uint32_t i;
for (i = (uint32_t)begin; i < end; i += PGSIZE)
{
pte_t *pte = pgdir_walk(env->env_pgdir, (void*)i, 0);
if ((i >= ULIM) || !pte || !(*pte & PTE_P) || ((*pte & perm) != perm))
{ //具体检测规则
user_mem_check_addr = (i < (uint32_t)va ? (uint32_t)va : i); //记录无效的线性地址,如果i比va小,那么就是va所在的页面的问题,但是还是返va
return -E_FAULT;
}
}
cprintf("user_mem_check success va: %x, len: %x\n", va, len);
return 0;
}
- 在kern/syscall.c中 检查调用系统system calls的参数
// Print a string to the system console.
// The string is exactly 'len' characters long.
// Destroys the environment on memory errors.
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,0);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}
- 修改debuginfo_eip,调用user_mem_check on usd, stabs, and stabstr.
debuginfo_eip 片段
// The user-application linker script, user/user.ld,
// puts information about the application's stabs (equivalent
// to __STAB_BEGIN__, __STAB_END__, __STABSTR_BEGIN__, and
// __STABSTR_END__) in a structure located at virtual address
// USTABDATA.
const struct UserStabData *usd = (const struct UserStabData *) USTABDATA;
// Make sure this memory is valid.
// Return -1 if it is not. Hint: Call user_mem_check.
// LAB 3: Your code here.
if(user_mem_check(curenv,(void *)USTABDATA, sizeof(struct UserStabData),0)<0)
{
return -1; //该memory不被允许访问
}
stabs = usd->stabs;
stab_end = usd->stab_end;
stabstr = usd->stabstr;
stabstr_end = usd->stabstr_end;
// Make sure the STABS and string table memory is valid.
// LAB 3: Your code here.
if(user_mem_checks(curenv,(void*)stabs,stab_end - stabs,0) || user_mem_check(curenv, (void *) stabstr, stabstr_end - stabstr, 0) < 0)
{
return -1;
}
}
- run user/breakpoint
(我这里好像没有出现libmain, 但是同样还是因为访问到了用户栈顶以上)
-> 借鉴博主的结果对比:
https://blog.csdn.net/scnu20142005027/article/details/51485616
- run user/evilhello
从字面意思即可理解,这里evilhello就是恶意程序,这里系统成功destroy了这个environment,并且没有panic