• 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

  • 相关阅读:
    Spring Boot中的那些生命周期和其中的可扩展点(转)
    mongodb,redis,mysql的区别和具体应用场景(转)
    linux相关知识
    docker安装应用整理
    SpEL表达式总结(转)
    An association from the table user_ product refers to an unmapped class: com. hiber.pojo. User
    LoadRunner安装时提示缺少C++ 2005 SP1(x86)插件
    Web框架,Hibernate向数据库插入数据,数据库没有值怎么办?
    数据库忘记原来的密码
    在线手机验证码免费查验接收
  • 原文地址:https://www.cnblogs.com/oasisyang/p/15520180.html
Copyright © 2020-2023  润新知