MIT6.828学习之Lab3_Part A: User Environments and Exception Handling

自问自答

1.如何知道写对还是写错?错在哪里?这里不想lab2一样有很多check
这个我还是不太懂,但是你可以跟踪console的输出,找到每一行输出对应的函数,以及下一行应该输出什么,在哪个函数里。这样就可以找到是哪个函数错了,再设个断点b *fucntion_name,再单步跟踪si就行

2如何把IDT中条目指向中断处理程序入口地址,如何把中断处理函数名与中断处理函数联系起来?.
通过#define SETGATE(gate, istrap, sel, off, dpl)宏可以将IDT中的条目执行handler的入口地址。
通过kern/trapentry.S里定义的两个宏将处理函数名与中断向量(trapno)联系起来,至于具体怎么调用那个处理函数我还是比较懵逼。难道说用这个宏,函数名就变成num对应的处理函数入口地址了?

3入口地址通过调用trapentry.S中的两个宏给出,具体怎么给的,给出了什么.?

#define TRAPHANDLER(name, num)						\
	.globl name;		/* define global symbol for 'name' */	\
	.type name, @function;	/* symbol type is function */		\
	.align 2;		/* align function definition */		\
	name:			/* function starts here */		\
	pushl $(num);							\
	jmp _alltraps

我只看到它声明了下name是个function,然后push了num,这样就可以根据num来找到处理函数入口地址嘛?
答:这两个宏的主要作用只是err跟trapno入栈完善Trapframe结构的一部分,然后调用_alltraps。上面有个name:这个下面就是函数内容啊,它把err跟trapno入栈了啊,这里就是处理程序的入口地址啊!!!

4_alltraps的具体作用是什么?它做的事情在后面哪里用到了?

按我的理解_alltraps是把栈内的trapframe弄完整、将内核数据段(GD_KD)给段寄存器(段选择子)DS,ES、call trap。
后面的trap(tf)就马上用到了,因为call之后,会把返回地址跟old ebp入栈,然后esp给ebp,然后0x8(new ebp)给trap()做参数,即tf=0x8(new ebp)=old esp=trapframe由于栈向下生长,所以正过来正好跟Trapframe的结构一模一样。(不确定)

					 +--------------------+  //stack向下增长,留心“-”号          
                     | 0x00000 | old SS   |     " - 4 <---- ESP 
                     |      old ESP       |     " - 8
                     |     old EFLAGS     |     " - 12
                     | 0x00000 | old CS   |     " - 16
                     |      old EIP       |     " - 20 
  iret将上面出栈----> +--------------------+			 <---- 以上在陷入发生时由硬件完成
                     |        err     	  |     " - 24
                     |      trapno        |     " - 28
                     +--------------------+ 		<----以上由TRAPHANDLER宏完成
                     |        ds          |     " - 32 
                     |        es          |     " - 36 
(trapframe)old esp-> |       regs         |     " - 不知道多少
                	 |     old esp        |
    			     +--------------------+    		<----以上由_alltraps完成  
    			     |     ret addr       |      
    	new ebp----> |     old ebp        |  <----esp 
    			     +--------------------+    		<----以上是call调用完成 
5.整个Lab3 PartA部分的代码的流程是怎样的.?具体的说一遍
env_init()所有env加入env_free_list,env[0]在表头、per-cpu(Load GDT and 段选择器)
-->trap_init()让IDT条目指向对应处理函数入口地址、trap_init_percpu
-->env_create(binary_obj_user_hello_start, type)这里面又包括两个函数
		env_alloc(&e,0);通过env_setup_vm初始化虚拟内存(页表)、初始化env各信息包括'e->env_tf'、从env_free_list中取出env[0]
		load_icode(e,binary)从binary中加载程序段到e对应的内存空间
-->env_run(&env[0]);curenv=env[0],改好状态
		env_pop_tf(&curenv->env_tf)&env_tf为起始地址的一段空间'当成栈',逐步popa、pop、iret到相应寄存器。且iret结束后进入'user mode'
-->寄存器都赋值了,开始执行eip=env_tf.tf_eip=0x800020 lib/entry.S/_start,然后call libmain
-->lib/libmain.c/libmain()调用umain()
-->user/hello.c/umain(),里面有lib/cprintf()函数,cprintf函数由许多'系统调用sys_cputs()'组成
		cprintf()调用vcprintf()调用vprintmt()调用putch()调用cputchar()调用lib/sys_cputs()调用syscall()
