MIT 6.828 (三) Lab 3: User Environments

(最近有点事,做的比较慢。哦,不,抄的比较慢。。。)

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

/* See COPYRIGHT for copyright information. */

#ifndef JOS_INC_ENV_H
#define JOS_INC_ENV_H

#include 
#include 
#include 

typedef int32_t envid_t; //用户环境ID 变量,32位的。

// An environment ID 'envid_t' has three parts:
//
// +1+---------------21-----------------+--------10--------+
// |0|          Uniqueifier             |   Environment    |
// | |                                  |      Index       |
// +------------------------------------+------------------+
//                                       \--- ENVX(eid) --/
//
// The environment index ENVX(eid) equals the environment's index in the
// 'envs[]' array.  The uniqueifier distinguishes environments that were
// created at different times, but share the same environment index.
// 这个ENV(eid) 可以获取在envs 数组里面的第几个。
// All real environments are greater than 0 (so the sign bit is zero).
// envid_ts less than 0 signify errors.  The envid_t == 0 is special, and
// stands for the current environment.  所有的 环境是大于0 的,envid_ts小于0是错误的 ,envid_t == 0 标示当前正在运行
//最大能支持同时活跃的进程数量
#define LOG2NENV		10
#define NENV			(1 << LOG2NENV)
#define ENVX(envid)		((envid) & (NENV - 1))

// Values of env_status in struct Env
enum {
	ENV_FREE = 0,//空闲
	ENV_DYING,//僵尸进程
	ENV_RUNNABLE,//准备就绪
	ENV_RUNNING,//运行态
	ENV_NOT_RUNNABLE//阻塞状态
};

// Special environment types 环境的特殊类型
enum EnvType {
	ENV_TYPE_USER = 0,
};

// 环境结构体   就是一个PCB 对这个有兴趣的同志可以看看我 的剖析 linux1.0 源码,这个就是简化版的那个东西。
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 这个变量存放着这个环境的页目录的虚拟地址
};
#endif // !JOS_INC_ENV_H

分析完后,就去看 kern/env.c
这个文件先不看完,就看看他定义了什么东西。

struct 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分配空间一样,并进行映射。

	//
	// 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是一模一样的。

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

另外再复习一下上节课的内存分配 (下面又是盗的图,哈哈~ )
MIT 6.828 (三) Lab 3: User Environments_第1张图片


Creating and Running Environments

现在你需要去编写 kern/env.c 文件来运行一个用户环境了。由于你现在没有文件系统,所以必须把内核设置成能够加载内核中的静态二进制程序映像文件。
Lab3 里面的 GNUmakefile 文件在obj/user/目录下面生成了一系列的二进制映像文件。如果你看一下 kern/Makefrag 文件,你会发现一些奇妙的地方,这些地方把二进制文件直接链接到内核可执行文件中,只要这些文件是.o文件。其中在链接器命令行中的-b binary 选项会使这些文件被当做二进制执行文件链接到内核之后。
kern/ini.c中的i386_init(),你会看到代码运行的环境中,这些二进制图像之一。然而,关键的功能设置用户环境是不完整的;您需要填写他们进来。
我们照着他的意思去看看,发现相较于前几次实验,多了几行。

	// Lab 3 user environment initialization functions
	env_init();
	trap_init();

#if defined(TEST)
	// Don't touch -- used by grading script!  这些不要碰,是从来测试的
	ENV_CREATE(TEST, ENV_TYPE_USER);  //env_create
#else
	// Touch all you want.
	ENV_CREATE(user_hello, ENV_TYPE_USER);
#endif // TEST*

	// We only have one user environment for now, so just run it.
	env_run(&envs[0]);

kern/env.h 里面可以看见这个宏的原型,就当他运行了几个不同的测试吧。我没找到这几个在哪。

#define ENV_PASTE3(x, y, z) x ## y ## z

#define ENV_CREATE(x, type)						\
	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()

