Lab3 - User Environments需要实现基本的内核功能,使得一个受保护的用户环境(进程)可以运行。在本实验中,需要让内核设置数据结构以追踪用户环境,创建一个单用户环境,加载程序镜像并执行。此外,内核还需要能处理用户环境的系统调用以及引发的异常。
2018年2月26日,完成了实验并写完了报告。

实验准备

根据官网,切换到分支lab3并且合并分支lab2。在合并的过程中,发生了冲突。

1
2
user% git checkout -b lab3 origin/lab3
user% git merge lab2

根据提示,是kern/monitor.c中发生了冲突,因为仅有一处冲突,手动编辑kern/monitor.c文件,并commit,即可解决冲突并合并分支。

实验三包括如下的新文件:

  • inc/env.h 用户模式环境的公有定义
  • inc/trap.h 陷阱处理的公有定义
  • inc/syscall.h 用户环境对内核的系统调用的公有定义
  • inc/lib.h 用户模式支持的库公有定义
  • kern/env.h 用户模式环境的内核私有定义
  • kern/env.c 用户模式环境的内核代码实现
  • kern/trap.h 内核私有的陷阱处理定义
  • kern/trap.c 陷阱处理代码
  • kern/trapentry.S 汇编语言的陷阱处理程序入口
  • kern/syscall.h 系统调用处理的内核私有定义
  • kern/syscall.c 系统调用实现代码
  • lib/Makefrag 用户模式库obj/lib/libjos.a的Makefile
  • lib/entry.S 用户环境的汇编语言入口
  • lib/libmain.c entry.S调用的用户模式库安装代码
  • lib/syscall.c 用户模式系统调用的打桩函数
  • lib/console.c 用户模式的putchar和getchar实现,提供了控制台IO
  • lib/exit.c 用户模式的exit实现
  • lib/panic.c 用户模式的panic实现
  • user/* 检验内核实验三代码的测试程序

此外,lab2中的一些文件在lab3中也被添加了新的内容,可以用git diff lab2查看具体的比较信息。

内联汇编

本实验中可能会用到GCC的内联汇编特性,应当至少理解给出的代码中的内联汇编代码片段。

实验过程

第一部分 User Environments and Exception Handling - 用户环境和错误处理

inc/env.h中给出了用户环境的基本定义。内核通过struct Env追踪每一个用户环境。本实验中只需要创建一个环境,然而你需要设计JOS内核实际支持多用户环境。实验四中,你将通过允许一个用户环境fork别的用户环境来利用多用户环境的特性。

kern/env.c中可以看到,内核管理3个与环境有关的全局变量:

1
2
3
struct Env *       envs   = NULL;    // All environments
struct Env * curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list

当JOS成功运行之后,envs指针指向一个struct Env的数组,代表了系统中所有的环境。在设计上,JOS内核允许NNEV个同时激活的环境,NNEV在inc/env.h中定义。
JOS内核使用env_free_list维护所有未激活的struct Env,这样的设计简化的环境的分配和释放,它们仅仅需要从该链表上添加或移除。
JOS内核使用curenv去追踪在任意时刻当前正在执行的环境。在启动之后到第一个环境运行的时间段中,curenv被初始化为NULL

环境状态

struct Envinc/env.h中被定义:

1
2
3
4
5
6
7
8
9
10
11
12
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run

// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};

  • env_tf - struct TrapFrameinc/trap.h中被定义,表示了当环境不运行时被保存的寄存器值,主要用于上下文切换
  • env_link - 指向了env_free_list上的下一个struct Envenv_free_list指向了链表中的第一个空闲环境
  • env_id - 唯一标识当前正在使用struct Env的环境。当环境终止后,struct Env可能被内核重新分配用于另一个不同的环境,但它们的env_id是不同的
  • env_parent_id - 存储了创建该环境的环境的env_id,通过该方式构建一个环境树,用于安全方面的决策
  • env_type - 用于区分特殊环境,对于大部分环境,该值为ENV_TYPE_USER,在后续Lab中会介绍其他的值
  • env_status - 状态
    • ENV_FREE - 表明struct Env处于空闲状态,应当位于env_free_list
    • ENV_RUNNABLE - 表明struct Env代表的环境正等待运行于处理器上
    • ENV_RUNNING - 表明struct Env代表的环境为正在运行的环境
    • ENV_NOT_RUNNABLE - 表明struct Env代表了一个正在运行的环境,但却没有准备好运行,如正在等待另一个环境的IPC(进程间通信)
    • ENV_DYING - 表明struct Env代表了一个僵死环境,僵死环境将在下一次陷入内核时被释放(直到Lab4才会使用该Flag)
  • env_pgdir - 保存了环境的页目录的内核虚拟地址

JOS中环境的概念综合了“线程”和“地址空间”,“线程”由env_tf域的被保存的寄存器值定义,“地址空间”由env_pgdir域指向的页目录和页表定义。为了运行一个环境,内核必须用保存的寄存器值和合适的地址空间设置CPU。

JOS的struct Env和xv6的struct proc很像,两种结构体都持有环境的用户模式寄存器状态(通过struct TrapFrame),然而,JOS中,独立的环境并不具有不同的内核栈,因为JOS内核中同时只能有一个运行的JOS环境,因此JOS只需要一个内核栈。

分配环境数组

练习1的代码如下,仅供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//////////////////////////////////////////////////////////////////////
// Make 'envs' point to an array of size 'NENV' of 'struct Env'.
// LAB 3: Your code here.
// get size of envs
uint32_t envs_size = sizeof(struct Env) * NENV;
// use boot_alloc to allocate memory
envs = (struct Env *)boot_alloc(envs_size);
// initialization
memset(envs, 0, envs_size);

//////////////////////////////////////////////////////////////////////
// Map the 'envs' array read-only by the user at linear address UENVS
// (ie. perm = PTE_U | PTE_P).
// Permissions:
// - the new image at UENVS -- kernel R, user R
// - envs itself -- kernel RW, user NONE
// LAB 3: Your code here.
boot_map_region(kern_pgdir, UENVS, envs_size, PADDR(envs), PTE_U | PTE_P);

创建并运行环境

现在将在kern/env.c中编写必要的代码去运行用户环境。由于JOS内核还不支持文件系统,所以只能配置内核以加载一个嵌入内核中静态二进制镜像。JOS内核将这个二进制镜像以ELF可执行镜像格式嵌入。

Lab3的GNUMakefileobj/user/目录下生成了一些二进制镜像。kern/Makefrag下可以看到,链接器的-b binary选项使得这些文件以原始的未被翻译二进制文件而非普通的被编译器生成的.o文件的方式链接, 通过-b binary方式链接的二进制文件就链接器而言可以为任意类型,甚至是文本文件或是图片。

如果在构建内核后观察obj/kern/kernel.sym,可以看到链接器生成了一些“奇怪”名字的符号如_binary_obj_user_hello_start_binary_obj_user_hello_end_binary_obj_user_hello_size。链接器通过二进制文件的名字生成了这些符号的名字,这些符号使得内核代码可以以某种方式引用这些嵌入的二进制文件。

练习2中遇到的问题:

  • 除了env->env_tf.tf_eip以外不要修改其他的值,因为其已经在env_alloc()中被初始化
  • region_alloc()中笔误导致只映射了一页
  • load_icode()中需要切换页表,之后加载每段仅用一个memcpy即可实现

练习2的代码如下,仅供参考:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// In env_init():
// LAB 3: Your code here.
ssize_t i;
// loop in reverse order to keep ascending order in env free list
for (i = NENV - 1; i >= 0; i--) {
// set env_status, env_id
envs[i].env_status = ENV_FREE;
envs[i].env_id = 0;
// set env_link and insert into env_free_list
envs[i].env_link = env_free_list;
env_free_list = &envs[i];
}

// In env_setup_vm():
// LAB 3: Your code here.
// set env_pgdir and generate page directory based on kern_pgdir
e->env_pgdir = (pde_t *)page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
// increase pp_ref
p->pp_ref++;

// In region_alloc():
// LAB 3: Your code here.
// (But only if you need it for load_icode.)
//
// Hint: It is easier to use region_alloc if the caller can pass
// 'va' and 'len' values that are not page-aligned.
// You should round va down, and round (va + len) up.
// (Watch out for corner-cases!)

// round va and va + len
uintptr_t start = (uintptr_t)ROUNDDOWN(va, PGSIZE);
uintptr_t end = (uintptr_t)ROUNDUP(va + len, PGSIZE);

for (; start < end; start += PGSIZE) {
// alloc page
struct PageInfo *p;
p = page_alloc(ALLOC_ZERO);
if (!p) { panic("out of memory when allocating region!"); }
// insert page into environment's page directory
if (page_insert(e->env_pgdir, p, (char *)start, PTE_W | PTE_U | PTE_P) <
0) {
panic("out of memory when allocating region!");
}
}

// In load_icode():
// LAB 3: Your code here.
// switch address space for loading program segments
lcr3(PADDR(e->env_pgdir));

struct Elf *elf = (struct Elf *)binary;
// check elf magic
if (elf->e_magic != ELF_MAGIC) { panic("invalid elf format!"); }

// set the program entry for env
e->env_tf.tf_eip = elf->e_entry;

struct Proghdr *ph, *eph;

// get the start and end of program header entry
ph = (struct Proghdr *)(binary + elf->e_phoff);
eph = ph + elf->e_phnum;
for (; ph < eph; ph++) {
if (ph->p_type == ELF_PROG_LOAD) { // if the segment is to be loaded
// alloc corresponding region(clear zero)
region_alloc(e, (char *)ph->p_va, ph->p_memsz);
// copy from ELF header to virtual addresses directly
memcpy((char *)ph->p_va, (char *)binary + ph->p_offset, ph->p_filesz);
}
}
// switch back
lcr3(PADDR(kern_pgdir));

// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
// LAB 3: Your code here.
// allocate a page and insert it into env's page directory
// panic when page_alloc or page_insert failed
struct PageInfo *stack_page = page_alloc(ALLOC_ZERO);
if (!stack_page) { panic("out of memory when alloc program's stack!"); }
if (page_insert(e->env_pgdir, stack_page, (char *)(USTACKTOP - PGSIZE),
PTE_W | PTE_U | PTE_P) < 0) {
panic("failed to set program's stack!");
}

// In env_create():
// LAB 3: Your code here.
struct Env *env;
// allocate new env with parent ID 0
if (env_alloc(&env, 0) < 0) { panic("failed to allocate env!"); }
// load elf binary and set env_type
load_icode(env, binary);
env->env_type = type;

// In env_run():
// LAB 3: Your code here.
if (curenv != NULL) { // context switch
if (curenv->env_status == ENV_RUNNING) {
// change to runnable if current status is running
// for not runnable, is not necessary to do this
curenv->env_status = ENV_RUNNABLE;
}
}

// set new curenv, update status and counter
curenv = e;
e->env_status = ENV_RUNNING;
e->env_runs++;
// address space switch
// reference from inc/x86.h
lcr3(PADDR(e->env_pgdir));
// drop into user mode
env_pop_tf(&(e->env_tf));

// panic("env_run not yet implemented");

成功进入用户环境后,若用户环境尝试使用int指令进行系统调用时,将产生错误,因为JOS没有设置任何从用户空间进入内核的方式。
当CPU发现无法处理系统中断调用后,会产生一个通用保护错误,发现无法处理它,然后生成一个二重错误,最终因无法处理而生成一个三重错误并放弃,然后系统重启。但是为了便于调试,patch的qemu不会重启。
关于重启的理由可以参考这里

完成后经过测试,程序在int $0x30处Triple Fault,可知本部分代码基本正确。

处理中断和异常

练习3要求阅读Intel 80386编程手册中的第九章:异常和中断

本实验中将沿用Intel关于中断和异常的术语。中断是由处理器以外的异步事件引发的,而异常是由当前正在执行的指令同步引发的。

保护控制转移基础

异常和中断均为“保护控制转移”,会引发处理器从用户到内核模式(CPL=0),从而避免给用户模式的代码干扰内核或是其他环境的机会。这要求了引发中断或者是异常的代码不能选择进入内核的地点或方式。
在x86中,主要有两种机制一起保证了内核总是在受保护的情况下进入,这两种机制为中断描述符表(Interrupt Descriptor Table)和任务状态段(Task State Segment)。

中断描述符表中,处理器保证了异常和中断仅能导致内核在若干个具体的、由内核明确定义好的入口执行,而不是在中断和异常发生时运行的代码。
x86允许最多设立256个不同的中断或异常入口,每一个入口都具有一个中断向量。中断向量为0-255的数字,中断向量由中断来源决定:不同的设备,错误条件和应用程序对内核的请求会产生不同向量的中断。CPU使用中断向量作为处理器的中断描述符表的索引。中断描述符表在内核私有的内存中建立。
处理器从该表中加载送入EIP寄存器的值(处理程序的入口),以及送入CS段寄存器的值(包括了特权级,在JOS中,所有的异常均在内核模式执行,即特权级0)。

任务状态段。处理器需要在唤醒处理程序前将在中断或异常发生前的旧的处理器状态保存起来,如旧的EIP寄存器值和旧的CS段寄存器值,以便处理程序之后能恢复现场,从中断或异常发生的地方继续执行。然而,保存旧的处理器状态的区域必须对于非特权的用户模式代码处于被保护的状态。否则,错误的或是恶意的用户代码可能会破坏内核。
因此,当x86处理器从用户模式特权级切换到内核模式时,其也会切换到内核内存中的一个栈。任务状态段指定了相应的段寄存器以及相应栈的地址。处理器将SS, EFLAGS, CS, EIP以及一个可选的错误码压入这个新栈中。然后从中断描述符中读取相应的CSEIP,并设置ESPSS指向新的栈。
尽管TSS有着多种作用,JOS仅用它来定义从用户模式切换到内核模式时的内核栈。因为JOS中的内核模式为x86的特权级0,故处理器仅使用TSSESP0SS0域。

异常和中断的类型

x86处理器能产生的所有同步异常均使用了0-31的中断向量,映射为IDT的0-31号入口。超过31的中断向量仅供由int指令或是由异步硬件中断产生的“软中断”使用。

嵌套异常和中断

处理器能同时处理内核模式和用户模式的异常。在内核模式中的异常不需要切换栈,因此,不需要压入旧的SSESP值。通过这种方式,内核可以优雅地处理内核产生的嵌套异常和中断。该能力对于实现保护是非常重要的。

若处理器已经在内核模式中并且接受了一个异常并且无法将旧值压入内核栈中时(如内存不足),那么,处理器将无论如何也无法恢复,只能重启。内核必须设计良好以避免这种情况发生。

建立IDT表

由于JOS中使用的为IA_32的陷阱码,更推荐参考IA_32的第五章

练习4中遇到的问题如下:

  • 设置陷阱门时段选择子应当为GD_KT而不是GD_KD,因为错误处理函数均被链接至了内核的text段。
  • 已存在page_fault_handler的函数,命名时需避免重名。
  • 不能直接用立即数设置段寄存器。

练习4的代码如下,仅供参考:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// In trapentry.S:
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/

TRAPHANDLER_NOEC(divide_error_handler, T_DIVIDE)
TRAPHANDLER_NOEC(debug_exception_handler, T_DEBUG)
TRAPHANDLER_NOEC(non_maskable_interrupt_handler, T_NMI)
TRAPHANDLER_NOEC(breakpoint_handler, T_BRKPT)
TRAPHANDLER_NOEC(overflow_handler, T_OFLOW)
TRAPHANDLER_NOEC(bounds_check_handler, T_BOUND)
TRAPHANDLER_NOEC(invalid_opcode_handler, T_ILLOP)
TRAPHANDLER_NOEC(device_not_available_handler, T_DEVICE)
TRAPHANDLER(double_fault_handler, T_DBLFLT)
TRAPHANDLER(invalid_tss_handler, T_TSS)
TRAPHANDLER(segment_not_present_handler, T_SEGNP)
TRAPHANDLER(stack_exception_handler, T_STACK)
TRAPHANDLER(general_protection_fault_handler, T_GPFLT)
TRAPHANDLER(pagefault_handler, T_PGFLT)
TRAPHANDLER_NOEC(floating_point_error_handler, T_FPERR)
TRAPHANDLER(alignment_check_handler, T_ALIGN)
TRAPHANDLER_NOEC(machine_check_handler, T_MCHK)
TRAPHANDLER_NOEC(simd_floating_point_error_handler, T_SIMDERR)

/*
* Lab 3: Your code here for _alltraps
*/

