0%

MIT-6.828-JOS 笔记 - Lab 3: User Environments

本实验将建立一个用户环境、建立异常处理机制并通过异常处理机制提供系统调用能力。

本实验代码:https://github.com/epis2048/MIT_6.828_2018/tree/lab3

一、用户环境

这里的用户环境类似UNIX进程的概念,主要是为进程虚拟化一个单独的空间。
JOS内核维护了三个全局变量,用于描述用户环境:

1
2
3
struct Env *envs = NULL
struct Env *curenv = NULL
static struct Env *env_free_list

类似Lab2中物理内存的管理方式,envs指向一个ENV结构的数组,curenv指向当前正在运行的环境,env_free_list指向一个ENV结构的链表,保存未在运行的环境。ENV结构定义在inc/env.h中:

1
2
3
4
5
6
7
8
9
10
struct Env {
struct Trapframe env_tf; // 寄存器快照
struct Env *env_link; // 链表的链
envid_t env_id; // 唯一的ID
envid_t env_parent_id; // 父环境ID
enum EnvType env_type; // 类别,大部分都是USER
unsigned env_status; // 当前状态
uint32_t env_runs; // 该环境已运行的次数
pde_t *env_pgdir; // 页目录地址
};

Exercise 1
先修改mem_init()中的代码,使之在分配完pages数组之后分配envs数组,并在UPAGES后映射UENVS。
思路和lab2中的pages数组的分配一样,在mem_init()分配完pages数组后,添加如下语句:

1
2
3
4
5
// kern/pmap.c
// LAB 3: Your code here.
// 为ENVS数组分配空间
envs = (struct Env*) boot_alloc(sizeof(struct Env) * NENV);
memset(envs, 0, sizeof(struct Env) * NENV);

还要映射UENVS

1
2
3
4
// kern/pmap.c
// Your code goes here:
// 将虚拟地址的UPAGES映射到物理地址pages数组开始的位置
boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);

Exercise 2
实现如下几个函数:
env_init():初始化envs数组,构建env_free_list链表。要求env_alloc返回env[0],这里可以使用头插法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// kern/env.c
void
env_init(void)
{
// Set up envs array
// LAB 3: Your code here.
env_free_list = NULL;
for(int i = NENV - 1; i >= 0; i--){
envs[i].env_id = 0;
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}
// Per-CPU part of the initialization
env_init_percpu();
}

env_setup_vm():分配一个物理页给Env用作页目录,该页目录继承自内核的页目录。帮助完成当前用户线性地址空间向物理地址空间的映射。

1
2
3
4
5
// kern/pmap.c
// 将刚分配的物理页用作页目录,并让其从内核页目录继承数据
p->pp_ref++;
e->env_pgdir = (pde_t *) page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);

region_alloc():在e指向的用户环境中,为va开头,长度为len的地址分配物理空间。主要使用Lab2中的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// kern/pmap.c
static void
region_alloc(struct Env *e, void *va, size_t len)
{
void *begin = ROUNDDOWN(va, PGSIZE), *end = ROUNDUP(va+len, PGSIZE); // 以PGSIZE取整
while(begin < end){
struct PageInfo * p = page_alloc(0);
if(p == NULL){
panic("region_alloc: Out of Memory!\n");
}
page_insert(e->env_pgdir, p, begin, PTE_W | PTE_U);
begin += PGSIZE;
}
}

load_icode():加载ELF文件到Env中。该函数传入了一个ELF文件的首地址(内存)。需要先判断是否是ELF文件,然后为每个Segment分配空间,最后将该ENV的EIP指向程序入口。

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
// kern/pmap.c
static void
load_icode(struct Env *e, uint8_t *binary)
{
// LAB 3: Your code here.
struct Elf * ELF = (struct Elf *) binary;
struct Proghdr * ph;
int ph_num;
if(ELF->e_magic != ELF_MAGIC){ // 判断格式是否是ELF
panic("Binary is not ELF format! \n");
}

ph = (struct Proghdr *) ((uint8_t *)ELF + ELF->e_phoff); // ph是程序头距离ELF的偏移
ph_num = ELF->e_phnum;
lcr3(PADDR(e->env_pgdir)); // 切换到当前用户环境的页目录表
for(int i = 0; i < ph_num; i++){
if(ph[i].p_type == ELF_PROG_LOAD){ // 只加载Load类型的Segment
if (ph->p_filesz > ph->p_memsz) {
panic("load_icode: file size is greater than memory size");
}
region_alloc(e, (void*)ph[i].p_va, ph[i].p_memsz); // 给每个Segment分配物理空间
memset((void*)ph[i].p_va, 0, ph[i].p_memsz);
memcpy((void*)ph[i].p_va, binary + ph[i].p_offset, ph[i].p_filesz);
}
}
e->env_tf.tf_eip = ELF->e_entry; // EIP指向程序入口

// LAB 3: Your code here.
// 分配初始栈空间
region_alloc(e, (void*)(USTACKTOP - PGSIZE), PGSIZE);
lcr3(PADDR(kern_pgdir)); // 切换回系统的页目录表
}