// Mark all environments in 'envs' as free, set their env_ids to 0,
// and insert them into the env_free_list. 把所有env 加入 空闲列表,然后设置 id=0
// Make sure the environments are in the free list in the same order
// they are in the envs array (i.e., so that the first call to
// env_alloc() returns envs[0]). 就是顺序从 0 递增 
//
void
env_init(void)
{
	// Set up envs array
	// LAB 3: Your code here.
	//上面分析过 要从0 开始,所以我们倒着遍历。
	env_free_list=NULL;
	for	(size_t i=NENV-1;i>=0;i--){
		envs[i]->env_id=0;
		envs[i]->env_status=ENV_FREE;
		envs[i]->env_link=env_free_list;
		env_free_list=&envs[i];
	}
	// Per-CPU part of the initialization
	env_init_percpu();
}

env_init() 中调用了env_init_percpu() 不知道这个是干啥的。根据注释,是初始化了GDT和段描述符。

// 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,创建这个第一个要干的肯定是分配内存,最开始要做的是分配一个页目录。这个页目录,肯定是要复制内核的一部分,因为内核那一部分,你是绝对不能动的。

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

分配完页目录,然后就是要给用户创建空间。只有一个页目录,肯定是不行的,你必须要给用户程序使用的空间。

//
// 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,我去看了看这个东西,对于某个字段是干啥的完全没有注释所以我也不知道该分析。

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

//
// Allocates and initializes a new environment.
// On success, the new environment is stored in *newenv_store.
// 分配了一个新的 环境,成功 就存在了 *newenv_store
// Returns 0 on success, < 0 on failure.  Errors include: 失败返回两种
//	-E_NO_FREE_ENV if all NENV environments are allocated
//	-E_NO_MEM on memory exhaustion
//
int
env_alloc(struct Env **newenv_store, envid_t parent_id)
{
	int32_t generation;
	int r;
	struct Env *e;
	//首先判断空闲 环境
	if (!(e = env_free_list))
		return -E_NO_FREE_ENV;
	//设置页目录
	// Allocate and set up the page directory for this environment.
	if ((r = env_setup_vm(e)) < 0)
		return r;

	// Generate an env_id for this environment. 设置  env_id 
	generation = (e->env_id + (1 << ENVGENSHIFT)) & ~(NENV - 1);
	if (generation <= 0)	// Don't create a negative env_id.
		generation = 1 << ENVGENSHIFT;
	e->env_id = generation | (e - envs);

	// Set the basic status variables. 设置基础信息
	e->env_parent_id = parent_id;
	e->env_type = ENV_TYPE_USER;
	e->env_status = ENV_RUNNABLE;
	e->env_runs = 0;

	// Clear out all the saved register state,
	// to prevent the register values
	// of a prior environment inhabiting this Env structure
	// from "leaking" into our new environment. 清空寄存器状态
	memset(&e->env_tf, 0, sizeof(e->env_tf));

	// Set up appropriate initial values for the segment registers.
	// GD_UD is the user data segment selector in the GDT, and
	// GD_UT is the user text segment selector (see inc/memlayout.h).
	// The low 2 bits of each segment register contains the
	// Requestor Privilege Level (RPL); 3 means user mode.  When
	// we switch privilege levels, the hardware does various
	// checks involving the RPL and the Descriptor Privilege Level
	// (DPL) stored in the descriptors themselves. 设置初始值
	e->env_tf.tf_ds = GD_UD | 3;
	e->env_tf.tf_es = GD_UD | 3;
	e->env_tf.tf_ss = GD_UD | 3;
	e->env_tf.tf_esp = USTACKTOP;
	e->env_tf.tf_cs = GD_UT | 3;
	// You will set e->env_tf.tf_eip later.  这个很眼熟吧,就是上个函数用的,这个就是入口地址

	// commit the allocation 空闲环境 指向另一个。
	env_free_list = e->env_link;
	*newenv_store = e;

	cprintf("[%08x] new env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
	return 0;
}

env_create

函数作用就是根据binary 创建一个env

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

这个就是真正的用户环境运行了。

/
// 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,上面那个函数已经转移了,等他再来运行这一行,说明整个操作系统已经结束了。
}

我们再分析分析这个文件里面一些其他的函数。