_alltraps:
// push ds and es and general registers
push %ds
push %es
pushal

// load ds and es with GD_KD, for kernel stack locates in data
mov $GD_KD, %ax
mov %ax, %ds
mov %ax, %es

// pass tf as an argument
pushl %esp

// call trap and no need to return
call trap

// In trap.c, trap_init():

// LAB 3: Your code here.
// declare of exception handler
void divide_error_handler();
void debug_exception_handler();
void non_maskable_interrupt_handler();
void breakpoint_handler();
void overflow_handler();
void bounds_check_handler();
void invalid_opcode_handler();
void device_not_available_handler();
void double_fault_handler();
void invalid_tss_handler();
void segment_not_present_handler();
void stack_exception_handler();
void general_protection_fault_handler();
void pagefault_handler();
void floating_point_error_handler();
void alignment_check_handler();
void machine_check_handler();
void simd_floating_point_error_handler();

// set up trap gate descriptor
SETGATE(idt[T_DIVIDE], 1, GD_KT, divide_error_handler, 0);
SETGATE(idt[T_DEBUG], 1, GD_KT, debug_exception_handler, 0);
SETGATE(idt[T_NMI], 1, GD_KT, non_maskable_interrupt_handler, 0);
SETGATE(idt[T_BRKPT], 1, GD_KT, breakpoint_handler, 3);
SETGATE(idt[T_OFLOW], 1, GD_KT, overflow_handler, 0);
SETGATE(idt[T_BOUND], 1, GD_KT, bounds_check_handler, 0);
SETGATE(idt[T_ILLOP], 1, GD_KT, invalid_opcode_handler, 0);
SETGATE(idt[T_DEVICE], 1, GD_KT, device_not_available_handler, 0);
SETGATE(idt[T_DBLFLT], 1, GD_KT, double_fault_handler, 0);
SETGATE(idt[T_TSS], 1, GD_KT, invalid_tss_handler, 0);
SETGATE(idt[T_SEGNP], 1, GD_KT, segment_not_present_handler, 0);
SETGATE(idt[T_STACK], 1, GD_KT, stack_exception_handler, 0);
SETGATE(idt[T_GPFLT], 1, GD_KT, general_protection_fault_handler, 0);
SETGATE(idt[T_PGFLT], 1, GD_KT, pagefault_handler, 0);
SETGATE(idt[T_FPERR], 1, GD_KT, floating_point_error_handler, 0);
SETGATE(idt[T_ALIGN], 1, GD_KT, alignment_check_handler, 0);
SETGATE(idt[T_MCHK], 1, GD_KT, machine_check_handler, 0);
SETGATE(idt[T_SIMDERR], 1, GD_KT, simd_floating_point_error_handler, 0);

