MIT6.828——Lab3 PartA(麻省理工操作系统实验)

Lab3 Part A

MIT6.828——Lab1 PartA

MIT6.828——Lab1 PartB

Lab2内存管理准备知识

MIT6.828——Lab2

内核维护了三个关于用户环境的全局量

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

分别对应所有的环境,当前运行的用户环境和空闲的环境链表。

Environment State

Env结构体的定义如下:

struct Env {
    struct Trapframe env_tf; 	// Saved registers
    struct Env *env_link; 		// Next free Env
    envid_t env_id; 			// Unique environment identifier
    envid_t env_parent_id; 		// env_id of this env's parent
    enum EnvType env_type; 		// Indicates special system environments
    unsigned env_status; 		// Status of the environment
    uint32_t env_runs; 			// Number of times environment has run
    // Address space
    pde_t *env_pgdir; 			// Kernel virtual address of page dir
};

各个字段的解释如下:

env_tf:

    当用户环境暂停运行时,重要寄存器的值(保护的现场)。内核也会进行用户态内核态切换时保存这些值,用户环境可以在之后被恢复。

env_link:

    这个指针指向env_free_list的后一个空闲的Env结构体。

env_id:

    唯一地确定使用这个结构体的用户环境。用户环境终止后,内核也许会把这个结构体分给另外一个环境,新的环境会有新的env_id值。

env_parent_id:

    创建这个用户环境的环境(parent)的env_id,构建一颗tree。

env_type:

    用于区别特别的用户环境。大多数清空下值都是ENV_TYPE_USER.

env_status:

    这个变量有以下可能的取值:

    ENV_FREE: 代表这个Env结构体不活跃的,应该在链表env_free_list中。

    ENV_RUNNABLE: 对应的用户环境已经就绪,等待被分配处理机。

    ENV_RUNNING: 对应的用户环境正在运行。

    ENV_NOT_RUNNABLE: Env结构体所代表的是一个当前状态下活跃的用户环境,但是并未就绪,在等待IPC(Interprocess communication)。

    ENV_DYING: Env对应的是一个僵尸环境(Zombie environment)。一个僵尸环境在下一次陷入内核时会被释放回收(Lab4 会使用)。

env_pgdir:

    存放着这个环境的页目录的虚拟地址。

Allocating the Environment Array

需要进一步地修改mem_init()函数,分配一个envs数组,这个数组保存所有的环境,并进行映射。需要新增的代码如下:

struct Env* envs = (struct Env*)boot_alloc(NENV * sizeof(struct Env));
memset(envs,0,NENV * sizeof(struct Env));
//... ...
boot_map_region(kern_pgdir,UENVS,PTSIZE,PADDR(envs),PTE_U);

Creating and Running Environments