//
// Frees env e and all memory it uses.
//
void
env_free(struct Env *e)
{
	pte_t *pt;
	uint32_t pdeno, pteno;
	physaddr_t pa;

	// If freeing the current environment, switch to kern_pgdir
	// before freeing the page directory, just in case the page
	// gets reused.
	if (e == curenv)
		lcr3(PADDR(kern_pgdir));  //切换到内核

	// Note the environment's demise.
	cprintf("[%08x] free env %08x\n", curenv ? curenv->env_id : 0, e->env_id);
//打印信息
	// Flush all mapped pages in the user portion of the address space
	static_assert(UTOP % PTSIZE == 0); //刷新所有映射
	for (pdeno = 0; pdeno < PDX(UTOP); pdeno++) {

		// only look at mapped page tables
		if (!(e->env_pgdir[pdeno] & PTE_P))
			continue;

		// find the pa and va of the page table
		pa = PTE_ADDR(e->env_pgdir[pdeno]);
		pt = (pte_t*) KADDR(pa);

		// unmap all PTEs in this page table 取消所有映射
		for (pteno = 0; pteno <= PTX(~0); pteno++) {
			if (pt[pteno] & PTE_P)
				page_remove(e->env_pgdir, PGADDR(pdeno, pteno, 0));
		}

		// free the page table itself
		e->env_pgdir[pdeno] = 0;
		page_decref(pa2page(pa));
	}

	// free the page directory 把页目录删掉
	pa = PADDR(e->env_pgdir);
	e->env_pgdir = 0;
	page_decref(pa2page(pa));

	// return the environment to the free list
	e->env_status = ENV_FREE;
	e->env_link = env_free_list;
	env_free_list = e;
}

//
// Frees environment e.
//
void
env_destroy(struct Env *e)
{
	env_free(e);

	cprintf("Destroyed the only environment - nothing more to do!\n");
	while (1)
		monitor(NULL);
}

//
// Restores the register values in the Trapframe with the 'iret' instruction.
// This exits the kernel and starts executing some environment's code.
//
// This function does not return. 
//
void
env_pop_tf(struct Trapframe *tf) //这个就是跳转,
{
	asm volatile(
		"\tmovl %0,%%esp\n"
		"\tpopal\n"
		"\tpopl %%es\n"
		"\tpopl %%ds\n"
		"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
		"\tiret\n"
		: : "g" (tf) : "memory");
	panic("iret failed");  /* mostly to placate the compiler */
}

一旦你完成上述子函数的代码,并且在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上,有两种机制配合工作来提供这种保护:

  1. 中断向量表:处理器保证中断和异常只能够引起内核进入到一些特定的,被事先定义好的程序入口点,而不是由触发中断的程序来决定中断程序入口点。
     X86允许多达256个不同的中断和异常,每一个都配备一个独一无二的中断向量。一个向量指的就是0到255中的一个数。一个中断向量的值是根据中断源来决定的:不同设备,错误条件,以及对内核的请求都会产生出不同的中断和中断向量的组合。CPU将使用这个向量作为这个中断在中断向量表中的索引,这个表是由内核设置的,放在内核空间中,和GDT很像。通过这个表中的任意一个表项,处理器可以知道:
    *需要加载到EIP寄存器中的值,这个值指向了处理这个中断的中断处理程序的位置。
    *需要加载到CS寄存器中的值,里面还包含了这个中断处理程序的运行特权级。(即这个程序是在用户态还是内核态下运行。)

  2. 任务状态段:处理器还需要一个地方来存放,当异常/中断发生时,处理器的状态,比如EIP和CS寄存器的值。这样的话,中断处理程序一会可以重新返回到原来的程序中。这段内存自然也要保护起来,不能被用户态的程序所篡改。
        正因为如此,当一个x86处理器要处理一个中断,异常并且使运行特权级从用户态转为内核态时,它也会把它的堆栈切换到内核空间中。一个叫做 “任务状态段(TSS)”的数据结构将会详细记录这个堆栈所在的段的段描述符和地址。处理器会把SSESPEFLAGSCSEIP以及一个可选错误码等等这些值压入到这个堆栈上。然后加载中断处理程序的CSEIP值,并且设置ESPSS寄存器指向新的堆栈。
        尽管TSS非常大,并且还有很多其他的功能,但是JOS仅仅使用它来定义处理器从用户态转向内核态所采用的内核堆栈,由于JOS中的内核态指的就是特权级0,所以处理器用TSS中的ESP0SS0字段来指明这个内核堆栈的位置,大小。

