(最近有点事,做的比较慢。哦,不,抄的比较慢。。。)
Lab 3: User Environments
Introduction
在这个实验中,我们将实现操作系统的一些基本功能,来实现用户环境下的进程的正常运行。你将会加强JOS内核的功能,为它增添一些重要的数据结构,用来记录用户进程环境的一些信息;创建一个单一的用户环境,并且加载一个程序运行它。你也可以让JOS内核能够完成用户环境所作出的任何系统调用,以及处理用户环境产生的各种异常。
Getting Started
照着官网上做就行了。
然后会多出他说的那些文件,后面用到的时候再说。
Part A: User Environments and Exception Handling
让我们看看inc/env.h
的文件,里面有用户环境的一些基本定义。我们直接分析分析一下。内核使用Env数据结构来跟踪每个用户环境。 在本实验中,最初只会创建一个环境,但您需要设计JOS内核以支持多个环境; lab4
将通过允许用户环境fork
其他环境来利用此功能。
env.h
1 | /* See COPYRIGHT for copyright information. */ |
分析完后,就去看 kern/env.c
。
这个文件先不看完,就看看他定义了什么东西。1
2
3struct Env *envs = NULL; // All environments 所有的环境
struct Env *curenv = NULL; // The current env 当前环境
static struct Env *env_free_list; // Free environment list 空闲环境列表
后面有一大堆介绍。Trapframe
这个里面具体有啥,我们后面用到的时候再看。
Allocating the Environments Array
前两个 结构体,在kern/env.h
里面有进行扩展,现在练习让我们,为他分配一个空间并映射,就是像上次为kern_pages
分配空间一样,并进行映射。1
2
3
4
5//////////////////////////////////////////////////////////////////////
// Make 'envs' point to an array of size 'NENV' of 'struct Env'.
// LAB 3: Your code here.
envs=(struct Env*)boot_alloc(NENV*sizeof(struct Env));
memset(envs,0,NENV*sizeof(struct Env));
这个 和,上次实验是一样的,和分配kern_pgdir
是一模一样的。1
2
3
4
5
6
7
8//////////////////////////////////////////////////////////////////////
// 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, PTSIZE, PADDR(envs), PTE_U);
另外再复习一下上节课的内存分配 (下面又是盗的图,哈哈~ )。
Creating and Running Environments
现在你需要去编写 kern/env.c
文件来运行一个用户环境了。由于你现在没有文件系统,所以必须把内核设置成能够加载内核中的静态二进制程序映像文件。Lab3
里面的 GNUmakefile
文件在obj/user/
目录下面生成了一系列的二进制映像文件。如果你看一下 kern/Makefrag
文件,你会发现一些奇妙的地方,这些地方把二进制文件直接链接到内核可执行文件中,只要这些文件是.o
文件。其中在链接器命令行中的-b binary
选项会使这些文件被当做二进制执行文件链接到内核之后。
在kern/ini.c
中的i386_init()
,你会看到代码运行的环境中,这些二进制图像之一。然而,关键的功能设置用户环境是不完整的;您需要填写他们进来。
我们照着他的意思去看看,发现相较于前几次实验,多了几行。1
2
3
4
5
6
7
8
9
10
11
12
13
14 // Lab 3 user environment initialization functions
env_init();
trap_init();
// Don't touch -- used by grading script! 这些不要碰,是从来测试的
ENV_CREATE(TEST, ENV_TYPE_USER); //env_create
// Touch all you want.
ENV_CREATE(user_hello, ENV_TYPE_USER);
// We only have one user environment for now, so just run it.
env_run(&envs[0]);
在 kern/env.h
里面可以看见这个宏的原型,就当他运行了几个不同的测试吧。我没找到这几个在哪。1
2
3
4
5
6
7
8
do { \
extern uint8_t ENV_PASTE3(_binary_obj_, x, _start)[]; \
env_create(ENV_PASTE3(_binary_obj_, x, _start), \
type); \
} while (0)
不出意外,我们的任务 就是补充多出来的这几个函数了。
- env_init(): 初始化所有的在envs数组中的 Env结构体,并把它们加入到 env_free_list中。 还要调用 env_init_percpu,这个函数要配置段式内存管理系统,让它所管理的段,可能具有两种访问优先级其中的一种,一个是内核运行时的0优先级,以及用户运行时的3优先级。
- env_setup_vm(): 为一个新的用户环境分配一个页目录表,并且初始化这个用户环境的地址空间中的和内核相关的部分。
- region_alloc(): 为用户环境分配物理地址空间
- load_icode(): 分析一个ELF文件,类似于boot loader做的那样,我们可以把它的内容加载到用户环境下。
- env_create(): 利用env_alloc函数和load_icode函数,加载一个ELF文件到用户环境中
- env_run(): 在用户模式下,开始运行一个用户环境。
现在开始,补充kern/env.c
,
env_init()
1 | // Mark all environments in 'envs' as free, set their env_ids to 0, |
在env_init()
中调用了env_init_percpu()
不知道这个是干啥的。根据注释,是初始化了GDT和段描述符。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// Load GDT and segment descriptors.
void
env_init_percpu(void)
{
lgdt(&gdt_pd);
// The kernel never uses GS or FS, so we leave those set to
// the user data segment.
asm volatile("movw %%ax,%%gs" : : "a" (GD_UD|3));
asm volatile("movw %%ax,%%fs" : : "a" (GD_UD|3));
// The kernel does use ES, DS, and SS. We'll change between
// the kernel and user data segments as needed.
asm volatile("movw %%ax,%%es" : : "a" (GD_KD));
asm volatile("movw %%ax,%%ds" : : "a" (GD_KD));
asm volatile("movw %%ax,%%ss" : : "a" (GD_KD));
// Load the kernel text segment into CS.
asm volatile("ljmp %0,$1f\n 1:\n" : : "i" (GD_KT));
// For good measure, clear the local descriptor table (LDT),
// since we don't use it.
lldt(0);
}
env_setup_vm()
初始化完 之后,因为trap()
是下一个的暂时不用管,所以我们直接跳到create_env
,创建这个第一个要干的肯定是分配内存,最开始要做的是分配一个页目录。这个页目录,肯定是要复制内核的一部分,因为内核那一部分,你是绝对不能动的。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//
// Initialize the kernel virtual memory layout for environment e. 初始化内核虚拟布局
// Allocate a page directory, set e->env_pgdir accordingly, 分配一个页目录给e->env_pgdir
// and initialize the kernel portion of the new environment's address space
// Do NOT (yet) map anything into the user portion
// of the environment's virtual address space.
//初始化内核部分,不用映射 用户部分。
// Returns 0 on success, < 0 on error. Errors include:
// -E_NO_MEM if page directory or table could not be allocated.
//成功返回 0 否则返回 -E_NO_MEM
static int
env_setup_vm(struct Env *e)
{
int i;
struct PageInfo *p = NULL;
// Allocate a page for the page directory 分配了一个页目录
if (!(p = page_alloc(ALLOC_ZERO)))
return -E_NO_MEM;
// Now, set e->env_pgdir and initialize the page directory.
//现在设置 e->env_pgdir 然后初始化页面目录
// Hint:
// - The VA space of all envs is identical above UTOP
// (except at UVPT, which we've set below).va 所有 envs 的虚拟地址 都是相同的在UTOP上面
// See inc/memlayout.h for permissions and layout.
// Can you use kern_pgdir as a template? Hint: Yes. 可以用kern_pgdir做一个模板
// (Make sure you got the permissions right in Lab 2.)
// - The initial VA below UTOP is empty. 初始化 虚拟地址在 UTOP 是空的
// - You do not need to make any more calls to page_alloc. 你不需要去做任何的page_alloc
// - Note: In general, pp_ref is not maintained for
// physical pages mapped only above UTOP, but env_pgdir
// is an exception -- you need to increment env_pgdir's
// pp_ref for env_free to work correctly.
// - The functions in kern/pmap.h are handy.
// 自己翻译吧,只可意会不可言传
// LAB 3: Your code here.
p->pp_ref++;
e->env_pgdir=(pde_t *)page2kva(p);
memcpy(e->env_pgdir, kern_pgdir, PGSIZE);
// UVPT maps the env's own page table read-only.
// Permissions: kernel R, user R
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
return 0;
}
region_alloc
分配完页目录,然后就是要给用户创建空间。只有一个页目录,肯定是不行的,你必须要给用户程序使用的空间。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//
// Allocate len bytes of physical memory for environment env,
// and map it at virtual address va in the environment's address space.
// Does not zero or otherwise initialize the mapped pages in any way.
// Pages should be writable by user and kernel.
// Panic if any allocation attempt fails.
//分配len 字节的 物理空间给 用户环境env,映射他的虚拟地址在环境的地址空间,不要用任何方式初始化页面。权限是内核用户可写,出错就 panic
static void
region_alloc(struct Env *e, void *va, size_t len)
{
// 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!)
void *start=ROUNDDOWN(va,PGSIZE),*end=ROUNDUP(va+len,PGSIZE);
for (void * addr=start;addr<end;addr+=PGSIZE){
struct PageInfo* p=page_alloc(0);
if(p==NULL){
panic("region alloc failed: No more page to be allocated.\n");
}
else {
if(page_insert(e->env_pgdir,p,addr, PTE_U | PTE_W)==-E_NO_MEM){
panic("region alloc failed: page table couldn't be allocated.\n");
}
}
}
}
写个函数之前,我们先去看看trap.h
load_icode
因为目前并没有文件系统,所以我们要需要分配的堆栈,并不是来自文件加载出来的。为了方便实验,JOS
让我们像加载操作系统一样加载这些文件。这个里面用到了Trapframe
,我去看了看这个东西,对于某个字段是干啥的完全没有注释所以我也不知道该分析。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//
// Set up the initial program binary, stack, and processor flags
// for a user process. 初始化进程的 二进制 栈 和 处理器
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
//这个程序只能调用在内核初始化,在运行第一个用户模式环境
// This function loads all loadable segments from the ELF binary image
// into the environment's user memory, starting at the appropriate
// virtual addresses indicated in the ELF program header. 加载所有的 可装载程序 从 ELF二进制映象文件到内存,开始在适当的虚拟地址在ELF 的头部
// At the same time it clears to zero any portions of these segments 段中任何部分初始化为0
// that are marked in the program header as being mapped
// but not actually present in the ELF file - i.e., the program's bss section.
//
// All this is very similar to what our boot loader does, except the boot
// loader also needs to read the code from disk. Take a look at
// boot/main.c to get ideas. 很像boot loader 做的,可以参考
//
// Finally, this function maps one page for the program's initial stack.
//这个函数映射一个页为了初始化堆栈
// load_icode panics if it encounters problems.
// - How might load_icode fail? What might be wrong with the given input?
//
static void
load_icode(struct Env *e, uint8_t *binary)
{
// Hints:
// Load each program segment into virtual memory
// at the address specified in the ELF segment header.加载每个程序段到虚拟内存 在 具体的ELF 头文件
// You should only load segments with ph->p_type == ELF_PROG_LOAD. 只需要加载ph->p_type == ELF_PROG_LOAD
// Each segment's virtual address can be found in ph->p_va 每个段的虚拟地址可以在ph->p_va找到
// and its size in memory can be found in ph->p_memsz. 大小是 ph->p_memsz
// The ph->p_filesz bytes from the ELF binary, starting at 文件开始在binary + ph->p_offset,应该被复制到 虚拟地址 ph->p_va。
// 'binary + ph->p_offset', should be copied to virtual address
// ph->p_va. Any remaining memory bytes should be cleared to zero.其他剩下的空间初始化为0
// (The ELF header should have ph->p_filesz <= ph->p_memsz.) 头部文件应该 ph->p_filesz <= ph->p_memsz
// Use functions from the previous lab to allocate and map pages.
//使用这个前面所写的函数
// All page protection bits should be user read/write for now. 所有页都是用户可读写的
// ELF segments are not necessarily page-aligned, but you can ELF 段可能不是页对齐。
// assume for this function that no two segments will touch
// the same virtual page.假设这个函数 不会两个段在同一个虚拟页
//
// You may find a function like region_alloc useful. 你可以发现 region_alloc是有用的
//
// Loading the segments is much simpler if you can move data
// directly into the virtual addresses stored in the ELF binary.
// So which page directory should be in force during
// this function? 如果你可以直接移动数据存到ELF 序列里面 架子段就很容易,所以 页目录应当使用在这个函数
//
// You must also do something with the program's entry point,
// to make sure that the environment starts executing there.
// What? (See env_run() and env_pop_tf() below.)
// 你必须对程序入口指针做点什么 确保 后面用的上。
// LAB 3: Your code here.
//根据,分析 首先需要做的一件事 应该是讲binary 转换成 ELF,参照bootmain。
struct Proghdr *ph, *eph;
struct Elf * ELF=(struct Elf *)binary;
if (ELFHDR->e_magic != ELF_MAGIC)panic("The loaded file is not ELF format!\n");
ph = (struct Proghdr *) ((uint8_t *) ELFHDR + ELFHDR->e_phoff);
eph = ph + ELFHDR->e_phnum;
//装载 用户目录
lcr3(PADDR(e->env_pgdir));
//第二部应该是加载段到内存
for(;ph<eph;ph++){
//加载条件是 ph->p_type == ELF_PROG_LOAD,地址是 ph->p_va 大小ph->p_memsz
if(ph->p_type == ELF_PROG_LOAD){
if (ph->p_filesz > ph->p_memsz)
panic("load_icode failed: p_memsz < p_filesz.\n");
region_alloc(e, ph->p_va,ph->p_memsz);
//复制ph->p_filesz bytes ,其他的补0
memset(ph->p_va,0,ph->p_memsz);
memcpy(ph->p_va,binary + ph->p_offset,ph->p_filesz);
}
}
lcr3(PADDR(kern_pgdir));
//最后是入口地址 这个实在 inc/trap.h 里面定义的
e->env_tf.tf_eip = ELFHDR->e_entry;
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE. 这个函数刚写过
// LAB 3: Your code here.
gion_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
}
在写enc_creat
之前,我们先来分析一下,我们并不需要写 env_alloc
,这个函数你可以理解为初始化一个env
。 我们不需要知道过分的细节,但是需要了解他做了什么。
env_alloc
1 | // |
env_create
函数作用就是根据binary
创建一个env
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//
// Allocates a new env with env_alloc, loads the named elf
// binary into it with load_icode, and sets its env_type.
// This function is ONLY called during kernel initialization,
// before running the first user-mode environment.
// The new env's parent ID is set to 0.
// 分配一个新的env 通过env_alloc 加载elf,设置他的its env_type 这个函数只在内核初始化抵用,在跑第一个用户环境,父亲设置为 0
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){
cprintf("%e\n",r);
panic("env_create:error");
}
load_icode(e,binary);
e->env_type=type;
}
env_run
这个就是真正的用户环境运行了。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/
// Context switch from curenv to env e. 上下文切换到 e
// Note: if this is the first call to env_run, curenv is NULL.
//如果第一个调用 curenv 是空的
// This function does not return.
//
void
env_run(struct Env *e)
{
// Step 1: If this is a context switch (a new environment is running): 如果有上下文切换
// 1. Set the current environment (if any) back to 第一步当前环境 就绪状态
// ENV_RUNNABLE if it is ENV_RUNNING (think about
// what other states it can be in),
// 2. Set 'curenv' to the new environment, 当前运行变成 新的环境
// 3. Set its status to ENV_RUNNING, 设置他的状态为 运行
// 4. Update its 'env_runs' counter, 更新计数
// 5. Use lcr3() to switch to its address space. 修改地址空间
// Step 2: Use env_pop_tf() to restore the environment's 第二部 使用那个啥恢复环境
// registers and drop into user mode in the
// environment.
// Hint: This function loads the new environment's state from 这个函数重新加载 新的用户转台 从啥
// e->env_tf. Go back through the code you wrote above
// and make sure you have set the relevant parts of
// e->env_tf to sensible values. 确保 那个哈是个真确的值
// LAB 3: Your code here.
if(curenv!=NULL&&curenv->env_status==ENV_RUNNING){
curenv->env_status=ENV_RUNNABLE;
}
curenv=e;
// if(&curenv->env_tf==NULL)cprintf("***");
e->env_status=ENV_RUNNING;
e->env_runs++;
lcr3(PADDR(curenv->env_pgdir));
cprintf("%x\n",curenv->env_tf.tf_eip);
env_pop_tf(&curenv->env_tf);
panic("env_run not yet implemented");//这个注释不注释没啥影响,因为我们现在就运行了一个 env,上面那个函数已经转移了,等他再来运行这一行,说明整个操作系统已经结束了。
}
我们再分析分析这个文件里面一些其他的函数。
1 |
|
一旦你完成上述子函数的代码,并且在QEMU下编译运行,系统会进入用户空间,并且开始执行hello程序,直到它做出一个系统调用指令int。但是这个系统调用指令不能成功运行,因为到目前为止,JOS还没有设置相关硬件来实现从用户态向内核态的转换功能。当CPU发现,它没有被设置成能够处理这种系统调用中断时,它会触发一个保护异常,然后发现这个保护异常也无法处理,从而又产生一个错误异常,然后又发现仍旧无法解决问题,所以最后放弃,我们把这个叫做”triple fault”。通常来说,接下来CPU会复位,系统会重启。
所以我们马上要来解决这个问题,不过解决之前我们可以使用调试器来检查一下程序要进入用户模式时做了什么。使用make qemu-gdb
并且在 env_pop_tf
处设置断点,这条指令应该是即将进入用户模式之前的最后一条指令。然后进行单步调试,处理会在执行完iret
指令后进入用户模式。然后依旧可以看到进入用户态后执行的第一条指令了,该指令是一个cmp
指令,开始于文件 lib/entry.S
中。 现在使用 b *0x...
设置一个断点在hello
文件(obj/user/hello.asm)
中的sys_cputs
函数中的 int $0x30
指令处。这个int
指令是一个系统调用,用来展示一个字符到控制台。如果你的程序运行不到这个int
指令,说明有错误。
其实不用上面那么麻烦,直接运行make qemu-gdb
然后输入c
指令,最终make gdb
会停在 int $0x30
,然后qemu
会显示错误”triple fault”。
(后面大部分都是理论文字,大部分都是翻译过来的,所以直接照搬了大佬门博客里面的。英语水平不好,怕翻译了看不懂)
Handling Interrupts and Exceptions
到目前为止,当程序运行到第一个系统调用int $0x30
时,就会进入错误的状态,因为现在系统无法从用户态切换到内核态。所以你需要实现一个基本的异常/系统调用处理机制,使得内核可以从用户态转换为内核态。你应该先熟悉一下X86的异常中断机制。
Basics of Protected Control Transfer
异常(Exception)和中断(Interrupts)都是“受到保护的控制转移方法”,都会使处理器从用户态转移为内核态。在Intel的术语中,一个中断指的是由外部异步事件引起的处理器控制权转移,比如外部IO设备发送来的中断信号。一个异常则是由于当前正在运行的指令所带来的同步的处理器控制权的转移,比如除零溢出异常。
为了能够确保这些控制的转移能够真正被保护起来,处理器的中断/异常机制通常被设计为:用户态的代码无权选择内核中的代码从哪里开始执行。处理器可以确保只有在某些条件下,才能进入内核态。在X86
上,有两种机制配合工作来提供这种保护:
中断向量表:处理器保证中断和异常只能够引起内核进入到一些特定的,被事先定义好的程序入口点,而不是由触发中断的程序来决定中断程序入口点。
X86允许多达256个不同的中断和异常,每一个都配备一个独一无二的中断向量。一个向量指的就是0到255中的一个数。一个中断向量的值是根据中断源来决定的:不同设备,错误条件,以及对内核的请求都会产生出不同的中断和中断向量的组合。CPU将使用这个向量作为这个中断在中断向量表中的索引,这个表是由内核设置的,放在内核空间中,和GDT
很像。通过这个表中的任意一个表项,处理器可以知道:
需要加载到EIP
寄存器中的值,这个值指向了处理这个中断的中断处理程序的位置。 需要加载到CS
寄存器中的值,里面还包含了这个中断处理程序的运行特权级。(即这个程序是在用户态还是内核态下运行。)任务状态段:处理器还需要一个地方来存放,当异常/中断发生时,处理器的状态,比如EIP和CS寄存器的值。这样的话,中断处理程序一会可以重新返回到原来的程序中。这段内存自然也要保护起来,不能被用户态的程序所篡改。
正因为如此,当一个x86处理器要处理一个中断,异常并且使运行特权级从用户态转为内核态时,它也会把它的堆栈切换到内核空间中。一个叫做 “任务状态段(TSS)”的数据结构将会详细记录这个堆栈所在的段的段描述符和地址。处理器会把SS
,ESP
,EFLAGS
,CS
,EIP
以及一个可选错误码等等这些值压入到这个堆栈上。然后加载中断处理程序的CS
,EIP
值,并且设置ESP
,SS
寄存器指向新的堆栈。
尽管TSS
非常大,并且还有很多其他的功能,但是JOS
仅仅使用它来定义处理器从用户态转向内核态所采用的内核堆栈,由于JOS
中的内核态指的就是特权级0,所以处理器用TSS中的ESP0
,SS0
字段来指明这个内核堆栈的位置,大小。Types of Exceptions and Interrupts
所有的由
X86
处理器内部产生的异常的向量值是0
到31
之间的整数。比如,页表错所对应的向量值是14.
而大于31
号的中断向量对应的是软件中断,由int
指令生成;或者是外部中断,由外部设备生成。
在这一章,我们将扩展JOS
的功能,使它能够处理0~31
号内部异常。在下一章会让JOS
能够处理48
号软件中断,主要被用来做系统调用。在Lab 4
中会继续扩展JOS
使它能够处理外部硬件中断,比如时钟中断。
An Example
让我们试一下除0
- 处理器会首先切换自己的堆栈,切换到由
TSS
的SS0
,ESP0
字段所指定的内核堆栈区,这两个字段分别存放着GD_KD
和KSTACKTOP
的值。 - 处理器把异常参数压入到内核堆栈中,起始于地址KSTACKTOP:
- 因为我们要处理的是除零异常,它的中断向量是0,处理器会读取
IDT
表中的0号表项,并且把CS:EIP
的值设置为0号中断处理函数的地址值。 - 中断处理函数开始执行处理中断。
对于某些特定类型的x86异常,除了上面图中要保存5五个字之外,还要再压入一个字,叫做错误码。比如页错误,就是其中一个实例。当压入错误码之后,内核堆栈的状态如下:
Nested Exceptions and Interrupts
处理器在用户态下和内核态下都可以处理异常或中断。只有当处理器从用户态切换到内核态时,才会自动地切换堆栈,并且把一些寄存器中的原来的值压入到堆栈上,并且调用IDT指定的合适的异常处理程序。但如果处理器已经由于正在处理中断而处在内核态下时(CS
寄存器的低两位已经都是0),此时CPU
只会向内核堆栈压入更多的值。通过这种方式,内核就可处理嵌套中断。
如果处理器已经在内核态下并且遇到嵌套中断,因为它不需要切换堆栈,所以它不需要存储原来的SS
,ESP
寄存器的值。如果这个异常类型不压入错误码,此时内核堆栈的就像下面这个样子:
这里有一个重要的警告,如果处理器在内核态下接受一个异常,而且由于一些原因,比如堆栈空间不足,不能把当前的状态信息(寄存器的值)压入到内核堆栈中时,那么处理器是无法恢复到原来的状态了,它会自动重启。
Setting Up the IDT
(又要准备干活了)
你现在应该有了建立IDT表以及JOS处理异常的基本信息。我们现在只需要开始建立表就行了。
是否记得lab 2
里面的内存分布,最低的那一页就是存这个的。
然后我们去看看inc/trap.h
,那个kern/trap.h
自己看看就行了。
如果想知道各个中断具体是啥看这个。
trap.h
1 |
|
最后你要实现的控制流的效果如下:
每一个中断或异常都有相应定义在trapentry.S
中中断处理程序,trap_init()
将用这些中断处理程序的地址初始化IDT
。每一个处理程序都应该在堆栈上构建一个结构体struct Trapframe
,并且调用trap()
函数指向这个结构体,trap()
然后处理异常/中断,给他分配一个中断处理函数。
练习4
要你编辑上面说这些东西。我们跟着他走,TRAPHANDLER_NOEC
和TRAPHANDLER_NOEC
,我们看看是啥。
TRAPHANDLER_NOEC和TRAPHANDLER_NOEC
在这个文件里面,也就是为每个中断创建一个函数,然后调用trap()
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###################################################################
###################################################################
/* TRAPHANDLER defines a globally-visible function for handling a trap. 定义了一个全局可见的函数,用来处理trap
* It pushes a trap number onto the stack, then jumps to _alltraps.
* Use TRAPHANDLER for traps where the CPU automatically pushes an error code.
* 他会把 陷阱号自推入堆栈,然后跳转 _alltraps,使用这个可以自动推入 错误码。
* You shouldn't call a TRAPHANDLER function from C, but you may
* need to _declare_ one in C (for instance, to get a function pointer
* during IDT setup). You can declare the function with
* void NAME(); 如果你想在C里面用要声明一下
* where NAME is the argument passed to TRAPHANDLER.
*/
/* 翻译过来 就是创建了一个 函数,name ,然后做了下面这些事*/
.globl name; /* define global symbol for 'name' 第一全局符号name */ \
.type name, @function; /* symbol type is function 符号类型是函数*/ \
.align 2; /* align function definition 对齐函数定义 */ \
name: /* function starts here 函数定义 */ \
pushl $(num); \
jmp _alltraps
/* Use TRAPHANDLER_NOEC for traps where the CPU doesn't push an error code.
* It pushes a 0 in place of the error code, so the trap frame has the same
* format in either case. 这个 和上面的区别就是不会 压入 错误码,用0来替代了??
*/
.globl name; \
.type name, @function; \
.align 2; \
name: \
pushl $0; \
pushl $(num); \
jmp _alltraps
inc/trap.h
已经分析过了。然后他说 我们需要实现_alltraps
。还需要在trap_init()
里面实现初始化入口定义。然后SETGATE
会帮助我们。所以我们去看看STEGATE
干了啥.
由于我并不知道他在哪,所以我们用grep
搜一下。发现在mmu.h
里面,上次我们分析了一部分,因为后面的没有用上,我就注释了一部分。如果已经知道的了就直接跳过。
1 | // Set up a normal interrupt/trap gate descriptor. 设置一个正常中断陷阱入口 描述符 |
后面就是告诉你_alltraps
要实现啥。我们还是先实现第一个trapentry.S
.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.text
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
/* 我现在也不知道为啥这个是这个 那个是那个*/
TRAPHANDLER_NOEC(t_divide, T_DIVIDE)
TRAPHANDLER_NOEC(t_debug, T_DEBUG)
TRAPHANDLER_NOEC(t_nmi, T_NMI)
TRAPHANDLER_NOEC(t_brkpt, T_BRKPT)
TRAPHANDLER_NOEC(t_oflow, T_OFLOW)
TRAPHANDLER_NOEC(t_bound, T_BOUND)
TRAPHANDLER_NOEC(t_illop, T_ILLOP)
TRAPHANDLER_NOEC(t_device, T_DEVICE)
TRAPHANDLER(t_dblflt, T_DBLFLT)
TRAPHANDLER(t_tss, T_TSS)
TRAPHANDLER(t_segnp, T_SEGNP)
TRAPHANDLER(t_stack, T_STACK)
TRAPHANDLER(t_gpflt, T_GPFLT)
TRAPHANDLER(t_pgflt, T_PGFLT)
TRAPHANDLER_NOEC(t_fperr, T_FPERR)
TRAPHANDLER(t_align, T_ALIGN)
TRAPHANDLER_NOEC(t_mchk, T_MCHK)
TRAPHANDLER_NOEC(t_simderr, T_SIMDERR)
TRAPHANDLER_NOEC(t_syscall, T_SYSCALL)
/*
* Lab 3: Your code here for _alltraps
*/
_alltraps:
pushl %ds
pushl %es
pushal /* push all general registers */
movl $GD_KD, %eax
movw %ax, %ds
movw %ax, %es
push %esp
call trap
然后 trap_init()
;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
49void t_divide();
void t_debug();
void t_nmi();
void t_brkpt();
void t_oflow();
void t_bound();
void t_illop();
void t_device();
void t_dblflt();
void t_tss();
void t_segnp();
void t_stack();
void t_gpflt();
void t_pgflt();
void t_fperr();
void t_align();
void t_mchk();
void t_simderr();
void t_syscall();
void
trap_init(void)
{
extern struct Segdesc gdt[];
// LAB 3: Your code here.
SETGATE(idt[T_DIVIDE], 0, GD_KT, t_divide, 0);
SETGATE(idt[T_DEBUG], 0, GD_KT, t_debug, 0);
SETGATE(idt[T_NMI], 0, GD_KT, t_nmi, 0);
SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3);
SETGATE(idt[T_OFLOW], 0, GD_KT, t_oflow, 0);
SETGATE(idt[T_BOUND], 0, GD_KT, t_bound, 0);
SETGATE(idt[T_ILLOP], 0, GD_KT, t_illop, 0);
SETGATE(idt[T_DEVICE], 0, GD_KT, t_device, 0);
SETGATE(idt[T_DBLFLT], 0, GD_KT, t_dblflt, 0);
SETGATE(idt[T_TSS], 0, GD_KT, t_tss, 0);
SETGATE(idt[T_SEGNP], 0, GD_KT, t_segnp, 0);
SETGATE(idt[T_STACK], 0, GD_KT, t_stack, 0);
SETGATE(idt[T_GPFLT], 0, GD_KT, t_gpflt, 0);
SETGATE(idt[T_PGFLT], 0, GD_KT, t_pgflt, 0);
SETGATE(idt[T_FPERR], 0, GD_KT, t_fperr, 0);
SETGATE(idt[T_ALIGN], 0, GD_KT, t_align, 0);
SETGATE(idt[T_MCHK], 0, GD_KT, t_mchk, 0);
SETGATE(idt[T_SIMDERR], 0, GD_KT, t_simderr, 0);
SETGATE(idt[T_SYSCALL], 0, GD_KT, t_syscall, 3);
// Per-CPU setup
trap_init_percpu();
}
用这个可以过了,但是我看到一个非常骚的操作,也就是挑战
.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
.text; \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
.if ec==0; \
pushl $0; \
.endif; \
pushl $(num); \
jmp _alltraps; \
.data; \
.long num, name, user
.data
.globl trapEntry
trapEntry:
.text
TRAPHANDLER(trapEntry0, T_DIVIDE, 0, 0);
TRAPHANDLER(trapEntry1, T_DEBUG, 0, 0);
TRAPHANDLER(trapEntry2, T_NMI, 0, 0);
TRAPHANDLER(trapEntry3, T_BRKPT, 0, 3);
TRAPHANDLER(trapEntry4, T_OFLOW, 0, 0);
TRAPHANDLER(trapEntry5, T_BOUND, 0, 0);
TRAPHANDLER(trapEntry6, T_ILLOP, 0, 0);
TRAPHANDLER(trapEntry7, T_DEVICE, 0, 0);
TRAPHANDLER(trapEntry8, T_DBLFLT, 1, 0);
TRAPHANDLER(trapEntry10, T_TSS, 1, 0);
TRAPHANDLER(trapEntry11, T_SEGNP, 1, 0);
TRAPHANDLER(trapEntry12, T_STACK, 1, 0);
TRAPHANDLER(trapEntry13, T_GPFLT, 1, 0);
TRAPHANDLER(trapEntry14, T_PGFLT, 1, 0);
TRAPHANDLER(trapEntry16, T_FPERR, 0, 0);
TRAPHANDLER(trapEntry17, T_ALIGN, 1, 0);
TRAPHANDLER(trapEntry18, T_MCHK, 0, 0);
TRAPHANDLER(trapEntry19, T_SIMDERR, 0, 0);
//TRAPHANDLER(trapEntry20, T_SYSCALL, 1, 3);
.data
.long 0, 0, 0
/*
* Lab 3: Your code here for _alltraps
*/
.text
_alltraps:
pushl %ds
pushl %es
pushal /* push all general registers */
movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es
pushl %esp
call trap
void
trap_init(void)
{
extern struct Segdesc gdt[];
extern long trapEntry[][3];
// trapEntry[][0]: interrupt/exception vector
// trapEntry[][1]: interrupt/exception handler trapEntry point
// trapEntry[][2]: DPL
for (int i = 0; trapEntry[i][1] != 0; i++ )
SETGATE(idt[trapEntry[i][0]], 0, GD_KT, trapEntry[i][1], trapEntry[i][2]);
// Per-CPU setup
trap_init_percpu();
}
神仙写法,看不懂,但是大致能理解啥意思。
骚不过,骚不过,真的骚不过。
Question
第一个没有必要回答了吧。不同中断处理不同。
第二个问题,好像问user/softint
为啥会产生 trap 13
中断。
查看user/softint.c
1
2
3
4
5
6
7
8
9// buggy program - causes an illegal software interrupt
void
umain(int argc, char **argv)
{
asm volatile("int $14"); // page fault
}
调用int $14
产生了一个软中断。当异常或中断是由int n
,int 3
,int
0指令产生时,处理器才会检查中断或陷阱门的DPL
。此时CPL
数值上必须小于或等于DPL
。这个限制可以防止特权级为3的应用程序使用软件中断访问重要的异常处理过程。当用户级使用软件中断时会引发一个General Protection Exception
,即trap 13
。
Part B: Page Faults, Breakpoints Exceptions, and System Calls
我们现在已经有了处理一部分中断的能力了,然我们来看看他做了啥。在中断最后一个函数_alltraps
调用了trap()
,然后我们去了kern/trap()
里面。我们来分析分析。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
41void
trap(struct Trapframe *tf)
{
// The environment may have set DF and some versions
// of GCC rely on DF being clear CLD 清除DF 复位 干啥的也不知道
asm volatile("cld" ::: "cc");
// Check that interrupts are disabled. If this assertion
// fails, DO NOT be tempted to fix it by inserting a "cli" in
// the interrupt path. 看中断有没有关了
assert(!(read_eflags() & FL_IF));//检查EFLAGS寄存器的IF标志位是否置0,即忽略可屏蔽的外部中断
cprintf("Incoming TRAP frame at %p\n", tf);
if ((tf->tf_cs & 3) == 3) {//if语句判断TrapFrame中的cs寄存器的CPL是否等于3,即是否是从用户态触发的中断
//如果从用户态触发的中断,检查当前进程是否存在,这个应该是检查monitor下是不能出现中断的,然后更新当前进程的env_tf域,并最终将tf指针更新为进程的env_tf域的指针,这么做的原因会在下一篇文章[启动用户进程,产生中断、系统调用的过程分析]中说明
// Trapped from user mode.
assert(curenv);
// Copy trap frame (which is currently on the stack)
// into 'curenv->env_tf', so that running the environment
// will restart at the trap point.
curenv->env_tf = *tf;
// The trapframe on the stack should be ignored from here on.
tf = &curenv->env_tf;
}
// Record that tf is the last real trapframe so
// print_trapframe can print some additional information.
//更新last_tf
last_tf = tf;
// Dispatch based on what type of trap occurred
//于发生的中断的类型进行分发。
trap_dispatch(tf);
// Return to the current environment, which should be running.
//回到进程的用户态
assert(curenv && curenv->env_status == ENV_RUNNING);
env_run(curenv);
}
也就是说 ,我们在 trap_dispatch()
对中断进行了分配。
Handling Page Faults
缺页故障的中断向量为14(T_PGFLT)
是一个很重要的异常,因为我们在后续的实验中,非常依赖于能够处理缺页中断的能力。当缺页中断发生时,系统会把引起中断的线性地址存放到控制寄存器CR2
中。在trap.c
中,已经提供了一个能够处理这种缺页异常的函数page_fault_handler()
。
所以我们就要分配到这个函数。这个if else
或者switch
判断一下就行,没啥说的不需要先做任何操作。1
2
3
4switch(tf->tf_trapno) {
case (T_PGFLT):
page_fault_handler(tf);
break;
就这样就行了。接着我们去看看 page_fault_handler()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24void
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 && 0x01 == 0) { //发生在内核态 就报错,因为如果是内核态出错,说明内核出问题了
panic("page_fault in kernel mode, fault address %d\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);
}
后面还会继续完善,当我们完成系统调用
The Breakpoint Exception
断点异常的中断向量为3(T_BRKPT)
,这个异常可以让调试器能够给程序加上断点。加断点的基本原理就是把要加断点的语句用一个1
字节的INT 3
软件中断指令替换,执行到INT 3
时,会触发软中断。在JOS
中,我们将通过把这个异常转换成一个伪系统调用,这样的话任何用户环境都可以使用这个伪系统调用来触发JOS
kernel monitor
。如果将JOS
kernel monitor
当做原始的调试器的话,断点异常的这种用法实际上是合理的。lib/panic.c
中panic()
函数的用户态实现就是在展示panic
信息之后,调用int 3
。
这个我也每个搞懂,为啥是调用monitor
.
1 | case (T_BRKPT): |
后面的挑战,是要我们实现,单步调试啥的。我不会告辞。
Question
- 问你为啥运行
breakpoint
(怎么运行这个,前面有个练习是说了run-name
)可以是General Protection
也可以是是Breakpoint
.这个是由trap_init
初始化的时候做的。和练习二是一样的问题。SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3);
把最后这个3
换成0
,你再跑一下就知道为啥了。DPL
字段代表的含义是段描述符优先级(Descriptor Privileged Level)
,如果我们想要当前执行的程序能够跳转到这个描述符所指向的程序哪里继续执行的话,有个要求,就是要求当前运行程序的CPL
,RPL
的最大值需要小于等于DPL
,否则就会出现优先级低的代码试图去访问优先级高的代码的情况,就会触发general protection exception
。那么我们的测试程序首先运行于用户态,它的CPL
为3
,当异常发生时,它希望去执行int 3
指令,这是一个系统级别的指令,用户态命令的CPL
一定大于int 3
的DPL
,所以就会触发general protection exception
,但是如果把IDT
这个表项的DPL
设置为3时,就不会出现这样的现象了,这时如果再出现异常,肯定是因为我们还没有编写处理break point exception
的程序所引起的,所以是break point exception
。 简单来说,就是breakpoint
假如设置在内核态,用户态就需要保护一下,进入内核态。 - 这个和上面差不多。
System calls
用户程序通过系统调用让内核帮它做事。当用户程序触发系统调用,处理器进入内核态。处理器和内核合作保存该用户程序当前的状态,然后由内核将执行相应的代码完成系统调用,最终回到用户程序继续执行。而用户程序到底是如何引起内核的注意,以及它如何说明它希望操作系统做什么事情的方法是有很多不同的实现方式的。
在JOS
内核中,我们会采用int
指令触发一个处理器的中断。特别的,我们用int $0x30
来代表系统调用中断。注意,中断0x30
不是通过硬件产生的,应该允许用户代码能够产生0x30
中断。
应用程序会把系统调用号以及系统调用的参数放到寄存器中。通过这种方法,内核就不需要去查询用户程序的堆栈或指令流了。系统调用号存放到%eax
中,参数则存放在%edx
,%ecx
,%ebx
,%edi
, 和 %esi
中。内核会把返回值送到%eax
中。在lib/syscall.c
中的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
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// System call stubs.
/* 来自一个大佬
* 在JOS中所有系统调用通过syscall这个函数进行:执行int T_SYSCALL,把函数参数存入若干指定的寄存器
* 并指定函数返回值返回到寄存器ax中
* 用第一个参数num来确定到底是哪个系统调用
* 参数num == SYS_cputs,check == 0,a1 == b->buf, a2 == b->idx,剩下a3、a4、a5都为0
*/
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;
asm volatile("int %1\n" //汇编指令模板,%1是占位符,对应后面的T_SYSCALL
: "=a" (ret) //=表示在汇编里只能改变该C变量的值,而不能取它的值
//ret值与%ax相联系,即指令执行完后ax的值存入变量ret
: "i" (T_SYSCALL), //中断向量T_SYSCALL,是立即数
"a" (num), //输入参数num,指令执行前先将num变量的值存入%ax
"d" (a1), //输入参数a1,指令执行前先将a1变量的值存入%dx
"c" (a2), //参数a2存入%cx
"b" (a3), //参数a3存入%bx
"D" (a4), //参数a4存入%di
"S" (a5), //参数a5存入%si
: "cc", "memory"); //向gcc声明在这条汇编语言执行后,标志寄存器eflags和内存可能发生改变
//加入“memory”,告诉GCC内存已经被修改,GCC得知这个信息后,
//就会在这段指令之前,插入必要的指令将前面因为优化缓存到寄存器中
//的变量值先写回内存,如果以后又要使用这些变量再重新读取。
if(check && ret > 0)
panic("syscall %d returned %d (> 0)", num, ret);
return ret;
}
//下面是各个函数。
//输出?? 在控制台输入输出 是要进入内核态的
void
sys_cputs(const char *s, size_t len)
{
syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}
//获取???
int
sys_cgetc(void)
{
return syscall(SYS_cgetc, 0, 0, 0, 0, 0, 0);
}
//删除???
int
sys_env_destroy(envid_t envid)
{
return syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0);
}
//获取id???
envid_t
sys_getenvid(void)
{
return syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0);
}
看完之后来做练习7
,那个啥,让我们去把这个加入异常,我们原本就已经加入进去了,不用管。后面我就看不懂了…,我发现系统内核里面还有个kern/syscall.c
这个是干啥的。到底调用哪个。。。别人说kern/syscall.c
是外壳,但是我个人感觉inc/syscall.c
才是。我觉得应该是inc/syscall.c
调用了kern/syscal.c
不知道对不对,我单步调试,并查看hello.asm
文件其中调用了sys_getenvid
。将断点打到0x800b15
可以看见。
要在lib/libmain.c
里面调用sys_getenvid
。先不用管这个是啥,下个实验会讲,先把这个添进去调试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17void
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();
}
1 | 800b15: 55 push %ebp |
能够明显的看见调用额 int30
,所以应该是 用户通过inc/syscall.c
进行系统调用。
后面就比较简单了。
前面也已经提示你了,所以我们直接调用就可以了。1
2
3
4
5
6
7
8
9case (T_SYSCALL):
ret_code = 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_code;
在sysycall
里面判断信号,分别调用哪几个函数。
1 | // Dispatches to the correct kernel function, passing the arguments. |
大家多用gdb
调试自己查看程序运行过程,这样可以理解更快。
挑战我就不看了,一般都是做不出来的,主要是没时间查看相关资料。
User-mode startup
上一个实验已经把代码给了,最后那一块如果好好理解了的话,这个基本上就能直接过了。
用户程序真正开始运行的地方是在lib/entry.S
文件中。该文件中,首先会进行一些设置,然后就会调用lib/libmain.c
文件中的 libmain()
函数。你首先要修改一下 libmain()
函数,使它能够初始化全局指针 thisenv
,让它指向当前用户环境的 Env 结构体。
然后 libmain()
函数就会调用 umain
,这个 umain
程序恰好是 user/hello.c
中被调用的函数。在之前的实验中我们发现,hello.c
程序只会打印 hello, world
这句话,然后就会报出 page fault
异常,原因就是 thisenv->env_id
这条语句。现在你已经正确初始化了这个 thisenv
的值,再次运行就应该不会报错了。
不理解的可以继续单步调试。断点打在f0103003
Page faults and memory protection
这个练习,我们已经做了一点了,前那个函数分配page_fault_handler
的时候我已经把page_fault_handler
完善了。这里就是告诉你 内核如果缺页,说明内核出问题了,不能继续运行了,必须报错panic。如果是用户能解决就解决,解决不了就删除。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23void
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 && 0x01 == 0) {
panic("page_fault in kernel mode, fault address %d\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);
}
然后根据题目的要求,我们还要继续完善 kern/pmap.c 文件中的 user_mem_assert , user_mem_check 函数,通过观察 user_mem_assert 函数我们发现,它调用了 user_mem_check 函数。而 user_mem_check 函数的功能是检查一下当前用户态程序是否有对虚拟地址空间 [va, va+len] 的 perm| PTE_P 访问权限。
自然我们要做的事情应该是,先找到这个虚拟地址范围对应于当前用户态程序的页表中的页表项,然后再去看一下这个页表项中有关访问权限的字段,是否包含 perm | PTE_P,只要有一个页表项是不包含的,就代表程序对这个范围的虚拟地址没有 perm|PTE_P 的访问权限。以上就是这段代码的大致思想。
1 | //这个函数分析 先挖个坑,做下个实验之前,来填一下。 |
1 | // Print a string to the system console. |
最终的trap_dispatch
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
35static void
trap_dispatch(struct Trapframe *tf)
{
// Handle processor exceptions.
// LAB 3: Your code here.
// Unexpected trap: The user process or the kernel has a bug.
switch(tf->tf_trapno) {
case (T_PGFLT):
page_fault_handler(tf);
break;
case (T_BRKPT):
monitor(tf);
break;
case (T_SYSCALL):
// print_trapframe(tf);
int32_t ret_code = 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_code;
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;
}
}
}
如果文章有错误或者看不懂,缺了啥的可以留言。