lab3:PartB

lab通关记录

MIT-6.828实验通关记录

B 部分:页面错误、断点异常和系统调用

改进异常代码,进行更细化的处理。

分别以下实现

  1. 增加页面错误处理函数
  2. 断点监视以便monitor使用
  3. 简单的系统调用实现
  4. 获取env_id的完善
  5. 缺页的简单处理

Exercise5

1.处理缺页

页错误异常,中断向量 14 ( T_PGFLT),是一个特别重要的异常,我们将在本实验和下一个实验中大量练习。当处理器发生页面错误时,它会将导致错误的线性(即虚拟)地址存储在特殊的处理器控制寄存器CR2 中。在trap.c 中, 我们提供了一个特殊函数的开头 page_fault_handler(),用于处理页面错误异常。

修改trap_dispatch() 以将页面错误异常分派到page_fault_handler(). 您现在应该能够make grade 在faultreadfaultreadkernelfaultwritefaultwritekernel测试中取得成功。如果其中任何一个不起作用,请找出原因并修复它们。请记住,您可以使用或 将JOS 引导到特定的用户程序中。例如,运行hello用户程序。 make run-xmake run-x-noxmake run-hello-nox。

修改trap_dispatch() 以将页面错误异常分派到page_fault_handler()

1
2
3
4
if (tf->tf_trapno == T_PGFLT) {//缺页中断
page_fault_handler(tf);
return;
}

2.断点监视

断点异常,中断向量 3 ( T_BRKPT),通常用于允许调试器通过用特殊的 1 字节int3软件中断指令临时替换相关程序指令来在程序代码中插入断点。

在 JOS 中,我们将稍微滥用此异常,将其转换为任何用户环境都可以用来调用 JOS 内核监视器的原始伪系统调用。

如果我们将 JOS 内核监视器视为原始调试器。例如,panic()lib/panic.c 中的用户模式实现,在int3显示其panic消息后执行。

Exercise6

trap_dispatch() 添加中断向量 3 ( T_BRKPT)的处理,让用户代码能够调用监视器

1
2
3
4
if (tf->tf_trapno == T_BRKPT) {
monitor(tf);
return;
}

问题:

  1. 断点测试用例将生成断点异常或一般保护错误,具体取决于您如何在 IDT 中初始化断点条目(即,您对SETGATE设置)。为什么?您需要如何设置它才能使断点异常按照上面指定的方式工作,哪些不正确的设置会导致它触发一般保护故障?

DPL必须设置成3.因为在中断判断时,必须判断RPL,CPL是优先级高于等于这个门描述符DPL。所以如果int3设置DPL为特权级0,那么就会出发故障。

更一般地说,CPL会更改来进入/退出内核态,RPL不会更改,所以当我们判断DPL时,DPL高特权就保证了用户不能访问内核代码和数据(压栈是内核里的中断代码指令,如果保护异常就会提前从中断返回)。因此本lab中只有int3的trap和syscallDPL设置为3,让用户能访问。

  1. ·你觉得中断机制哪部分最重要,考虑到user/softint测试程序的作用?

特权检查和保护现场

3.系统调用

用户进程通过调用系统调用来要求内核为它们做事。当用户进程调用系统调用时,处理器进入内核模式,处理器和内核协同保存用户进程的状态,内核执行适当的代码以进行系统调用,然后恢复用户进程。

在 JOS 内核中,我们将使用int 导致处理器中断的指令。特别是,我们将int $0x30 用作系统调用中断。我们已经T_SYSCALL为您定义了常量 为 48 (0x30)。您必须设置中断描述符以允许用户进程引起该中断。注意,中断 0x30 不能由硬件产生,所以不会因为允许用户代码产生而造成歧义。

应用程序将在寄存器中传递系统调用号和系统调用参数。这样,内核就不需要在用户环境的堆栈或指令流中四处游荡。系统调用号会去%eax,和参数(其中最多5个)将去%edx%ecx%ebx%edi,和%esi分别。内核将返回值传回%eax. 调用系统调用的汇编代码已为您编写, syscall()lib/syscall.c 中。你应该通读它并确保你理解发生了什么。

