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

编程

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(

" movl %0,%%esp

" // esp指向tf结构,弹出时会弹到tf里

" popal

" // 弹出tf_regs中值到各通用寄存器

" popl %%es

" // 弹出tf_es 到 es寄存器

" popl %%ds

" // 弹出tf_ds 到 ds寄存器

" addl $0x8,%%esp

" // 跳过tf_trapno和tf_err

" iret

" // 中断返回 弹出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

以上是 MIT6.828——Lab3PartA(麻省理工操作系统实验) 的全部内容, 来源链接: utcz.com/z/520018.html

回到顶部