问题1:
若是使用同一个处理程序,将无法限制调用错误处理程序的代码的特权级,也无法得知中断向量的值。

问题2:
仅有内核代码允许执行页错误处理程序,尽管调用了int $14,仍然因为保护机制而生成了中断向量13。如果内核允许int $14唤醒页错误处理程序,那么恶意的程序可以因此而随意触发缺页错误,导致系统无法正常工作。

第二部分 Page Faults, Breakpoints Exceptions, and System Calls - 页错误,断点异常和系统调用

处理页错误

页错误的中断向量号为14,是非常重要的异常。当处理器接收一个页错误时,会将引发页错误的线性地址存储在处理器控制寄存器CR2中。

练习5的代码如下,仅供参考:

1
2
3
4
5
// In trap_dispatch():
if (tf->tf_trapno == T_PGFLT) {
// dispatch page fault exceptions
page_fault_handler(tf);
}

断点异常

断点异常的中断向量号为3,通常被调试器用作向程序代码中添加断点,原理是将程序中的某一条指令暂时改为1字节的int3的软中断指令。在JOS中,将大量使用这一异常来实现一个原始的伪系统调用,使得用户环境可以使用它来环境JOS内核监视器,可以把内核监视器看做一个原始的调试器。
用户模式的panic,就是通过显示panic消息后执行int3实现的。