现在需要完成如何让用户环境跑起来的代码了。因为还没有文件系统,因此只能加载嵌入内核自身的静态二进制映像。Lab3的makefile会生成几个二进制文件放在obj/user中,一些技巧将这些二进制文件link到了内核之中。二进制文件中会有一个特殊的符号,通过生成的这些符号可以来引用到这些代码。

  • 第一个函数env_init(),需要初始化所有的Env结构,将其挂入链表,也调用env_init_percpu来配置底层的信息。

    void
    env_init(void)
    {
    	// Set up envs array
    	// LAB 3: Your code here.
    	for(int 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();
    }
    

    与lab2的pages数组处理类似。注意链表的顺序

  • 第二个函数env_setup_vm(),为新的环境分配页目录,并且初始化

    static int
    env_setup_vm(struct Env *e)
    {
        //------------------------------------------
        // 源代码中的注释此处为了篇幅,很多详细说明都略去了
        // 详细的信息,请自行阅读源代码
        //------------------------------------------
    	int i;
    	struct PageInfo *p = NULL;
    	// 给页目录的分配一个物理页来存储
    	if (!(p = page_alloc(ALLOC_ZERO)))
    		return -E_NO_MEM;
       
        // 得到页目录的虚拟地址所在
    	e->env_pgdir = (pde_t*)page2kva(p);
        // 要求的自增引用计数
    	p->pp_ref++;
    
        // 这部分的页目录值,和kern_pgdir是一致的
        // 因此 也可以使用
        // memcpy(e->env_pgdir,kern_pgdir,PGSIZE);
    	for(i=0;i<PDX(UTOP);i++){
    		e->env_pgdir[i]=0;
    	}
    	for(i=PDX(UTOP);i<NPDENTRIES;i++){
    		e->env_pgdir[i]=kern_pgdir[i];
    	}
    	// 唯一和kern_pgdir不一样的是对于自身的映射
    	e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;
    	return 0;
    }
    

    设置完页目录,用户环境继承了内核的地址映射,对于后续而言,每个用户进程都能有自己的虚拟地址空间,且共享内核。

  • 第三个函数region_alloc(),作用是为环境分配物理空间。分配物理空间,就是之前说的分配物理页,使用的是page_alloc()。分配物理也,然后更改页表。

    static void
    region_alloc(struct Env *e, void *va, size_t len)
    {
    	void * beigin =ROUNDDOWN(va,PGSIZE);
    	void * end = ROUNDUP(va+len,PGSIZE);
    	for(;beigin<end;beigin+=PGSIZE){
            // 申请物理页
    		struct PageInfo* apage=page_alloc(0);
    		if(!apage){
    			panic("region_alloc fail ,out of memory!");
    		}
            // 安装到页表
    		page_insert(e->env_pgdir,apage,beigin,PTE_U|PTE_W);
    	}
    }
    
  • 第四个函数 load_icode(),用来解析一个ELF映像,像Lab1中bootloader做的一样。并把映像加载到新环境的用户空间。在编写时,如下几点值得注意:

    1. 阅读boot/main.c 来得到灵感
    2. 只有p_type=ELF_PROG_LOAD的段才需要被被加载
    3. ph->p_va 是需要被加载到的虚地址
    4. ph->p_memsz 是整个在内存中占的大小,也是我们申请空间时的大小
    5. 从 binary + ph->p_offset 开始的ph->p_filesz字节需要被复制到ph->p_va处
    6. 需要考虑一些ELF头的入口点处理
    7. 这个过程在进行环境处理时,因为需要映射新的页,因此需要切换页目录
    8. 哪些地方会产生panic?
    static void
    load_icode(struct Env *e, uint8_t *binary)
    {
    	struct Proghdr *ph,*end_ph;
    	struct Elf * elf_header = (struct Elf*)binary;
    	if(elf_header->e_magic!=ELF_MAGIC){
    		panic("not a elf format file");
    	}
    	ph=(struct Proghdr*)((uint8_t*)elf_header+elf_header->e_phoff);
    	end_ph=ph+elf_header->e_phnum;
    	lcr3(PADDR(e->env_pgdir));
    	for(;ph<end_ph;ph++){
    		if(ph->p_type==ELF_PROG_LOAD){
    			if(ph->p_memsz-ph->p_filesz<0){
    				panic("p_memsz < p_filesz");
    			}
    			region_alloc(e,(void*)ph->p_va,ph->p_memsz);
    			memcpy((void*)ph->p_va,(void*)binary+ph->p_offset,ph->p_filesz);
    			memset((void*)(ph->p_va+ph->p_filesz),0,ph->p_memsz-ph->p_filesz);
    		}
    	}
    	e->env_tf.tf_eip=elf_header->e_entry;
    	region_alloc(e,(void*)(USTACKTOP-PGSIZE),PGSIZE);
    	lcr3(PADDR(kern_pgdir));
    }
    
  • 第五个函数env_create(),用来分配环境并加载ELF文件。实现很简单,使用env_alloc获得一个新的环境,然后用load_icode加载。

    void
    env_create(uint8_t *binary, enum EnvType type)
    {
    	struct Env* new_env;
    	int r;
    	if((r=env_alloc(&new_env,0))!=0){
    		panic("env alloc fail in env creat :%e",r);
    	}	
    	new_env->env_type=type;
    	load_icode(new_env,binary);
    }
    
  • 第六个函数env_run(),在用户态中开始运行一个环境。这部分函数只要按照注释完成即可。

    void
    env_run(struct Env *e)
    {
    	if((curenv!=NULL) && curenv->env_status==ENV_RUNNING){
    		curenv->env_type=ENV_RUNNABLE;
    	}
    	curenv=e;
    	e->env_status=ENV_RUNNING;
    	e->env_runs++;
    	lcr3(PADDR(e->env_pgdir));
        //保存环境
    	env_pop_tf(&e->env_tf);
    }
    

有一个函数也值得讨论,那就是env_pop_tf(),相关的结构和定义如下:

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


void
env_pop_tf(struct Trapframe *tf)
{
	asm volatile(
		"\tmovl %0,%%esp\n"		//	esp指向tf结构,弹出时会弹到tf里
		"\tpopal\n"				//  弹出tf_regs中值到各通用寄存器
		"\tpopl %%es\n"			//  弹出tf_es 到 es寄存器
		"\tpopl %%ds\n"			//  弹出tf_ds 到 ds寄存器
		"\taddl $0x8,%%esp\n"   //  跳过tf_trapno和tf_err
		"\tiret\n"				//  中断返回 弹出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相应寄存器
		: : "g" (tf) : "memory");
	panic("iret failed");  /* mostly to placate the compiler */
}

运行make qemu-gdbmake gdb,然后断点打在env_pop_tf,执行到iret指令,在iret之前

eax            0x0                 0
ecx            0x0                 0
edx            0x0                 0
ebx            0x0                 0
esp            0xf01d1030          0xf01d1030
ebp            0x0                 0x0
esi            0x0                 0
edi            0x0                 0
eip            0xf01038e2          0xf01038e2 <env_pop_tf+31>
eflags         0x96                [ PF AF SF ]
cs             0x8                 8
ss             0x10                16
ds             0x23                35
es             0x23                35
fs             0x23                35
gs             0x23                35

可以看到此时的cs为00001 000,是我们GDT中的第一个段,内核段。在iret之后

eax            0x0                 0
ecx            0x0                 0
edx            0x0                 0
ebx            0x0                 0
esp            0xeebfe000          0xeebfe000
ebp            0x0                 0x0
esi            0x0                 0
edi            0x0                 0
eip            0x800020            0x800020
eflags         0x2                 [ ]
cs             0x1b                27
ss             0x23                35
ds             0x23                35
es             0x23                35
fs             0x23                35
gs             0x23                35

cs=0X1b=0001 1011,所以是GDT中的第三个描述符(user code segment),权限为3(用户态)。

obj/user/hello.asm找到

800b93:	cd 30                	int    $0x30
	syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);