-->syscall(...)包含int %T_SYSCALL,中断向量48号,从IDT[48]中找到对应的处理函数入口地址syscall_handler(),陷入'内核态'
-->kern/trapentry.S/TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL);
		_alltraps 完善栈,让栈看上去像Trapframe结构,请看上面问题4
			call trap
-->trap(tf) 重设方向标志位DF、从栈里copy trap frame
		trap_dispatch(tf);
			print_trapframe(tf);打印tf内信息
				print_regs(&tf->tf_regs);
			env_destroy(curenv);由于tf.tf_cs != GD_KT所以destroy了本该继续执行的environment
				env_free(e);
-->while(1) monitor(NULL);
6.整个过程中栈中具体是什么样的?

问题4那个栈时陷入时栈的变化过程,但是,栈陷入之前,从env_create到env_run里的env_pop_tf好像又有些不一样。我感觉是这样的:
env_create调用了env_alloc,这个过程中给e->env_tf赋值了,然后在env_pop_tf中把env_tf的地址给了%esp,那系统就把以&env_tf开始的一段地址当成栈,一点点的popa、pop、iret到各个寄存器,这样就可以配好了第一个用户程序执行所需要的环境,iret之后中断结束,正式进入user mode,按照cs:eip找到用户程序的第一条指令_start: cmp $0xeebfe00,%esp
还有个概念很重要,那就是tf的意义,让cpu运行的是那些寄存器的值,tf只是保存的时陷入的environment的信息以便后面能恢复,并不是tf控制cpu的运行!!!

//注释出自这里:http://www.mamicode.com/info-detail-2493874.html
void env_pop_tf(struct Trapframe *tf)
{
	asm volatile(
		"\tmovl %0,%%esp\n"  /*将%esp指向tf地址处*/
		"\tpopal\n"			//弹出Trapframe结构中的tf_regs值到通用寄存器
		"\tpopl %%es\n"		//弹出Trapframe结构中的tf_es值到%es寄存器
		"\tpopl %%ds\n"		 //弹出Trapframe结构中的tf_ds值到%ds寄存器
		"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
		"\tiret\n"  /*  //中断返回指令,具体动作如下:从Trapframe结构中依次弹出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相应寄存器*/
		: : "g" (tf) : "memory"); //g是一个通用约束,可以表示使用通用寄存器、内存、立即数等任何一种处理方式
	panic("iret failed");  /* mostly to placate the compiler */
}

7.什么时候从内核态进入用户态,什么时候又从用户态进入内核态,又什么时候再次进入用户态的,具体的点在哪里?
在env_pop_tf里的iret之后所有Trapframe值给到相应寄存器,寄存器控制处理器从内核态进入用户态
执行hello.c的时候,lib/cprintf()层层调用,最后调用到syscall,在syscall中int $T_SYSCALL后产生中断,陷入到内核态
在trap中发现tf.tf_cs & 3 == 3即是由用户环境陷入进来的,所以在trap_dispatch中env_destroy了当前用户环境,所以并没有再进入用户态

8.iret是中断返回指令,那什么时候开始中断的呢?
我感觉应该时改变了那些段选择器及其他寄存器的值后,处理器由寄存器们控制着进入用户模式,所以iret之后寄存器值全部赋好,进入用户模式,所以问什么时候开始中断好像没什么意义。

实验过程

Exercise 2

env structure介绍

保存着用户环境的信息

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 大多是ENV_TYPE_USER
	unsigned env_status;		// Status of the environment 
								//ENV_FREE:inactive env, 这个状态下肯定在env_free_list中
								//ENV_RUNNABLE:waiting to run
								//ENV_RUNNING: the currently running environment
								//ENV_NOT_RUNNABLE:当前active environment,但还没准备好去运行
								//ENV_DYING:A zombie environment 下次陷入内核时被释放
								
	uint32_t env_runs;		// Number of times environment has run

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

初始化envs数组中的所有Env结构,并将它们添加到env_free_list中。还调用env_init_percpu,它为特权级别0(内核)和特权级别3(用户)使用单独的段配置分段硬件。