Exercise7

int $0x30 用作系统调用中断。已经T_SYSCALL为您定义了常量 为 48 (0x30)

1
2
3
4
5
6
7
8
TRAPHANDLER_NOEC(th_syscall, T_SYSCALL)//trapentry.S的中断预处理
SETGATE(idt[T_SYSCALL], 0, GD_KT, th_syscall, 3);//trap.c的trap_init()的IDT向量设置

if (tf->tf_trapno == T_SYSCALL) { //trap.c的trap_dispatch()用来跳转到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;
}

syscall():就是系统调用跳转表的简单实现

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
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.
int32_t ret;
switch (syscallno) { //eax保存的系统调用号
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;
}

4.获取env_id

用户程序开始运行在lib/entry.S的顶部 。经过一些设置后,此代码libmain()lib/libmain.c 中调用, 。您应该修改libmain()以初始化全局指针 thisenv以指向数组struct Env中的此环境 envs[]。(请注意,lib/entry.S已经定义envs 为指向UENVS您在 A 部分中设置的映射。)提示:查看inc/env.h并使用 sys_getenvid.

libmain()然后调用umain,在 hello 程序的情况下,它在 user/hello.c 中。请注意,在打印“ hello, world ”后,它会尝试访问 thisenv->env_id. 这就是它较早出错的原因。现在您已经thisenv正确初始化,它应该不会出错。如果它仍然有问题,您可能没有映射UENVS用户可读的 区域(回到pmap.c 的A 部分 ;这是我们第一次实际使用该UENVS区域)。

Exercise8

确保make grade 在hello测试中取得成功。

libmain()以初始化全局指针 thisenv以指向数组struct Env中的此环境 envs[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void
libmain(int argc, char **argv)
{
// set thisenv to point at our Env structure in envs[].
// LAB 3: Your code here.
envid_t envid = sys_getenvid(); //系统调用,最终会被syscall处理
thisenv = envs + ENVX(envid); //获取Env结构指针

// 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();
}

user/hello.c应该thisenv就有效了

1
2
3
4
5
6
void
umain(int argc, char **argv)
{
cprintf("hello, world\n");
cprintf("i am environment %08x\n", thisenv->env_id);
}

5.缺页

您现在将使用一种机制来解决这两个问题,该机制会检查从用户空间传递到内核的所有指针。当程序向内核传递一个指针时,内核将检查地址是否在地址空间的用户部分,以及页表是否允许内存操作。

因此,内核永远不会因为取消引用用户提供的指针而遭受页面错误。如果内核发生页面错误,它应该panic

Exercise9

更改kern/trap.c,如果页面错误在内核模式下发生的,panic。

提示:判断故障是发生在用户态还是内核态,检查tf_cs.

user_mem_assertkern/pmap.cuser_mem_check在同一个文件中实现。

kern/syscall.c更改为系统调用的完整性检查参数。

启动内核,运行user/buggyhello。环境应予以销毁,内核应该不会panic。你应该看到:

1
2
3
4
[00001000] va 00000001 的 user_mem_check 断言失败
[00001000] 免费环境 00001000
破坏了唯一的环境——无事可做!

最后,改变debuginfo_eipkern/ kdebug.c调用user_mem_checkusdstabsstabstr。如果您现在运行 user/breakpoint,应该能够backtrace从内核监视器运行 并在内核因页面错误而恐慌之前看到回溯遍历到lib/libmain.c

接下来给出代码:

更改kern/trap.c/page_fault_handler处理缺页中断:如果页面错误在内核模式下发生的应该panic

1
2
if ((tf->tf_cs & 3) == 0) 
panic("page_fault_handler():page fault in kernel mode!\n");

然后按照要求补全一些细节:

kern/pmap.c/user_mem_check()进行检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 start = (uint32_t) ROUNDDOWN(va, PGSIZE);
uint32_t end = (uint32_t) ROUNDUP(va+len, PGSIZE);
for (uint32_t i = start; 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);
return -E_FAULT;
}
}
cprintf("user_mem_check success va: %x, len: %x\n", va, len);
return 0;
}