断点设置在此处,由于系统调用还没有实现,这里往下执行就会触发triple fault。


可以有如下的函数调用图:

  • start (kern/entry.S)
  • i386_init (kern/init.c)
    • cons_init

    • mem_init

    • env_init

    • trap_init (still incomplete at this point)

    • env_create

      • env_alloc

        • env_setup_vm
      • load_icode

        • region_alloc
    • env_run

      • env_pop_tf

User stack and Kernel stack

这里提前说明一下关于用户栈和内核栈,以及这俩的切换过程,在后续进程等地方,这一套机制都很受用。

这是涉及到特权级切换的情况,用户程序的栈和内核的栈,组合形成一套栈。这个过程ss,sp,eflags,cs,eip在中断发生时由处理器压入,通用寄存器部分需要自己实现,详情可以参考哈工大李治军老师关于操作系统的课程

Handling Interrupts and Exceptions

Part of 80386 Programmer's Manual

这是这部分开头练习的要求,这里就来读一读8086程序员手册。

首先便是中断和溢出的分类:

一般地不刻意区分这些术语(在这套体系中)。

NMI和Exception都分配了唯一的中断号,系统保留0~31这32个中断号(因此,如果用户自定义中断,中断号应从32开始)。

如果一定要区分的话,exception被分为faults, traps和aborts, 区分的标准是这些exception如何被通知,何时重新执行造成溢出的指令。

下一个话题是中断描述符表IDT,每个中断或者溢出的服务程序都和IDT中的8B中断描述符相关联。和GDT,LDT不同,IDT的第一个描述符并不是空的。

IDT中的描述符有三种类别:任务们,中断门,陷阱门(由type字段标识)。

至于中断服务程序的定位,就是在查GDT或LDT之前,多查一次IDT

而中断服务程序如果和当前代码之间存在特权级的转移,那么栈的变化在上文已经说明了。

An Example

讲前文的诸多小知识拼凑起来,通过一个例子来过一遍整个过程。

处理器正在用户空间执行代码,遇到了一条除以零的指令,由此引发溢出:

  1. 处理器切换到内核栈(由SS0 ESP0进行内核栈的定位),此时内核栈为空。
  2. 内核栈压入一系列溢出现场,进行现场保护

  1. 因为正在处理除以零溢出,因此中断向量0被索引到了,因此处理器读取IDT的第0项,将cs:eip指向中断处理程序。
  2. 处理程序获得控制权并处理该溢出,比如说该程序终止该用户环境的运行。

某些特定的x86溢出,除了会压入上面的经典5个字段,还会压入error code。在处理栈时,不要忘了跳过这个字段,如果需要的话。

Setting Up the IDT

经过了理论部分,现在到了该实现IDT的时候了。

首先是trapentry.S, 在这个文件中提供了如下两个宏:

作用是压入中断号,跳转到_alltraps;其中对于压入错误码的使用TRAPHANDLER,对于不压入错误码的使用TRAPHANDLER_NOEC。此处入口的name应该是一个函数的名字,正如内部声明:.type name, @function; /* symbol type is function */

#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