void
env_init(void)
{
	// Set up envs array
	// LAB 3: Your code here.
	size_t i=0;
	for(; i<NENV; i++){
		envs[i].env_id = 0;
		//env in free list so the status is ENV_FREE, You shouldn't forget it!
		envs[i].env_status = ENV_FREE; 
		if(i==0){
			env_free_list = &envs[i];
		}else{
			envs[i-1].env_link = &envs[i];
		}
	}
	// Per-CPU part of the initialization
	env_init_percpu();
}

不要忘了,如果某个user environment在env_free_list中,那么它的状态也得设为ENV_FREE才行

env_setup_vm()

为新environment分配一个页面目录,并初始化新environment地址空间的内核部分。

这里我看半天没看懂,说什么所有envs在UTOP之上的虚拟地址都是相同的(除了UVPT),UTOP之下就初始化为0。还说只初始化new environment地址空间的内核部分,不要映射任何内容到用户部分。

后面想着好像是这样的,之前了解了ULIM之上是内核部分,ULIM往下是用户环境,所以把内核部分再加上pages与envs的内容(即UTOP以上)原封不动的从kern_pgdir中复制到env_pgdir中就叫做初始化内核部分,且所有envs在UTOP之上的虚拟地址都是相同的。

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;

	// LAB 3: Your code here.
	e->env_pgdir = (pde_t *)page2kva(p);
	p->pp_ref++; //The hint is just a little bit above

	size_t i=0;
	// Do NOT (yet) map anything into the user portion
	for(; i<PDX(UTOP); i++){
		e->env_pgdir[i] = 0;
	}

	//initialize the kernel portion of the new environment's address space.
	for(; i<NENV; i++){
		e->env_pgdir[i] = kern_pgdir[i];
	}
	
	// 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()

为an environment分配和映射物理内存

这是个很简单的问题,只要向虚拟地址va分配并映射物理页就行,但我搞好久都没搞出来,可能对lab2的内容又忘了。。。

static void
region_alloc(struct Env *e, void *va, size_t len)
{
	// LAB 3: Your code here.
	struct PageInfo *p = NULL;
	void *i;
	for(i=(void*)ROUNDDOWN(va, PGSIZE); i<(void *)ROUNDUP(va+len, PGSIZE); i+=PGSIZE){	
		// Allocate a page 
		if (!(p = page_alloc(0))) //allocate a physical page
			panic("fail to alloc page!\n");
		if(page_insert(e->env_pgdir, p, i, PTE_U|PTE_W)!=0) //map the page to virtual address
			panic("fail to region_alloc!\n");


	}
		
}
load_icode()

您将需要解析an ELF binary image,就像the boot loader已经做的那样,并将其内容加载到new environment的用户地址空间中

提示文字也太多了吧。。。

自己写不出来啊!
首先binary应该转成Elf structure指针我不知道
其次我本应该知道设置environment执行入口是在e->env_tf.tf_eip中,结果我没想起来,还傻傻的照着main.c的入口地址那样写,这只是在加载不是执行,只是用户程序又不是内核部分
lcr3(e->env_pgdir)我懂但是不知道居然有这种操作
最后根据the program header加载segments,具体是给ph->p_va分配ph-.memsz大小的物理内存,然后直接把data从binary中move到虚拟地址对应的物理内存中。我写对了region_alloc,也没想到是用memmove跟memset,我就说readseg是读到物理地址,怎么提示里压根没提到过ph->p_pa。