kern/syscall.c进行我们的sys_cputs的检查curenv指针指向的函数调用

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.
user_mem_assert(curenv, s, len, 0);
// Print the string supplied by the user.
cprintf("%.*s", len, s);
}

Exercise10

启动内核,运行user/evilhello。你应该看到:

1
2
3
4
5
[00000000] 新环境 00001000
...
[00001000] va f010000c 的 user_mem_check 断言失败
[00001000] 免费环境 00001000

实际上细节补充完毕后实验10也完成了

总结一下我们的中断处理函数

1
2
3
4
5
6
7
8
9
10
11
12
1.trap_init初始化中断向量表

2.handler宏的预处理和_alltraps处理负责中断的保护现场:trapframe压栈,然后call trap

3.trap调用trap_dispatch
trap_dispatch负责中断向量跳转,实验只实现了syscall,缺页,trap,特权保护的跳转或直接处理

4.syscall负责调用内核函数,实验采用switch只实现了几个内核函数的call

handler->_alltraps->trap->trap_dispatch->syscall->sys_cputs->cprintf
->monitor
->page_fault_handler

总结一下整个流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
系统调用->(实际上进入了内核)中断处理->调用内核函数->调用后ret返回trap函数->env_run返回用户环境

一些问题
1.为什么有lib/syscall和kern/syscall分别?
因为是系统调用和中断处理跳转内核函数的区别

2.为什么压栈保护现场要分两次?
因为第一次压栈有错误码的分别,第二次cs,ds段寄存器和通用寄存器,节省代码量,并且通用寄存器这样的部分trapframe还没存储

3.为什么trap需要保存栈的trapframe到env的trapframe结构?已经有栈不行吗?
因为当fork时候也同样要从中断结尾处返回,因此设计了env_run,在fork后和trap结尾都被调用,通过env的trapframe“返回”用户态,从而保证了合理。对于栈的trapframe就忽略,也是跟函数返回的不同。

4.什么是CPL,RPL,DPL?
RPL是选择子的末2位,CPL就是cs寄存器(存放代码段选择子)的末两位,所以后者是前者的特例。并且当我们从用户态进入内核RPL认为是调用门的特权级,会被区分。DPL是描述符的特权级。我们要求max{CPL,RPL}和DPL比较。举个例子,当我们中断处理时,中断是内核的函数,CPL=0,但是RPL=3,所以我们中断判断特权才要让int和syscall保持DPL为3,否则会就会产生保护出错,exercise6问题就是问这个的。

5.CPL和DPL和PTE_X有什么区别?不都是特权保护?
对,但是还是有不同。由于我们跟linux一样,JOS放弃了分段管理转向了分页管理。但是硬件强行要求分段。所以段选择子,段描述符里的特权保留,但是描述符里的地址(16位)全部都是0.因此当我们必须规定在分页管理下页的访问权限(PTE_U这样),至于CPL,RPL,DPL权限检查,除了在中断时候有用(因为没访问页所以可以用得上),其他情况下设置不应该影响分页的检查才行。具体来说,16位的访问段大小实在是太小了,所以除了访问GDT,IDT的响应检查等以外。其他的情况下比如guard page,对于用户的特权级就是通过,但是PTE检查就是失败,无论是内核还是用户都会经历这样的过程。从而合理。

6.用户访问内存为什么会中断?
并不是读写内存就会中断,大部分情况下是你使用系统调用才会中断,你对text,data,栈,堆读写都不会中断。甚至如果你绕过对文件的读写的系统调用,使用mmap直接映射到用户空间,那么你的读写也不会有系统调用,也就不会中断了。
不要弄混特权检查和中断,是中断需要对特权检查来保证用户进入内核后的约束,而特权检查是访问内存的必经之路。

代码在这:Source Code

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2020-2024 环烷烃
  • Visitors: | Views:

我很可爱,请我喝一瓶怡宝吧~

支付宝
微信