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 <inc/types.h>
#include <inc/trap.h>
#include <inc/memlayout.h>

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

另外再複習一下上節課的內存分配 (下面又是盜的圖,哈哈~ )
在這裏插入圖片描述


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:在這裏插入圖片描述
  3. 因爲我們要處理的是除零異常,它的中斷向量是0,處理器會讀取IDT表中的0號表項,並且把CS:EIP的值設置爲0號中斷處理函數的地址值。
  4. 中斷處理函數開始執行處理中斷。

對於某些特定類型的x86異常,除了上面圖中要保存5五個字之外,還要再壓入一個字,叫做錯誤碼。比如頁錯誤,就是其中一個實例。當壓入錯誤碼之後,內核堆棧的狀態如下:
在這裏插入圖片描述

Nested Exceptions and Interrupts

處理器在用戶態下和內核態下都可以處理異常或中斷。只有當處理器從用戶態切換到內核態時,纔會自動地切換堆棧,並且把一些寄存器中的原來的值壓入到堆棧上,並且調用IDT指定的合適的異常處理程序。但如果處理器已經由於正在處理中斷而處在內核態下時(CS寄存器的低兩位已經都是0),此時CPU只會向內核堆棧壓入更多的值。通過這種方式,內核就可處理嵌套中斷。

如果處理器已經在內核態下並且遇到嵌套中斷,因爲它不需要切換堆棧,所以它不需要存儲原來的SSESP寄存器的值。如果這個異常類型不壓入錯誤碼,此時內核堆棧的就像下面這個樣子:
在這裏插入圖片描述
這裏有一個重要的警告,如果處理器在內核態下接受一個異常,而且由於一些原因,比如堆棧空間不足,不能把當前的狀態信息(寄存器的值)壓入到內核堆棧中時,那麼處理器是無法恢復到原來的狀態了,它會自動重啓。

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 <inc/types.h>
//保存通用寄存器的值
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 */

最後你要實現的控制流的效果如下:
在這裏插入圖片描述每一箇中斷或異常都有相應定義在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 <inc/lib.h>

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 <inc/syscall.h>
#include <inc/lib.h>

/* 來自一個大佬
* 在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;
            }
    }
}

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章