static void
load_icode(struct Env *e, uint8_t *binary)
{	
	//turn the binary into a Elf structure
	struct Elf *ELF_Header = (struct Elf*)binary; 
	if (ELF_Header->e_magic != ELF_MAGIC)
		panic("The binary is not a ELF magic!\n");


	//make sure that the environment starts executing from the entry point.
	if(ELF_Header->e_entry == 0)
		panic("The program can't be executed because the entry point is invalid!\n");
	e->env_tf.tf_eip = ELF_Header->e_entry;


	//	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?
	
	  //lcr3(e->env_pgdir);错大发了
	lcr3(PADDR(e->env_pgdir));//load this user pgdir
	//load the segments 
	//ph and eph is the Program header 
	struct Proghdr *ph, *eph;
	ph = (struct Proghdr *) ((uint8_t *) ELF_Header + ELF_Header->e_phoff);
	eph = ph + ELF_Header->e_phnum;
	for (; ph < eph; ph++)
		// p_pa is the load address of this segment (as well
		// as the physical address)
		if(ph->p_type == ELF_PROG_LOAD){
			if(ph->p_memsz < ph->filesz)
				panic("segment out of memory!\n");
			//alloc ph->p_memsz bytes of physical memory for the ph->p_va
			region_alloc(e, (void *)ph->p_va, ph->p_memsz);
			//You can move data to ph->p_va via the e->env_pgdir
			/*之前的错误代码
			memmove((void *)ph->p_va, (void *)(ELF_Header+ph->p_offset), ph->p_filesz);
			memset((void *)(ph->p_va+ph->p_filesz), 0, ph->p_memsz-ph->p_filesz);*/
			memset((void *)ph->p_va, 0, ph->p_memsz);
			memcpy((void *)ph->p_va, binary+ph->p_offset, ph->p_filesz);//主要是binary+ph->p_offset
		}
	
	lcr3(PADDR(kern_pgdir));//???居然还得加载回来,不然softint拿不到分
	// Now map one page for the program's initial stack
	// at virtual address USTACKTOP - PGSIZE.
	region_alloc(e, (void *)(USTACKTOP-PGSIZE), PGSIZE);
	
}
env_create()

使用env_alloc分配an environment ,并调用load_icode将ELF二进制文件加载到其中。

这个还是比较简单,提示得很清楚

void
env_create(uint8_t *binary, enum EnvType type)
{
	// LAB 3: Your code here.
	struct Env *e;
	if(env_alloc(&e, 0)<0)
		panic("fail to create a env!\n")
	load_icode(e, binary);
	e->env_type = type;
}
env_run()

在用户模式下运行的给定environment。

void
env_run(struct Env *e)
{
	if(curenv != e){
		//是我考虑太不周全了
		if(curenv != NULL && curenv->env_status == ENV_RUNNING)
			curenv->env_status = ENV_RUNNABLE;
		
		curenv = e;
		curenv->env_status = ENV_RUNNING;
		curenv->env_runs++;
		//lcr3((uint32_t)curenv->env_pgdir);又错大发了
		lcr3(PADDR(curenv->env_pgdir));
		env_pop_tf(&curenv->env_tf);
	}
	panic("env_run not yet implemented");
}

triple fault还是不太懂,好像是CPU连着三次报错,就算是叫天天不应,叫地地不灵了,只能重启系统?

但是这个系统调用指令不能成功运行,因为到目前为止,JOS还没有设置相关硬件来实现从用户态向内核态的转换功能。当CPU发现,它没有被设置成能够处理这种系统调用中断时,它会触发一个保护异常,然后发现这个保护异常也无法处理,从而又产生一个错误异常,然后又发现仍旧无法解决问题,所以最后放弃,我们把这个叫做”triple fault”。通常来说,接下来CPU会复位,系统会重启。
谢谢bysui的翻译

int指令(软件中断指令)
INT(软件中断指令)是CALL指令的一种特殊形式。call指令调用调用的子程序是用户程序的一部分,而INT指令调用的操作系统提供的子程序或者其他特殊的子程序。
int指令后面一般接中断号。

在这里插入图片描述
完全搞不清到底对不对。。。不知道进没进user mode,不知道数据加载到user environment没。。。也不知道现在运行了哪个nev。。。(这里在Lab3里已经有说明了。env_pop_tf,它应该是您在实际进入用户模式之前命中的最后一个函数。处理器应该在iret指令之后进入用户模式。然后,您应该看到用户环境的可执行文件中的第一条指令,即lib/entry.S中标签start处的cmpl指令。)

我慢慢来试试看,输出了[00000000] new env 00001000说明进到了env_alloc里面,且curenv->env_id=NULL,new了一个environment成功,其e->env_id=00001000

然后我按Lab3里的提示,设了个断点在 800a9b: cd 30 int $0x30
在这里插入图片描述
这是不是在说,我从二进制ELF映像文件中读取数据失败了?
经过仔细的排查,终于发现,load_icode()中lcr3的赋值写的lcr3((uint32_t)e->env_pgdir);应该是lcr3(PADDR(e->env_pgdir));

更加让我震惊的事发生了:

//设个断点在env_pop_tf函数开始,执行到iret后本以到lib/entry.S/0x800020 _start,
//结果0x800020处指令都错了!!!
0xf0102e66 <env_pop_tf+15>: iret
0x800020:  add 	%al,(%eax) //本应该是 0x800020:	cmp		$0xeebfe00,%esp
0xf0103686 <pgflt_handler+2>: jmp	0xf01036a6 <_alltraps>

经过仔细的排查,发现还是load_icode里写错了,改正后,再设个断点,就可以得到这个:
在这里插入图片描述
设个断点在int 0x30 in sys_cputs() in hello (see obj/user/hello.asm for the user-space address)处,结果如下
在这里插入图片描述

Exercise 3

80386 Programmer’s Manual: Chapter 9 Exceptions and Interrupts(Personal Translation)

Basics of Protected Control Transfer

1.Exceptions and interrupts are both “protected control transfers,” which cause the processor to switch from user to kernel mode (CPL=0) without giving the user-mode code any opportunity to interfere with the functioning of the kernel or other environments(不给用户模式代码任何面对内核与其他环境的机会)

CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。 当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL,RPL}(数越小级别越高)。下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
谢谢仁兄

2.an interrupt is a protected control transfer that is caused by an asynchronous event(异步事件) usually external to the processor(如外部I/O设备的活动). An exception, in contrast, is a protected control transfer caused synchronously(同步的) by the currently running code(如除零错误或者无效地址访问)

3.the processor ensures that the kernel can be entered only under carefully controlled conditions(严格控制的条件下进入):

  • The Interrupt Descriptor Table.内核的entry-points都是内核自己定义的。x86有256个中断或异常的入口点,对应着不同的中断向量(0-255), The CPU uses the vector as an index into the processor’s interrupt descriptor table (IDT)。从IDT的对应条目中加载EIP和CS register值。
  • The Task State Segment 处理器需要一个地方保存旧处理器中断或异常发生前的状态,如EIP的原始值和CS在处理器调用异常处理程序之前,所以异常处理程序结束后处理器可以恢复之前状态和恢复中断的代码。
    因此,当x86处理器发生中断或陷阱,导致特权级别从用户更改为内核模式时,它还会切换到内核内存中的堆栈。 一个名为任务状态段(task state segment, TSS)的结构指定了这个堆栈所在的段选择器和地址。处理器(在这个新堆栈上)推送SS、ESP、EFLAGS、CS、EIP和an optional error code。然后它从中断描述符加载CS和EIP,并设置ESP和SS引用新的堆栈。
    JOS只使用TSS来定义处理器应该切换到的kernel stack。主要是 ESP0 and SS0 fields of the TSS

4.数字越大,特权级别越低。

中断和异常的类型见上面的链接。这里只需知道48(0x30)是一个软件中断。或者可以这么说,异常和NMI中断都在0-31,31-255是INTR中断(可屏蔽中断)

5.An Example

除零异常:
 					 +--------------------+ KSTACKTOP   //stack向下增长,留心“-”号          
                     | 0x00000 | old SS   |     " - 4
                     |      old ESP       |     " - 8
                     |     old EFLAGS     |     " - 12
                     | 0x00000 | old CS   |     " - 16
                     |      old EIP       |     " - 20 <---- ESP 
                     +--------------------+           
        1.处理器切换到由TSS中的SS0(包含GD_KD)ESP0(包含KSTACKTOP)指向的stack。 
        2.处理器将old ss、old ESP、异常数据EFLAGS等推入堆栈
        3.除零异常的中断向量是0,所以处理器读取IDT条目0并设置'CS:EIP'指向条目0描述的the handler function(处理函数)4.处理函数控制和处理这个exception,如结束这个用户环境

对于某些x86异常,除零这种five words "standard",处理器还会把"error code"推入堆栈,如The page fault exception, number 14
					 +--------------------+ KSTACKTOP             
                     | 0x00000 | old SS   |     " - 4
                     |      old ESP       |     " - 8
                     |     old EFLAGS     |     " - 12
                     | 0x00000 | old CS   |     " - 16
                     |      old EIP       |     " - 20
                     |     error code     |     " - 24 <---- ESP
                     +--------------------+             

6.The processor can take(接受) exceptions and interrupts both from kernel and user mode. It is only when entering the kernel from user mode, however, that the x86 processor automatically switches(自动切换) stacks before pushing its old register state onto the stack and invoking the appropriate exception handler through the IDT. (在老寄存器状态入栈和从IDT调用合适的异常处理程序之前)