Types of Exceptions and Interrupts

 所有的由X86处理器内部产生的异常的向量值是031之间的整数。比如,页表错所对应的向量值是14.而大于31号的中断向量对应的是软件中断,由int指令生成;或者是外部中断,由外部设备生成。
 在这一章,我们将扩展JOS的功能,使它能够处理0~31号内部异常。在下一章会让JOS能够处理48号软件中断,主要被用来做系统调用。在Lab 4中会继续扩展JOS使它能够处理外部硬件中断,比如时钟中断。

An Example

让我们试一下除0

  1. 处理器会首先切换自己的堆栈,切换到由TSSSS0ESP0字段所指定的内核堆栈区,这两个字段分别存放着GD_KDKSTACKTOP的值。
  2. 处理器把异常参数压入到内核堆栈中,起始于地址KSTACKTOP:MIT 6.828 (三) Lab 3: User Environments_第2张图片
  3. 因为我们要处理的是除零异常,它的中断向量是0,处理器会读取IDT表中的0号表项,并且把CS:EIP的值设置为0号中断处理函数的地址值。
  4. 中断处理函数开始执行处理中断。

对于某些特定类型的x86异常,除了上面图中要保存5五个字之外,还要再压入一个字,叫做错误码。比如页错误,就是其中一个实例。当压入错误码之后,内核堆栈的状态如下:
MIT 6.828 (三) Lab 3: User Environments_第3张图片

Nested Exceptions and Interrupts

处理器在用户态下和内核态下都可以处理异常或中断。只有当处理器从用户态切换到内核态时,才会自动地切换堆栈,并且把一些寄存器中的原来的值压入到堆栈上,并且调用IDT指定的合适的异常处理程序。但如果处理器已经由于正在处理中断而处在内核态下时(CS寄存器的低两位已经都是0),此时CPU只会向内核堆栈压入更多的值。通过这种方式,内核就可处理嵌套中断。

如果处理器已经在内核态下并且遇到嵌套中断,因为它不需要切换堆栈,所以它不需要存储原来的SSESP寄存器的值。如果这个异常类型不压入错误码,此时内核堆栈的就像下面这个样子:
MIT 6.828 (三) Lab 3: User Environments_第4张图片
这里有一个重要的警告,如果处理器在内核态下接受一个异常,而且由于一些原因,比如堆栈空间不足,不能把当前的状态信息(寄存器的值)压入到内核堆栈中时,那么处理器是无法恢复到原来的状态了,它会自动重启。

Setting Up the IDT

(又要准备干活了)

你现在应该有了建立IDT表以及JOS处理异常的基本信息。我们现在只需要开始建立表就行了。
是否记得lab 2里面的内存分布,最低的那一页就是存这个的。
然后我们去看看inc/trap.h,那个kern/trap.h自己看看就行了。
如果想知道各个中断具体是啥看这个。

trap.h

#ifndef JOS_INC_TRAP_H
#define JOS_INC_TRAP_H

// Trap numbers
// These are processor defined:  这是各种中断  对于这些建议大家学学嵌入式,手写个CPU(我的github 上有个简单的...) 下面各种错误还是大家自行百度,我解释几个常用的
#define T_DIVIDE     0		// divide error 除0
#define T_DEBUG      1		// debug exception 
#define T_NMI        2		// non-maskable interrupt 非屏蔽中断???
#define T_BRKPT      3		// breakpoint	断点
#define T_OFLOW      4		// overflow		溢出
#define T_BOUND      5		// bounds check	边界检查?
#define T_ILLOP      6		// illegal opcode	非法操作码  
#define T_DEVICE     7		// device not available 	设备不可用
#define T_DBLFLT     8		// double fault 
/* #define T_COPROC  9 */	// reserved (not generated by recent processors)
#define T_TSS       10		// invalid task switch segment 无效任务段切换
#define T_SEGNP     11		// segment not present 段不存在
#define T_STACK     12		// stack exception 栈异常
#define T_GPFLT     13		// general protection fault
#define T_PGFLT     14		// page fault 页错误
/* #define T_RES    15 */	// reserved
#define T_FPERR     16		// floating point error  浮点错误
#define T_ALIGN     17		// aligment check 对齐检查
#define T_MCHK      18		// machine check  
#define T_SIMDERR   19		// SIMD floating point error