env_create():从env_free_list链表拿一个Env结构,加载从binary地址开始处的ELF可执行文件到该Env结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
// kern/pmap.c
void
env_create(uint8_t *binary, enum EnvType type)
{
// LAB 3: Your code here.
struct Env * e;
int r = env_alloc(&e, 0);
if(r < 0){
panic("env_create: Create Env failed: %e", r);
}
e->env_type = type;
load_icode(e, binary);
}

env_run():开始运行一段Env。如果当前有正在运行的,则把他挂起。

1
2
3
4
5
6
7
8
9
10
11
12
13
void
env_run(struct Env *e)
{
// LAB 3: Your code here.
if(curenv != NULL && curenv->env_status == ENV_RUNNING){ //如果当前有运行的Env,则挂起
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
e->env_status = ENV_RUNNING;
e->env_runs++;
lcr3(PADDR(e->env_pgdir)); // 加载当前Env的线性地址到分页寄存器
env_pop_tf(&e->env_tf); // 从栈中取tf结构
}

这些函数的调用关系:
env_create()
–>env_alloc()
—->env_setup_vm()
–>load_icode()
—->region_alloc()

二、中断和异常

第一部分中完成了用户环境的基本代码,下面开始完成中断异常的处理程序。

Exercise 4
首先需要修改一段汇编代码:trapentry.S。使用TRAPHANDLER和TRAPHANDLER_NOEC宏创建0~16号中断的中断处理函数。TRAPHANDLER和TRAPHANDLER_NOEC创建的函数都会跳转到_alltraps处,这里参考inc/trap.h中的Trapframe结构,tf_ss,tf_esp,tf_eflags,tf_cs,tf_eip,tf_err在中断发生时由处理器压入,所以现在只需要压入剩下寄存器(%ds,%es,通用寄存器)。然后将%esp压入栈中(也就是压入trap()的参数tf)。

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
34
35
// kern/trapentry.S
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
TRAPHANDLER_NOEC(th0, 0)
TRAPHANDLER_NOEC(th1, 1)
TRAPHANDLER_NOEC(th3, 3)
TRAPHANDLER_NOEC(th4, 4)
TRAPHANDLER_NOEC(th5, 5)
TRAPHANDLER_NOEC(th6, 6)
TRAPHANDLER_NOEC(th7, 7)
TRAPHANDLER(th8, 8)
TRAPHANDLER_NOEC(th9, 9)
TRAPHANDLER(th10, 10)
TRAPHANDLER(th11, 11)
TRAPHANDLER(th12, 12)
TRAPHANDLER(th13, 13)
TRAPHANDLER(th14, 14)
TRAPHANDLER_NOEC(th16, 16)

/*
* Lab 3: Your code here for _alltraps
*/
//参考inc/trap.h中的Trapframe结构。tf_ss,tf_esp,tf_eflags,tf_cs,tf_eip,tf_err在中断发生时由处理器压入,所以现在只需要压入剩下寄存器(%ds,%es,通用寄存器)
//切换到内核数据段
_alltraps:
pushl %ds
pushl %es
pushal
pushl $GD_KD
popl %ds
pushl $GD_KD
popl %es
pushl %esp //压入trap()的参数tf,%esp指向Trapframe结构的起始地址
call trap //调用trap()函数

然后修改trap_init(),目的是创建IDT(中断向量表)。

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
// kern/trap.c
// LAB 3: Your code here.
void th0();
void th1();
void th3();
void th4();
void th5();
void th6();
void th7();
void th8();
void th9();
void th10();
void th11();
void th12();
void th13();
void th14();
void th16();
SETGATE(idt[0], 0, GD_KT, th0, 0); //格式如下:SETGATE(gate, istrap, sel, off, dpl),定义在inc/mmu.h中
SETGATE(idt[1], 0, GD_KT, th1, 0); //设置idt[1],段选择子为内核代码段,段内偏移为th1
SETGATE(idt[3], 0, GD_KT, th3, 3);
SETGATE(idt[4], 0, GD_KT, th4, 0);
SETGATE(idt[5], 0, GD_KT, th5, 0);
SETGATE(idt[6], 0, GD_KT, th6, 0);
SETGATE(idt[7], 0, GD_KT, th7, 0);
SETGATE(idt[8], 0, GD_KT, th8, 0);
SETGATE(idt[9], 0, GD_KT, th9, 0);
SETGATE(idt[10], 0, GD_KT, th10, 0);
SETGATE(idt[11], 0, GD_KT, th11, 0);
SETGATE(idt[12], 0, GD_KT, th12, 0);
SETGATE(idt[13], 0, GD_KT, th13, 0);
SETGATE(idt[14], 0, GD_KT, th14, 0);
SETGATE(idt[16], 0, GD_KT, th16, 0);

三、异常处理和系统调用

1. Handling Page Faults

缺页中断中断号是14,发生时引发缺页中断的线性地址将会被存储到CR2寄存器中。

Exercise 5
修改trap_dispatch(),在发生trap时判断类型,如果是缺页中断则调用page_fault_handler()。

1
2
3
4
5
6
7
// kern/trap.c
// LAB 3: Your code here.
// 缺页中断
if (tf->tf_trapno == T_PGFLT) {
page_fault_handler(tf);
return;
}

2. The Breakpoint Exception

断点异常中断号是3,调试器常常插入一字节的int3指令临时替代某条指令,从而引发断点异常。

Exercise 6
修改trap_dispatch(),使得当断点异常发生时调用内核的monitor。

1
2
3
4
5
6
// kern/trap.c
// 断点异常
if (tf->tf_trapno == T_BRKPT) {
monitor(tf);
return;
}

3. System calls

x86使用int指令实现系统调用,使用0x30作为中断号。应用使用寄存器传递系统调用号和参数。系统调用号保存在%eax,五个参数依次保存在%edx, %ecx, %ebx, %edi, %esi中。返回值保存在%eax中。

Exercise 7
需要我们做如下几件事:

  1. 为中断号T_SYSCALL添加一个中断处理函数
  2. 在trap_dispatch()中判断中断号如果是T_SYSCALL,调用定义在kern/syscall.c中的syscall()函数,并将syscall()保存的返回值保存到tf->tf_regs.reg_eax等将来恢复到eax寄存器中。
  3. 修改kern/syscall.c中的syscall()函数,使能处理定义在inc/syscall.h中的所有系统调用。

先分别在trapentry.S和trap.c的trap_init()函数中添加系统调用代码。

1
2
// kern/strapentry.S
TRAPHANDLER_NOEC(th_syscall, T_SYSCALL)
1
2
3
// kern/trap.c
void th_syscall();
SETGATE(idt[T_SYSCALL], 0, GD_KT, th_syscall, 3);

然后修改trap_dispatch()

1
2
3
4
5
6
// kern/trap.c
if (tf->tf_trapno == T_SYSCALL) { // 系统调用,从寄存器中取出系统调用号和五个参数,传给kern/syscall.c中的syscall(),并将返回值保存到tf->tf_regs.reg_eax
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
// kern/syscall.c
int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
int32_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;
}

系统调用过程:以用户程序中的cprintf为例,该函数调用lib/printf.c中的代码,然后调用lib/syscall.c中的汇编语言,汇编语言使用int 30h命令,出发trap,进而进入内核态,由trap_dispatch函数决定调用哪个函数进行返回。最终系统处理完成后,由trap()函数调用env_run(),回到用户程序。

4. User-mode startup

Exercise 8
根据JOS的设计,用户程序在运行前都会走libmain()函数,该函数调用用户的umain()函数。故在该函数中,还需要增加一次系统调用,获取当前的envid。

1
2
3
4
// lib/libmain.c
// LAB 3: Your code here.
envid_t envid = sys_getenvid(); //系统调用,我们已经在Exercise 7中实现了
thisenv = envs + ENVX(envid); //获取Env结构指针

5. Page faults and memory protection

最后,缺页中断和内存保护还有些问题。缺页中断应该只能处理用户程序的缺页,而系统缺页则不能处理。当发生系统缺页时,应发出panic。此外,要检查用户给定的指针,不能出现越界问题。

Exercise 9
在page_fault_handler()函数中,对发生中断时程序的运行状态做判断即可,若处于内核态,则说明发生了系统缺页。

1
2
3
4
// kern/trap.c
// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0) //内核态发生缺页中断直接panic
panic("page_fault_handler():page fault in kernel mode!\n");

内存保护则包含如下两个函数:user_mem_assert()用于调用user_mem_check(),当user_mem_check()返回越界时,该函数终止程序。

1
2
3
4
5
6
7
8
9
10
11
// kern/pmap.c
// 内存范围检查,防止用户态程序访问内核
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_check():分别判断程序请求地址的所属页的权限,若不允许用户读写,则返回越界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// kern/pmap.c
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);
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); //记录无效的那个线性地址
return -E_FAULT;
}
}
cprintf("user_mem_check success va: %x, len: %x\n", va, len);

return 0;
}

有了工具函数,我们看kern/syscall.c中的系统调用函数只有sys_cputs()参数中有指针,所以需要对其进行检测。

1
2
3
4
5
6
7
8
9
10
// kern/syscall.c
static void
sys_cputs(const char *s, size_t len)
{
// LAB 3: Your code here.
user_mem_assert(curenv, s, len, 0);

// Print the string supplied by the user.
cprintf("%.*s", len, s);
}

总结

本实验大概做了三件事:

  1. 进程建立,可以加载用户ELF文件并执行。
  2. 创建异常处理函数,建立并加载IDT,使JOS能支持中断处理。
  3. 利用中断机制,使JOS支持系统调用。