练习6的代码如下,仅供参考:

1
2
3
4
if (tf->tf_trapno == T_BRKPT) {
// dispatch breakpoint exceptions
monitor(tf);
}

问题3:
若是用特权级0初始化断点异常的IDT,那么会触发通用保护错误,这是因为用户模式的代码无法执行特权级0(内核模式)的处理程序,需要用特权级3初始化断点异常的IDT,这样才能使得断点测试正确通过。

问题4:
这些措施都是为了保护内核和用户环境的相互独立,使得用户环境仅能在收到允许的情况下执行某些内核的代码,保证了恶意程序不会破坏内核,窃取数据。同时也能保证用户环境能从内核得到必要的功能支持。

系统调用

用户进程通过系统调用请求内核为它工作。当用户进程唤醒系统调用时,处理器进入内核模式,处理器和内核合作保存用户进程状态,内核执行合适的代码完成系统调用,并恢复至用户进程。系统调用的具体实现随平台不同而不同。

JOS内核使用int $0x30作为系统调用。需要建立相关的中断描述符,注意中断向量0x30不可能由硬件生成,毫无疑问应该允许用户执行对应的处理程序。

应用程序会将系统调用号和系统调用参数存入寄存器中。避免了内核访问用户环境栈或是指令流。系统调用号放在寄存器%eax中,最多五个参数分别被相应地放在寄存器%edx, %ecx, %ebx, %edi%esi中。内核将返回值放在寄存器%eax中。唤醒系统调用的汇编代码已经提供为lib/syscall.c中的syscall()