// These are arbitrarily chosen, but with care not to overlap  下面可以任意选择,但是不要重叠
// processor defined exceptions or interrupt vectors. 应该就是 自定义 异常
#define T_SYSCALL   48		// system call
#define T_DEFAULT   500		// catchall

#define IRQ_OFFSET	32	// IRQ 0 corresponds to int IRQ_OFFSET 	 外部中断

// Hardware IRQ numbers. We receive these as (IRQ_OFFSET+IRQ_WHATEVER)
#define IRQ_TIMER        0
#define IRQ_KBD          1
#define IRQ_SERIAL       4
#define IRQ_SPURIOUS     7
#define IRQ_IDE         14
#define IRQ_ERROR       19

#ifndef __ASSEMBLER__

#include 
//保存通用寄存器的值
struct PushRegs {
	/* registers as pushed by pusha */
	uint32_t reg_edi;
	uint32_t reg_esi;
	uint32_t reg_ebp;
	uint32_t reg_oesp;		/* Useless */
	uint32_t reg_ebx;
	uint32_t reg_edx;
	uint32_t reg_ecx;
	uint32_t reg_eax;
} __attribute__((packed));
//任务段
struct Trapframe {
	struct PushRegs tf_regs;
	uint16_t tf_es;
	uint16_t tf_padding1;
	uint16_t tf_ds;
	uint16_t tf_padding2;
	uint32_t tf_trapno;
	/* below here defined by x86 hardware   下面是x86 硬件定义的 */
	uint32_t tf_err;
	uintptr_t tf_eip;
	uint16_t tf_cs;
	uint16_t tf_padding3;
	uint32_t tf_eflags;
	/* below here only when crossing rings, such as from user to kernel 不知道是啥*/
	uintptr_t tf_esp;
	uint16_t tf_ss;
	uint16_t tf_padding4;
} __attribute__((packed));


#endif /* !__ASSEMBLER__ */

#endif /* !JOS_INC_TRAP_H */

最后你要实现的控制流的效果如下:
MIT 6.828 (三) Lab 3: User Environments_第5张图片每一个中断或异常都有相应定义在trapentry.S中中断处理程序,trap_init()将用这些中断处理程序的地址初始化IDT。每一个处理程序都应该在堆栈上构建一个结构体struct Trapframe,并且调用trap()函数指向这个结构体,trap()然后处理异常/中断,给他分配一个中断处理函数。

练习4 要你编辑上面说这些东西。我们跟着他走,TRAPHANDLER_NOECTRAPHANDLER_NOEC,我们看看是啥。

TRAPHANDLER_NOEC和TRAPHANDLER_NOEC

在这个文件里面,也就是为每个中断创建一个函数,然后调用trap()

###################################################################
# exceptions/interrupts
###################################################################

/* 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 ,然后做了下面这些事*/
#define TRAPHANDLER(name, num)						\
	.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来替代了??
 */
#define TRAPHANDLER_NOEC(name, num)					\
	.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里面,上次我们分析了一部分,因为后面的没有用上,我就注释了一部分。如果已经知道的了就直接跳过。

// Set up a normal interrupt/trap gate descriptor. 设置一个正常中断陷阱入口 描述符
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate. 1是trap 0是interrupt
    //   see section 9.6.1.3 of the i386 reference: "The difference between //看看那个啥
    //   an interrupt gate and a trap gate is in the effect on IF (the  //中断门和陷阱门有啥不一样在IF(中断允许的标志)上面
    //   interrupt-enable flag). An interrupt that vectors through an//中断向量通过 中断门重置 IF 从而组织其他中断中断当前中断。 
    //   interrupt gate resets IF, thereby preventing other interrupts from
    //   interfering with the current interrupt handler. A subsequent IRET
	// 然后然后用IRET 恢复。
    //   instruction restores IF to the value in the EFLAGS image on the
    //   stack. An interrupt through a trap gate does not change IF."
    //说的简单点,中断不能再次中断,trap 可以被中断。
