Mit6.828 Day25 Lab3 PartB Page Faults, Breakpoints Exceptions, and System Calls

Mit6.828 Day25 Lab3 PartB Page Faults, Breakpoints Exceptions, and System Calls

摘要:MIT6.828 Lab3 PartB

0x01 写在前面

这个部分我们将通过完善异常处理,包括标题提到的页面错误、断点异常和系统调用。我准备花1-2天的时间完成这Part的内容(希望吧。

0x02 Part B: Page Faults, Breakpoints Exceptions, and System Calls

Handling Page Faults

页错误(中断向量为14)是非常重要的,其发生时,处理器会将导致该错误的线性地址存储到CR2寄存器中。

Exercise 5

这个exercise需要我们去修改trap_dispatch()函数来捕获页错误给page_fault_handler()。同时,提醒我们说我们可以通过格式make run-name-of-the-user-env-nox 来开启我们的JOS并调用对应的用户程序。

这个部分其实蛮好写的,首先根据我们在PartA部分在trapentry.S中所看到的那两个函数,其会将中断号压入到堆栈中,而我们压入到堆栈最后的组成其实就是Trapframe这个结构体,在这个结构体中,我们可以得知trapno就是我们需要的中断号。

题目要求我们处理缺页中断,但事实上,一个操作系统中有许多的中断需要处理,所以我们的逻辑是:根据switch语句来判断我们所需要处理的中断从而调用对应的handler来进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch(tf->tf_trapno){
case (T_PGFLT):
page_fault_handler(tf);
break;
default:
// 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;
}
}

尝试运行make grade可以发现那几个需要得分的部分过了:

The Breakpoint Exception

该异常(中断号为3)一般用于调试器去设置一个暂时代替int3指令的断点。

Exercise 6

这个exercise要求我们去修改trap_dispatch()函数以实现断点异常。

这个练习其实跟上一个差不多,只不过根据exercise的描述:

In JOS we will abuse this exception slightly by turning it into a primitive pseudo-system call that any user environment can use to invoke the JOS kernel monitor.

也就是说,我们只需要在case里面调用monitor来处理即可。在trap_dispatch()中添加实现代码如下:

1
2
3
case (T_BRKPT):
monitor(tf);
break;

Question

3.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?

4.What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

对于question3,我这边是没有出现的,一开始便是正常的break point exception,在网上查询资料,仍然是DPL的问题,在于trap_init()中SETGATE的第四个参数,如果将其设置为0,则会触发general protection exception。这是由于如果我们想要将程序跳转到描述符所指向的位置继续执行的话,必须要是的CPL、RPL的值小于等于DPL,而在用户态中,CPL、RPL的值必大于0,故会报出general protection exception。

对于question4,主要还是DPL的设置,用以限制用户环境下对敏感指令的使用。

System calls

用户程序如果需要内核去帮助它们做一些事,需要进行系统调用。

中断号为48, int 0x30指令调用

JOS以及为我们在lib/syscall.c中写好了系统调用的处理程序,我们一起看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
int32_t ret;

// Generic system call: pass system call number in AX,
// up to five parameters in DX, CX, BX, DI, SI.
// Interrupt kernel with T_SYSCALL.
//
// The "volatile" tells the assembler not to optimize
// this instruction away just because we don't use the
// return value.
//
// The last clause tells the assembler that this can
// potentially change the condition codes and arbitrary
// memory locations.

asm volatile("int %1\n"
: "=a" (ret)
: "i" (T_SYSCALL),
"a" (num),
"d" (a1),
"c" (a2),
"b" (a3),
"D" (a4),
"S" (a5)
: "cc", "memory");

if(check && ret > 0)
panic("syscall %d returned %d (> 0)", num, ret);

return ret;
}

由教案中所言,传入了6个重要参数,从中断号到其余的五个参数依次进入eax、edx、ecx、ebx、edi、esi寄存器。

而后有一个校验的过程,防止该系统调用更改了返回地址。

Exercise 7

为中断向量T_SYSCALL在内核中增加一个处理器,有三个具体的任务需要完成:

1)我们需要编辑trapentry.S和kern/trap.c的trap_init();

2)而后我们还需要在trap_dispatch()中通过添加system()以处理响应的系统中断;

3)最后,我们需要实现syscall()以确保如果系统调用号异常时返回-E_INVAL,并通过调用相关的内核函数处理好所有在inc/syscall.h中列好的系统调用。

首先我们要完成第一个任务,我发现我在PartA就已经增加了具体的代码:

1
2
3
4
// trapentry.S中
TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL)
// trapinit中
SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3);

而后第二个任务,根据我们上面对传入参数的分析,添加以下代码即可:

1
2
3
4
case (T_SYSCALL):
ret = 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);
tf->tf_regs.reg_eax = ret;
break;

这边需要我们注意的是,我们需要调用的其实是kern/system.c,因此最后的参数形式也是按照里面的来设置。

最后,来到kern/system.c,首先根据提示在sys_cputs()中增加我们所需要的权限验证代码:

1
2
3
4
5
6
7
8
9
10
11
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.
uesr_mem_assert(curenv, s, len, 0);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}

而后,田中syscall中的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.

//panic("syscall not implemented");

switch (syscallno) {
case (SYS_cputs):
sys_cputs((const char *)a1, a2);
return 0;
case (SYS_cgetc):
return sys_cgetc();
case (SYS_getenvid):
return sys_getenvid();
case (SYS_env_destroy):
return sys_env_destroy(a1);
default:
return -E_INVAL;
}
}