#define TRAPHANDLER_NOEC(name, num)					\
	.globl name;									\
	.type name, @function;							\
	.align 2;										\
	name:											\
	pushl $0;										\
	pushl $(num);									\
	jmp _alltraps

阅读注释,可以完善该文件:

_alltraps中的push %esp 相当于传递了一个Trapframe结构,因为经典的5个字段由处理器自动压入,而_alltraps中压入的顺序,正好可以与Trapframe结构对应起来,因此trap函数可以获得Trapframe信息。

/*
 * Lab 3: Your code here for generating entry points for the different traps.
 */
	TRAPHANDLER_NOEC(int0,0);
	TRAPHANDLER_NOEC(int1,1);
	TRAPHANDLER_NOEC(int2,2);
	TRAPHANDLER_NOEC(int3,3);
	TRAPHANDLER_NOEC(int4,4);
	TRAPHANDLER_NOEC(int5,5);
	TRAPHANDLER_NOEC(int6,6);
	TRAPHANDLER_NOEC(int7,7);
	TRAPHANDLER(int8,8);
	TRAPHANDLER(int10,10);
	TRAPHANDLER(int11,11);
	TRAPHANDLER(int12,12);
	TRAPHANDLER(int13,13);
	TRAPHANDLER(int14,14);
	TRAPHANDLER_NOEC(int16,16);
	TRAPHANDLER_NOEC(__syscall,T_SYSCALL);
/*
 * Lab 3: Your code here for _alltraps
 */
_alltraps:
	pushl %ds
	pushl %es
	pushal
	push $GD_KD
	popl %ds
	push $GD_KD
	popl %es
	pushl %esp
	call trap

下面要建立IDT,首先关于门描述符,在mmu.h中提供了相关的工具

// Gate descriptors for interrupts and traps
struct Gatedesc {
	unsigned gd_off_15_0 : 16;   // low 16 bits of offset in segment
	unsigned gd_sel : 16;        // segment selector
	unsigned gd_args : 5;        // # args, 0 for interrupt/trap gates
	unsigned gd_rsv1 : 3;        // reserved(should be zero I guess)
	unsigned gd_type : 4;        // type(STS_{TG,IG32,TG32})
	unsigned gd_s : 1;           // must be 0 (system)
	unsigned gd_dpl : 2;         // descriptor(meaning new) privilege level
	unsigned gd_p : 1;           // Present
	unsigned gd_off_31_16 : 16;  // high bits of offset in segment
};

// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
    //   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
    //   interrupt-enable flag). An interrupt that vectors through an
    //   interrupt gate resets IF, thereby preventing other interrupts from
    //   interfering with the current interrupt handler. A subsequent IRET
    //   instruction restores IF to the value in the EFLAGS image on the
    //   stack. An interrupt through a trap gate does not change 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;		\
}

因此trap_init()函数如下

void
trap_init(void)
{
	extern struct Segdesc gdt[];

	// LAB 3: Your code here.
	void int0();
	void int1();
	void int2();
	void int3();
	void int4();
	void int5();
	void int6();
	void int7();
	void int8();
	void int10();
	void int11();
	void int12();
	void int13();
	void int14();
	void int16();
	void _syscall_();

	SETGATE(idt[0],0,GD_KT,int0,0);
	SETGATE(idt[1],0,GD_KT,int1,0);
	SETGATE(idt[2],0,GD_KT,int2,0);
	SETGATE(idt[3],0,GD_KT,int3,0);
	SETGATE(idt[4],0,GD_KT,int4,0);
	SETGATE(idt[5],0,GD_KT,int5,0);
	SETGATE(idt[6],0,GD_KT,int6,0);
	SETGATE(idt[7],0,GD_KT,int7,0);
	SETGATE(idt[8],0,GD_KT,int8,0);
	SETGATE(idt[10],0,GD_KT,int10,0);
	SETGATE(idt[11],0,GD_KT,int11,0);
	SETGATE(idt[12],0,GD_KT,int12,0);
	SETGATE(idt[13],0,GD_KT,int13,0);
	SETGATE(idt[14],0,GD_KT,int14,0);
	SETGATE(idt[16],0,GD_KT,int16,0);
	SETGATE(idt[T_SYSCALL],0,GD_KT,_syscall_,0);

	// Per-CPU setup 
	trap_init_percpu();
}

至此,函数的调用关系如图:

当遇到中断时,会调用trap:

trap会打印出相关的信息。

现在可以开始测试了:

实验三的A部分到此完结。下一篇文章,关于PartA 的一些问题和PartB

posted @ 2021-11-07 15:55  OasisYang  阅读(674)  评论(0编辑  收藏  举报