// - sel: Code segment selector for interrupt/trap handler 代码段地址
// - off: Offset in code segment for interrupt/trap handler //代码段偏移
// - dpl: Descriptor Privilege Level - 特权等级
//	  the privilege level required for software to invoke //软件等级
//	  this interrupt/trap gate explicitly using an int instruction.//int 指令调用?
#define SETGATE(gate, istrap, sel, off, dpl)			\
{								\
	(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;		\
	(gate).gd_sel = (sel);					\
	(gate).gd_args = 0;					\
	(gate).gd_rsv1 = 0;					\
	(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32;	\
	(gate).gd_s = 0;					\
	(gate).gd_dpl = (dpl);					\
	(gate).gd_p = 1;					\
	(gate).gd_off_31_16 = (uint32_t) (off) >> 16;		\
}

// Set up a call gate descriptor.  //建立呼叫门描述??? 和上面好像没啥差距,就是少了个istrap
#define SETCALLGATE(gate, sel, off, dpl)           	        \
{								\
	(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff;		\
	(gate).gd_sel = (sel);					\
	(gate).gd_args = 0;					\
	(gate).gd_rsv1 = 0;					\
	(gate).gd_type = STS_CG32;				\
	(gate).gd_s = 0;					\
	(gate).gd_dpl = (dpl);					\
	(gate).gd_p = 1;					\
	(gate).gd_off_31_16 = (uint32_t) (off) >> 16;		\
}

后面就是告诉你_alltraps 要实现啥。我们还是先实现第一个trapentry.S.

.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()

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

用这个可以过了,但是我看到一个非常骚的操作,也就是挑战.

#define TRAPHANDLER(name, num, ec, user)						\
.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

// buggy program - causes an illegal software interrupt

#include 

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()里面。我们来分析分析。

void
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 判断一下就行,没啥说的不需要先做任何操作。

    switch(tf->tf_trapno) {
        case (T_PGFLT):
            page_fault_handler(tf);
            break; 

就这样就行了。接着我们去看看 page_fault_handler()

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 && 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.cpanic()函数的用户态实现就是在展示panic信息之后,调用int 3

这个我也每个搞懂,为啥是调用monitor.

        case (T_BRKPT):
            monitor(tf);        
            break;

后面的挑战,是要我们实现,单步调试啥的。我不会告辞。

Question

  1. 问你为啥运行breakpoint(怎么运行这个,前面有个练习是说了 run-name)可以是General Protection 也可以是是Breakpoint.这个是由trap_init 初始化的时候做的。和练习二是一样的问题。SETGATE(idt[T_BRKPT], 0, GD_KT, t_brkpt, 3);把最后这个3 换成0,你再跑一下就知道为啥了。DPL字段代表的含义是段描述符优先级(Descriptor Privileged Level),如果我们想要当前执行的程序能够跳转到这个描述符所指向的程序哪里继续执行的话,有个要求,就是要求当前运行程序的CPLRPL的最大值需要小于等于DPL,否则就会出现优先级低的代码试图去访问优先级高的代码的情况,就会触发general protection exception。那么我们的测试程序首先运行于用户态,它的CPL3,当异常发生时,它希望去执行 int 3指令,这是一个系统级别的指令,用户态命令的CPL一定大于 int 3DPL,所以就会触发general protection exception,但是如果把IDT这个表项的DPL设置为3时,就不会出现这样的现象了,这时如果再出现异常,肯定是因为我们还没有编写处理break point exception的程序所引起的,所以是break point exception。 简单来说,就是breakpoint假如设置在内核态,用户态就需要保护一下,进入内核态。
  2. 这个和上面差不多。

System calls

用户程序通过系统调用让内核帮它做事。当用户程序触发系统调用,处理器进入内核态。处理器和内核合作保存该用户程序当前的状态,然后由内核将执行相应的代码完成系统调用,最终回到用户程序继续执行。而用户程序到底是如何引起内核的注意,以及它如何说明它希望操作系统做什么事情的方法是有很多不同的实现方式的。

JOS内核中,我们会采用int指令触发一个处理器的中断。特别的,我们用int $0x30来代表系统调用中断。注意,中断0x30不是通过硬件产生的,应该允许用户代码能够产生0x30中断。

应用程序会把系统调用号以及系统调用的参数放到寄存器中。通过这种方法,内核就不需要去查询用户程序的堆栈或指令流了。系统调用号存放到%eax中,参数则存放在%edx,%ecx,%ebx,%edi, 和 %esi 中。内核会把返回值送到%eax中。在lib/syscall.c中的syscall()函数就是触发一个系统调用的代码。不用说了,我们先去看看。

// System call stubs.

#include 
#include 

/* 来自一个大佬
* 在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。先不用管这个是啥,下个实验会讲,先把这个添进去调试。

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

  800b15:	55                   	push   %ebp
  800b16:	89 e5                	mov    %esp,%ebp
  800b18:	57                   	push   %edi
  800b19:	56                   	push   %esi
  800b1a:	53                   	push   %ebx
	//
	// The last clause tells the assembler that this can
	// potentially change the condition codes and arbitrary
	// memory locations.

	asm volatile("int %1\n"
  800b1b:	ba 00 00 00 00       	mov    $0x0,%edx
  800b20:	b8 02 00 00 00       	mov    $0x2,%eax
  800b25:	89 d1                	mov    %edx,%ecx
  800b27:	89 d3                	mov    %edx,%ebx
  800b29:	89 d7                	mov    %edx,%edi
  800b2b:	89 d6                	mov    %edx,%esi
  800b2d:	cd 30                	int    $0x30

能够明显的看见调用额 int30,所以应该是 用户通过inc/syscall.c进行系统调用。
后面就比较简单了。
前面也已经提示你了,所以我们直接调用就可以了。

        case (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里面判断信号,分别调用哪几个函数。

// Dispatches to the correct kernel function, passing the arguments.
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;
    }
}

大家多用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。如果是用户能解决就解决,解决不了就删除。

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 && 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 的访问权限。以上就是这段代码的大致思想。

//这个函数分析 先挖个坑,做下个实验之前,来填一下。
//
// Check that an environment is allowed to access the range of memory
// [va, va+len) with permissions 'perm | PTE_P'. 检查内存权限
// Normally 'perm' will contain PTE_U at least, but this is not required. 权限至少是PTE_u
// 'va' and 'len' need not be page-aligned; you must test every page that
// contains any of that range.  You will test either 'len/PGSIZE',
// 'len/PGSIZE + 1', or 'len/PGSIZE + 2' pages.
// 这个地方告诉你 ,va 和len 肯能不是页对齐的, 需要你搞一下。
// A user program can access a virtual address if (1) the address is below
// ULIM, and (2) the page table gives it permission.  These are exactly
// the tests you should implement here.
// 地址 应该在ULIM之下 权限应该对
// If there is an error, set the 'user_mem_check_addr' variable to the first
// erroneous virtual address.  如果出错了 把  user_mem_check_addr地址指向第一个出错的
//
// Returns 0 if the user program can access this range of addresses,
// and -E_FAULT otherwise.
//
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;
    start = ROUNDDOWN((char *)va, PGSIZE);   //这个地方是页对齐
    end = ROUNDUP((char *)(va + len), PGSIZE);
    pte_t *cur = NULL; //虚拟地址对应的 物理地址

    for(; start < end; start += PGSIZE) {
        cur = pgdir_walk(env->env_pgdir, (void *)start, 0);  //遍历这个虚拟地址
        //检查地址位置 ,权限
        if((int)start > ULIM || cur == NULL || ((uint32_t)(*cur) & perm) != perm) {
              if(start == ROUNDDOWN((char *)va, PGSIZE)) { //这个的意思是如果一开场就错了说明出错在va
                    user_mem_check_addr = (uintptr_t)va;
              }
              else {
                      user_mem_check_addr = (uintptr_t)start;
              }
              return -E_FAULT;
        }
    }
        
    return 0;
}
// Print a string to the system console.
// The string is exactly 'len' characters long.
// Destroys the environment on memory errors.
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);
}

最终的trap_dispatch

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

如果文章有错误或者看不懂,缺了啥的可以留言。

你可能感兴趣的:(MIT6.828,MIT6.828,操作系统)