7.If the processor is already in kernel mode when the interrupt or exception occurs (the low 2 bits of the CS register are already zero), then the CPU just pushes more values on the same kernel stack. In this way, the kernel can gracefully handle nested exceptions(嵌套异常) caused by code within the kernel itself. This capability is an important tool in implementing protection, as we will see later in the section on system calls.

If the processor is already in kernel mode and takes a nested exception, 
since it does not need to switch stacks, it does not save the old SS or ESP registers
如果也不需要push error code的话,the kernel stack在进入the exception handler时是这样的:
					 +--------------------+ <---- old ESP
                     |     old EFLAGS     |     " - 4
                     | 0x00000 | old CS   |     " - 8
                     |      old EIP       |     " - 12
                     +--------------------+            
如果发生异常时处理器已经在kernel mode,又因为某些原因(如栈空间不足)无法将old state入栈,那就无法恢复之前现场,处理器只好重启,设计时应该避免这种情况

8.会产生error code 的异常是:
在这里插入图片描述

Exercise 4

主要任务是设置IDT。这里将设置IDT中的中断向量0-31对应的中断处理程序描述符。0-31号异常是由Intel定义并保留的,无法更改。

The file kern/trap.h contains definitions that are strictly private to the kernel(内核严格私有), while inc/trap.h contains definitions that may also be useful to user-level programs and libraries(用户级程序和库).

The overall flow of control(总体控制流程)如下:
      IDT                   trapentry.S         trap.c
   
+----------------+                        
|   &handler1    |---------> handler1:          trap (struct Trapframe *tf)
|                |             // do stuff      {
|                |             call trap          // handle the exception/interrupt
|                |             // ...           }
+----------------+
|   &handler2    |--------> handler2:
|                |            // do stuff
|                |            call trap
|                |            // ...
+----------------+
       .
       .
       .
+----------------+
|   &handlerX    |--------> handlerX:
|                |             // do stuff
|                |             call trap
|                |             // ...
+----------------+

Each exception or interrupt should have its own handler in trapentry.S
trap_init() 应该用the addresses of these handlers初始化the IDT 
每个处理程序都要建a struct Trapframe (see inc/trap.h) on the stack and call trap() (in trap.c) with a pointer to the Trapframe.
trap() then handles the exception/interrupt or dispatches to a specific handler function. 

按照提示,首先应该是在trapentry.S中添加中断或异常处理函数。
先来看看给出trapentry.S已经给出的两个宏定义函数

/* TRAPHANDLER defines a globally-visible function for handling a 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.
 *
 * 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();
 * where NAME is the argument passed to TRAPHANDLER.
 */
#define TRAPHANDLER(name, num)						\
	.globl name;		/* define global symbol for '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.
 */
#define TRAPHANDLER_NOEC(name, num)					\
	.globl name;							\
	.type name, @function;						\
	.align 2;							\
	name:								\
	pushl $0;							\
	pushl $(num);							\
	jmp _alltraps

两个比较简单的函数,会将error code 与 trap number 入栈,然后转到_alltraps,难不成通过trap num将处理程序地址赋给name???如果是不会产生错误代码的中断或异常则用0代替error code入栈。其中name声明为全局函数变量,根据提示,如果要先在C中做函数声明后才能作为参数

既然给出了这么两个函数,那么入口地址就是调用这两个函数给出,具体怎么给的,给出了什么?暂时还不知道。

//在inc/trap.h中先声明处理函数名
void divide_handler();
void debug_handler();
void nmi_handler();    
void brkpt_handler(); 
void oflow_handler();
void bound_handler();
void illop_handler();
void device_handler();

void dblflt_handler();
void tss_handler();
void segnp_handler();
void stack_handler();
void gpflt_handler();
void pgflt_handler();

void fperr_handler();
void align_handler();
void mchk_handler();
void simderr_handler();

//然后在trapentry.S中产生入口地址
/*
 * Lab 3: Your code here for generating entry points for the different traps.
 * interrupt vector 8、10、11、12、13、14 have error code
 * 结果之前还全部写反了,也是醉了。
 */