练习7中遇到的问题如下:

  • syscall作为软中断不会压入错误码
  • 调用syscall函数时应当使用保存的栈帧中的寄存器值而非实际的寄存器值,原因是在函数调用间某些寄存器的值会发生改变
  • 练习7需要参考lib/syscall.c中得知参数的位置关系

练习7的代码如下,仅供参考:

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
// In trapentry.S:
TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL)

// In trap.c, trap_init():
void syscall_handler();

SETGATE(idt[T_SYSCALL], 1, GD_KT, syscall_handler, 3);

// In kern/syscall.c, syscall():
// LAB 3: Your code here.

// panic("syscall not implemented");

switch (syscallno) {
case SYS_cputs:
// call sys_cputs
sys_cputs((const char *)a1, (size_t)a2);
break;
case SYS_cgetc:
// cll sys_cgetc
return sys_cgetc();
break;
case SYS_getenvid:
// call sys_getenvid
return sys_getenvid();
break;
case SYS_env_destroy:
// call sys_env_destroy
return sys_env_destroy((envid_t)a1);
break;
default: return -E_INVAL;
}

// will not reach here
return -E_UNSPECIFIED;

用户模式启动

一个用户在lib/entry.S的顶部开始运行,经过某些设置后,代码调用lib/libmain.c。你应当修改libmain()以初始化指向当前环境的struct Env指针thisenv(注意到lib/entry.S已经定义了指向你在第一部分映射的UENVSenvs)。