尝试调用hello进程,成功打印hello world而后发生缺页中断:

challenge 部分跳过

User-mode startup

在JOS中,用户程序在lib/entry.S的顶部开始运行,而后调用在lib/libmain.c中的libmain(),在这里,我们需要初始化全局指针thisenv并将其指向envs[]数组。

再后libmain()会调用umain,之前的错误之所以产生,是由于hello.c中的umain尝试去访问了thisenv->env_id,而如果thisenv被设置好了,该错误就消失了。

Exercise 8

这个exercise做的其实就是上面的事。

我们先来看看lib/entry.S吧:

没啥好解读的,就是一开始设置了各种同进程相关的环境变量,而后调用了libmain。

跳到libmain()中:

其实很简单,我们只需要将当前的环境赋给thisenv即可,代码如下,ENVX用以通过envid获取对应的envx索引,定义于env.h中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
libmain(int argc, char **argv)
{
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
thisenv = &envs[ENVX(sys_getenvid())];

// save the name of the program so that panic() can use it
if (argc > 0)
binaryname = argv[0];

// call user main routine
umain(argc, argv);

// exit gracefully
exit();
}

make grade hello部分得分:

Page faults and memory protection

绝大部分的系统调用接口让用户程序传递一个指针给内核,这些指针指向需要读和写的用户缓冲区,内核在进行系统调用时解析这些指针。

有两个问题:

  • 如果在内核中发生了页错误,应该记住导致这些错误的解析;
  • 用户有可能传入用户不可读但是内核可读的地址从而造成任意读取。

因此,在接受用户程序的指针时,我们会仔细地去检查这些指针指向的地址空间

Exercise 9

这个Exercise有以下几个任务:

1)修改kern/trap.c实现当缺页错误在内核模式发生时警报;

2)阅读kern/pmap.c中的user_mem_assert并实现user_mem_check

3)修改kern/syscall.c以检查传递给系统调用的参数的完整性

4)修改kern/kdebug.c中的debuginfo_eip以调用user_mem_check检查usd,stabs和stabstr的合法性。

我们一个个来完成这些任务。首先是第一个:

要求我们对发生在内核模式下的缺页错误进行警报,那么如何判断当前是否处在内核模式下呢?教案中其实已经给了我们提示了,让我们去查看CS寄存器的低位。事实上应该根据CS寄存器的低两位——也就是CPL位来进行判断。我们知道,内核模式和用户模式最大的区别在于权限,内核模式的权级在于0级,用户模式的权级在于3,故我们可以借此来进行判断。

所以代码的书写其实就很简单了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void
page_fault_handler(struct Trapframe *tf)
{
uint32_t fault_va;

// Read processor's CR2 register to find the faulting address
fault_va = rcr2();

// Handle kernel-mode page faults.

// LAB 3: Your code here.
if((tf->tf_cs & 3)==0)
panic("Page fault in kernel mode happened in %08x\n", fault_va);
// We've already handled kernel-mode exceptions, so if we get here,
// the page fault happened in user mode.

// Destroy the environment that caused the fault.
cprintf("[%08x] user fault va %08x ip %08x\n",
curenv->env_id, fault_va, tf->tf_eip);
print_trapframe(tf);
env_destroy(curenv);
}

来到第二个任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Checks that environment 'env' is allowed to access the range
// of memory [va, va+len) with permissions 'perm | PTE_U | PTE_P'.
// If it can, then the function simply returns.
// If it cannot, 'env' is destroyed and, if env is the current
// environment, this function will not return.
//
void
user_mem_assert(struct Env *env, const void *va, size_t len, int perm)
{
if (user_mem_check(env, va, len, perm | PTE_U) < 0) {
cprintf("[%08x] user_mem_check assertion failure for "
"va %08x\n", env->env_id, user_mem_check_addr);
env_destroy(env); // may not return
}
}

其实之前已经使用过这个user_mem_assert这个函数了,只不过未实现完成的user_mem_check总是会返回0让if判断通过罢了。如今我们只需要填充user_mem_check即可,根据注释里的提示。我们逻辑很简单,找到环境页目录中对应的页表项,遍历其中的页来对每个页进行判断。这里用到了我们之前写好的pgdir_walk()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
// LAB 3: Your code here.
char * end = NULL;
char * start = NULL;
end = ROUNDUP((char*)(va+len), PGSIZE);
start = ROUNDDOWN((char*)va, PGSIZE);
pte_t *curpg = NULL;
for(;start<end;start+=PGSIZE){
curpg = pgdir_walk(env-env_pgdir, (void *)start, 0);
if(curpg == NULL|| (size_t)start>ULIM || ((uint32_t)(*curpg) & perm) != perm){
// user_mem_check_addr = (uintptr_t)va; wrong
if(start == ROUNDDOWN((char*)va, PGSIZE))
user_mem_check_addr = (uintptr_t)va;
else
user_mem_check_addr = (uintptr_t)start;
return -E_FAULT;
}


}
return 0;
}

第三步之前已经加过,这边不再添加。

来到最后一步,首先找到对应的函数,而后添加如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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, usd, sizeof(usd), PTE_U)<0)
return -1;
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_check(curenv, stabs, stab_end-stabs, PTE_U)<0
|| user_mem_check(curenv, stabstr, stabstr_end-stabstr, PTE_U)<0){
return -1;
}

make grade一下,通过:

0x03 总结

PartA主要是设置各种中断的入口,PartB则是在具体如何去处理对应的中断上下功夫,在本次Lab中主要是system calls,breakpoint和page fault。


评论