TRAPHANDLER_NOEC(divide_handler, T_DIVIDE);
TRAPHANDLER_NOEC(debug_handler, T_DEBUG);
TRAPHANDLER_NOEC(nmi_handler, T_NMI);
TRAPHANDLER_NOEC(brkpt_handler, T_BRKPT);
TRAPHANDLER_NOEC(oflow_handler, T_OFLOW);
TRAPHANDLER_NOEC(bound_handler, T_BOUND);
TRAPHANDLER_NOEC(illop_handler, T_ILLOP);
TRAPHANDLER_NOEC(device_handler, T_DEVICE);

TRAPHANDLER(dblflt_handler, T_DBLFLT);
TRAPHANDLER(tss_handler, T_TSS);
TRAPHANDLER(segnp_handler, T_SEGNP);
TRAPHANDLER(stack_handler, T_STACK);
TRAPHANDLER(gpflt_handler, T_GPFLT);
TRAPHANDLER(pgflt_handler, T_PGFLT);

TRAPHANDLER_NOEC(fperr_handler, T_FPERR);
TRAPHANDLER_NOEC(align_handler, T_ALIGN);
TRAPHANDLER_NOEC(mchk_handler, T_MCHK);
TRAPHANDLER_NOEC(simderr_handler, T_SIMDERR);

下一步就是写_alltraps了。首先得push values to make the stack look like a struct Trapframe。emmm真是搞不懂,什么叫让栈看起来像Trapframe,那就先看下Trapframe的结构:

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 */
	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));
//__attribute__((packed));告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐
// __attribute__关键字主要是用来在函数或数据声明中设置其属性

//注释出自这里:http://www.mamicode.com/info-detail-2493874.html
void env_pop_tf(struct Trapframe *tf)
{
	asm volatile(
		"\tmovl %0,%%esp\n"  /*将%esp指向tf地址处*/
		"\tpopal\n"			//弹出Trapframe结构中的tf_regs值到通用寄存器
		"\tpopl %%es\n"		//弹出Trapframe结构中的tf_es值到%es寄存器
		"\tpopl %%ds\n"		 //弹出Trapframe结构中的tf_ds值到%ds寄存器
		"\taddl $0x8,%%esp\n" /* skip tf_trapno and tf_errcode */
		"\tiret\n"  /*  //中断返回指令,具体动作如下:从Trapframe结构中依次弹出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相应寄存器*/
		: : "g" (tf) : "memory"); //g是一个通用约束,可以表示使用通用寄存器、内存、立即数等任何一种处理方式
	panic("iret failed");  /* mostly to placate the compiler */
}

入栈的话参数从右往左,结构体中的值就是从下往上了,现在应该本来就是内核栈(why?)所以tf_sstf_esp不需要入栈,中间eflags、cs、eip由硬件完成,所以只要入栈ds与es还有regs

/*
 * Lab 3: Your code here for _alltraps
 
 */
.global _alltraps
_alltraps:
		#make the stack look like a struct Trapframe
		/*pushl %es
		pushl %ds
		还是写反了*/
		pushl %ds
		pushl %es
		pushal

		#load GD_KD into %ds and %es
		movl $GD_KD, %edx
		movl %edx, %ds
		movl %edx, %es

		# push %esp as an argument to trap()
		pushl %esp 

		call trap

这个pushal指令我都没查到。。。我傻了,pushal查不到那就查pusha指令

PUSHA/PUSHAD
当操作数的大小是32位时:
这两个指令的作用是把通用寄存器压栈。寄存器的入栈顺序依次是:EAX,ECX,EDX,EBX,ESP(初始值),EBP,ESI,EDI.
当操作数的大小是16位时:
这两个指令的作用是把通用寄存器压栈。寄存器的入栈顺序依次是:AX,CX,DX,BX,SP(初始值),BP,SI,DI.

POPA/POPAD
这两个指令的指令码也是一样的。
这两个指令用于把栈中的值弹出到通用寄存器。
其实是执行和PUSHA/PUSHAD相反的操作。 当操作数的大小是32位时:
出栈顺序依次是:EDI,ESI,EBP,EBX,EDX,ECX,EAX;
当操作数的大小是16位时:
出栈顺序依次是:DI,SI,BP,BX,DX,CX,AX;
谢谢 ARM的程序员敲着诗歌的梦

所以这个pushal其实就是对应着Trapframe里的struct PushRegs tf_regs;