libmain()然后调用umain,对于hello程序而言,位于user/hello.c中。在打印hello, world后,它尝试访问thisenv->env_id。这也是为什么hello程序会出现fault

练习8中遇到的问题如下:

  • 可以使用宏ENVXenvid得到envenvs中的偏移量,而无需遍历整个envs
  • Lab2中pgdir_walk未设置PTE_U导致访问时出现了页错误(仅有PTE项中设置PTE_U是不足的)。

练习8的答案如下,仅供参考:

1
2
3
4
// In libmain.c, libmain():
// LAB 3: Your code here.
// get env id use system call and use ENVX to get index
thisenv = envs + ENVX(sys_getenvid());

页错误和内存保护

内存保护是操作系统的重要特性,保证了一个程序的错误不会毁坏内核或是其他的程序。
操作系统通常和硬件一起实现内存保护。操作系统负责告知硬件哪些虚拟地址是有效的、哪些是无效的。当一个程序试图去访问一个无效的地址或是一个没有权限的地址时,处理器在引起错误的指令处停止程序,然后带着相应的信息陷入内核。若错误可修复,则内核修复该错误并继续执行程序;若错误不可恢复,则程序无法继续运行。
现在考虑系统调用,系统调用允许用户程序向内核传递指针,内核在处理系统调用时将指针解引用,会出现以下两种问题:

  1. 内核的缺页错误潜在地比用户程序的缺页更加严重。若内核在操作私有的数据结构时发生缺页,那么将是内核自己的漏洞,错误处理程序应当panic内核。然而当内核解引用用户程序传递的指针时,需要某种方式标记由解引用导致的缺页实际上代表的是用户程序的利益。
  2. 内核比用户程序具有更多的权限。在这种情况下,用户程序可能传递给内核一个指针,该指针指向的内存只能被内核读写而不能被用户程序读写。在这种情况下,内核不能对该指针解引用,这么做会暴露私有数据或是破坏内核完整性。

当内核处理用户程序传递的指针时必须非常小心。内核必须检查用户传入的指针。

练习9和练习10遇到的问题如下:

  • 需要获取段寄存器的最低两位以得到段特权级

练习9和练习10的答案如下,仅供参考:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// In kern/trap.c, page_fault_handler():
// LAB 3: Your code here.
if ((tf->tf_cs & 3) == 0) {
// code that causes page fault in kernel mode
panic("page fault in kernel!");
}

// In kern/pmap.c, user_mem_check():
int user_mem_check(struct Env *env, const void *va, size_t len, int perm) {
// LAB 3: Your code here.
if ((uintptr_t)va >= ULIM) {
// condition 1 - below ULIM violated
user_mem_check_addr = (uintptr_t)va;
return -E_FAULT;
}

uintptr_t va_start = (uintptr_t)ROUNDDOWN(va, PGSIZE);
uintptr_t va_end = (uintptr_t)ROUNDUP(va + len, PGSIZE);

for (; va_start < va_end; va_start += PGSIZE) {
// note we set page directory entry with less restrict
// we will only test page table entry here
pte_t *pgtable_entry_ptr =
pgdir_walk(env->env_pgdir, (char *)va_start, false);
if ((*pgtable_entry_ptr & (perm | PTE_P)) != (perm | PTE_P)) {
// condition 2 - permission violated
if (va_start <= (uintptr_t)va) {
// va lie in the first page and not aligned, return va
user_mem_check_addr = (uintptr_t)va;
} else if (va_start >= (uintptr_t)va + len) {
// va lie in the last page and exceed va + len, return va + len
user_mem_check_addr = (uintptr_t)va + len;
} else {
// return corresponding page's initial address
user_mem_check_addr = va_start;
}

return -E_FAULT;
}
}

// pass user memory check
return 0;
}

// In kern/syscall.c, syscall():
case SYS_cputs:
// checks memory before use sys_cputs to dereference a1
user_mem_assert(curenv, (char *)a1, (size_t)a2, PTE_U);
// call sys_cputs
sys_cputs((const char *)a1, (size_t)a2);
break;

// In kern/kdebug.c, debuginfo_eip():
// 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(struct UserStabData), PTE_U) < 0) {
return -1;
}

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

发生了页错误的原因还要观察memlayout.h,在USTACKTOP上方有一个位于0xeebfd000Empty Memory,该虚拟地址没有映射任何的物理页。而在mon_backtrace的最后,访问到了位于此处的虚拟地址,因而导致了一个不可处理的页错误。

实验小结

最终执行make grade,评分脚本的输出如下:

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
36
37
38
39
40
41
42
divzero:
$ make run-divzero-nox-gdb QEMUEXTRA+=-snapshot
OK (1.3s)
softint:
$ make run-softint-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
badsegment:
$ make run-badsegment-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
Part A score: 30/30
faultread:
$ make run-faultread-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
faultreadkernel:
$ make run-faultreadkernel-nox-gdb QEMUEXTRA+=-snapshot
OK (0.9s)
faultwrite:
$ make run-faultwrite-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
faultwritekernel:
$ make run-faultwritekernel-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
breakpoint:
$ make run-breakpoint-nox-gdb QEMUEXTRA+=-snapshot
OK (2.0s)
testbss:
$ make run-testbss-nox-gdb QEMUEXTRA+=-snapshot
OK (2.1s)
hello:
$ make run-hello-nox-gdb QEMUEXTRA+=-snapshot
OK (1.0s)
buggyhello:
$ make run-buggyhello-nox-gdb QEMUEXTRA+=-snapshot
OK (1.8s)
buggyhello2:
$ make run-buggyhello2-nox-gdb QEMUEXTRA+=-snapshot
OK (2.1s)
evilhello:
$ make run-evilhello-nox-gdb QEMUEXTRA+=-snapshot
OK (1.1s)
Part B score: 50/50
Score: 80/80

至此,实验三结束。

第三个实验的难度明显比实验一和实验二大。
用户环境的管理相对简单,而设置陷阱和中断的部分涉及大量的x86硬件知识,需要大量参考Intel手册,要求直接写汇编代码设置栈帧也颇有挑战。

评论和共享

  • 第 1 页 共 1 页
作者的图片

码龙黑曜

iOS开发者/计算机科学/兽人控


华中科技大学 本科在读


Wuhan, China