然后就是trap_init() should initialize the IDT with the addresses of these handlers。用SETGATE宏定义。那就先看看这个定义

// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
    //中断门会重置IF(the interrupt-enable flag)防止其他中断妨碍current interrupt handler. 陷阱门不会重置IF
// - 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.
#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;		\
}

参数gate是idt表的index入口,可以通过idt[T_*]设置,
istrap可以在开发手册的exception summary中查到,但是中断门和陷阱门descriptors格式好像是一样的,是不是随便设都行?
sel设为GD_KT已经定义好了(不是很明白),
off对应处理函数地址,
关于dpl的描述在本文Exercise 3一开始就提到了,还是没看懂,但知道dpl是固定的

void
trap_init(void)
{
	extern struct Segdesc gdt[];
	//:它通过传递第二个参数值为 1 来指定这是一个陷阱门。
	//陷阱门不会清除 IF 位,这使得在处理系统调用的时候也接受其他中断。
	// LAB 3: Your code here.
	SETGATE(idt[T_DIVIDE], 1, GD_KT, divide_handler, 0);
	SETGATE(idt[T_DEBUG], 1, GD_KT, debug_handler, 0);
	SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0);
	SETGATE(idt[T_BRKPT], 1, GD_KT, brkpt_handler, 3);
	SETGATE(idt[T_OFLOW], 1, GD_KT, oflow_handler, 0);
	SETGATE(idt[T_BOUND], 1, GD_KT, bound_handler, 0);
	SETGATE(idt[T_ILLOP], 1, GD_KT, illop_handler, 0);
	SETGATE(idt[T_DEVICE], 1, GD_KT, device_handler, 0);
	SETGATE(idt[T_DBLFLT], 1, GD_KT, dblflt_handler, 0);
	SETGATE(idt[T_TSS], 1, GD_KT, tss_handler, 0);
	SETGATE(idt[T_SEGNP], 1, GD_KT, segnp_handler, 0);
	SETGATE(idt[T_STACK], 1, GD_KT, stack_handler, 0);
	SETGATE(idt[T_GPFLT], 1, GD_KT, gpflt_handler, 0);
	SETGATE(idt[T_PGFLT], 1, GD_KT, pgflt_handler, 0);
	SETGATE(idt[T_FPERR], 1, GD_KT, fperr_handler, 0);
	SETGATE(idt[T_ALIGN], 0, GD_KT, align_handler, 0);
	SETGATE(idt[T_MCHK], 0, GD_KT, mchk_handler, 0);
	SETGATE(idt[T_SIMDERR], 0, GD_KT, simderr_handler, 0);
	SETGATE(idt[T_SYSCALL], 0, GD_KT, syscall_handler, 3);
	
	// Per-CPU setup 
	trap_init_percpu();
}

make grade 得到了20分。。。softint不得分,在load_icode()的for循环后面加一句lcr3(PADDR(kern_pgdir));就可以了,具体原因见下面问题2

回答问题

1.为每个异常/中断提供单独的处理函数的目的是什么?(即。,如果所有异常/中断都交付给同一个处理程序,则不能提供当前实现中存在的哪些特性?)
答:中断是处理来自处理器外部的异步事件,异常是处理执行指令过程中,处理器自己发现的状况,那么不同的异常/中断自然要有不同的处理函数。有时处理结束得返回报错位置继续执行,有时就得结束当前程序运行下一个。而且a interrupt gate会重置IF,而a trap gate就不会。如果用同一个处理程序,就很难提供这么多不同的特性。

2.为了让 user/softint程序正常运行,您做了什么吗?grade脚本期望它产生一个通用的保护错误(trap 13),但是softint的代码是int $14。为什么会产生中断向量13?如果内核实际上允许softint的int $14指令调用内核的页面错误处理程序(即中断向量14),会发生什么?
答:trap 13是general protection fault,由于softint是用户程序,特权级别为3,页面错误的处理程序的DPL为0,特权级别为3的程序调用特权级别为0的程序就会产生一个general protection fault。如果运行的话,可能会导致内核的页表与物理页不匹配吧,我猜的。所以在load_icode()的for循环后面加上了lcr3(PADDR(kern_pgdir));是softint正常运行。

参考

全局描述符GDT
第九章 中断和异常
这位仁兄的报告解决了我很多疑惑
谢谢CT_36对寄存器的解释

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值