Lab3 PartA: User Environments and Exception Handing

Exercise 1

Allocating the Environments Array

修改在kern/pmap.c文件中mem_init()函数的代码,使其能分配和映射envs数组。这个数组完全是由NENV个ENV结构组成的。支持envs所在的内存空间也应该在UENVS虚拟地址处(定义在inc/memlayout.h)被映射成用户只读。

在lab2中,在mem_init()中分配了内存给page[]数组,它记录了内核中哪些页是空闲的哪些页不是。需要做的是修改mem_init函数,分配一个Env结构数组,叫envs。

参考page数组的分配来写代码即可。boot_map_region()中size的值赋为PTSIZE,原因是在inc/memlayout.h中可见,envs的大小为PTSIZE.

 // Make 'envs' point to an array of size 'NENV' of 'struct Env'.
        // LAB 3: Your code here.
        envs = (struct Env*) boot_alloc(sizeof(struct Env)*NENV);
        memset(envs,0,sizeof(struct Env) * NENV);


// LAB 3: Your code here.
         boot_map_region(kern_pgdir,UENVS,PTSIZE,PADDR(envs),PTE_U|PTE_P);

嗯居然报错了?

是boot_malloc没有分配成功吗?

参考了https://blog.csdn.net/wysiwygo/article/details/104296090   这篇博客,发现是boot_malloc中end的值出了问题,并没有指向数据区域的最后,而是指向了数据区域的内,照着这篇博客给出的解决方法解决了,成功输出check_kern_pgdir() succeeded!

但是为啥这个错误没有在lab2中出现?

Exercise 2

Creating and Running Environments

现在需要在kern/env.c文件中写代码来运行用户环境。因为暂时还没有一个文件系统,所以将设置内核来加载一个静态二进制映像,它嵌入在内核本身中。JOS将这个二进制文件作为ELF可执行映像嵌入到内核中。

Lab3里的GNUmakefile在obj/user/目录下生成了一系列二进制映像文件。如果你看一下kern/Makefrag文件,你就会发现一些魔术把二进制文件直接链接到内核可执行文件中,只要这些文件是.o文件。链接器命令行上的-b二进制选项导致这些文件作为“原始”未解释的二进制文件链接,而不是作为编译器生成的常规.o文件链接。

在kern/init.c文件中的i386_init函数中,将会看到在环境中运行其中一个二进制映像的代码。然而,设置用户环境的关键代码还不完整,需要去完成它。

在文件env.c中,完成下列函数的代码。

env_init()

初始化所有的在envs数组中的 Env结构体,并把它们加入到 env_free_list中。 还要调用 env_init_percpu,这个函数要配置段式内存管理系统,让它所管理的段,可能具有两种访问优先级其中的一种,一个是内核运行时的0优先级,以及用户运行时的3优先级。

注意在env_free_list中需要与环境的顺序一样。

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

为一个新的用户环境分配一个页目录表,并且初始化这个用户环境的地址空间中的和内核相关的部分。

NPDENTRIES是否可以换成PDX(UVPT)?

 // LAB 3: Your code here.
        e->env_pgdir = (pde_t *) page2kva(p);
        p->pp_ref++;

        for(i=0;i<PDX(UTOP);i++){
          e->env_pgdir[i] = 0;
        }
        //NPDENTRIES is 1024, the number of page table entries
        for(i=PDX(UTOP);i<NPDENTRIES;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;

region_alloc()

为用户环境分配物理地址空间。

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!)
        struct PageInfo *pp;
        uint32_t start = ROUNDDOWN((uint32_t)va,PGSIZE);
        uint32_t end = ROUNDUP((uint32_t)va+len,PGSIZE);
        int i;
        for(i=start;i<end;i+=PGSIZE){
          pp = (struct PageInfo *)page_alloc(0);
         if(!pp){
          panic("region_alloc failed!\n");
         }
         if(page_insert(e->env_pgdir,pp,(void*)i,PTE_W|PTE_U)!=0)
                panic("region alloc error\n");
        }
}

load_icode()

分析一个ELF文件,类似于boot loader做的那样,我们可以把它的内容加载到用户环境下。用户环境建立,可以加载用户ELF文件并执行。(目前还没有文件系统,需要在内核代码硬编码需要加载的用户程序)

参数e表示需要操作的用户环境,binary表示可执行用户代码的起始地址。所以可以回答我在代码注释中提出的疑问,这个elf文件和加载内核时的elf文件不是同一个文件。

tf保存的是陷入内核时的environment的信息以便后面能恢复,因此在tf_eip中存储elfhdr->e_entry的值,使得切入到用户模式,这就是执行的第一条指令。

struct Proghdr *ph, *eph;

        // if it is possible to define elfhdr as ELFHDR directly?
        // get program's ELF's address. 
        struct Elf *elfhdr = (struct Elf*) binary;

        //is this a valid ELF?
        if(elfhdr->e_magic != ELF_MAGIC)
                panic("elf header's magic is wrong\n");

        //load each program segment (how do this? I don't know.)
        ph = (struct Proghdr *)((uint8_t *) elfhdr + elfhdr->e_phoff);
        eph = ph + elfhdr->e_phnum;

        lcr3(PADDR(e->env_pgdir));
        for(; ph<eph; ph++){
           if(ph->p_type != ELF_PROG_LOAD)
                   continue;
           if(ph->p_filesz > ph->p_memsz)
                   panic("filesz is larger than memsz, error\n");
           region_alloc(e,(void *)ph->p_va, ph->p_memsz);
           memset((void *)ph->p_va,0,ph->p_memsz);
           memcpy((void *)ph->p_va,binary+ph->p_offset,ph->p_filesz);
        }
         e->env_tf.tf_eip = elfhdr->e_entry; //???

        // Now map one page for the program's initial stack
        // at virtual address USTACKTOP - PGSIZE.
        lcr3(PADDR(kern_pgdir));
        // LAB 3: Your code here.
        region_alloc(e,(void *)(USTACKTOP-PGSIZE),PGSIZE);

env_create()

利用env_alloc函数和load_icode函数,加载一个ELF文件到用户环境中

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

env_run()

在用户模式下,开始运行一个用户环境。

env_pop_tf的作用是将e->env_tf结构中的寄存器快照弹出到寄存器中,

if(curenv && curenv->env_status == ENV_RUNNING){
           curenv->env_status = ENV_RUNNABLE;
        }

        curenv = e;
        curenv->env_status = ENV_RUNNING;
        curenv->env_runs++;
        lcr3(PADDR(curenv->env_pgdir)); //why?

        env_pop_tf(&curenv->env_tf);//what meaning?

执行上述程序后,发生了"tripple fault"。我们需要来解决这个问题,开始用gdb调试器进行调试。

在env_pop_tf设置断点,这条指令时即将进入用户模式的最后一条指令,然后用si命令进行单步调试,直到碰到iret指令。

(gdb) b env_pop_tf
Breakpoint 1 at 0xf0103937: file kern/env.c, line 480.
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0xf0103937 <env_pop_tf>:	push   %ebp

Breakpoint 1, env_pop_tf (tf=0xf01d1000) at kern/env.c:480
480	{
(gdb) si
=> 0xf0103938 <env_pop_tf+1>:	mov    %esp,%ebp
0xf0103938	480	{
(gdb) si
=> 0xf010393a <env_pop_tf+3>:	push   %ebx
0xf010393a	480	{
(gdb) si
=> 0xf010393b <env_pop_tf+4>:	sub    $0x8,%esp
0xf010393b	480	{

......

(gdb) si
=> 0xf0103952 <env_pop_tf+27>:	iret   
0xf0103952	481		asm volatile(
(gdb) info registers
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            0xf0103952	0xf0103952 <env_pop_tf+27>
eflags         0x96	[ PF AF SF ]
cs             0x8	8
ss             0x10	16
ds             0x23	35
es             0x23	35
fs             0x23	35
gs             0x23	35
(gdb) si
=> 0x800020:	cmp    $0xeebfe000,%esp

执行完iret指令后进入用户模式。就是在lib/entry.S中可见的start命令。USTACKTOP的地址就是0xeebfe000.

接下来设置断点b *0x... 设置一个断点在hello文件(obj/user/hello.asm)中的sys_cputs函数中的 int $0x30 指令处。这个int指令是一个系统调用,用来展示一个字符到控制台。在obj/user/hello.asm中可查看到该指令的位置。

设置断点。查看寄存器,这条指令开始就进入了死胡同,没法执行下一条。

Breakpoint 2, 0x00800b44 in ?? ()
(gdb) si
=> 0x800b44:	int    $0x30

Breakpoint 2, 0x00800b44 in ?? ()
(gdb) info reg
eax            0x0	0
ecx            0xd	13
edx            0xeebfde88	-289415544
ebx            0x0	0
esp            0xeebfde54	0xeebfde54
ebp            0xeebfde60	0xeebfde60
esi            0x0	0
edi            0x0	0
eip            0x800b44	0x800b44
eflags         0x92	[ AF SF ]
cs             0x1b	27
ss             0x23	35
ds             0x23	35
es             0x23	35
fs             0x23	35
gs             0x23	35

Exercise 3

Handling Interrupts and Exceptions

关于这个练习参考了https://www.cnblogs.com/fatsheep9146/p/5341836.html这篇博客的翻译,给出的手册等碰到不懂的再去翻吧...读英文资料好吃力好费时间...

碰到的问题是还没分清哪些错误是外部中断哪些是内部异常。记录一下。

Exercise 4

设置IDT表,在JOS处理异常,暂时还只需要处理内部异常(中断向量0-31)。最后需要实现的代码效果如下:

 每一个中断或异常都有它自己的中断处理函数,分别定义在 trapentry.S中,trap_init()将初始化IDT表。每一个处理函数都应该构建一个结构体 Trapframe 在堆栈上,并且调用trap()函数指向这个结构体,trap()然后处理异常/中断,给他分配一个中断处理函数。

整个操作系统的中断控制流程如下

1.trap_init()将所有的中断处理函数的起始地址放到中断向量表IDT中。

2.中断发生时,CPU捕捉到该中断(不管是外部中断还是内部中断),进入内核态,根据中断向量咋中断向量表中进行查询,找到对应表项。

3.保存被中断的程序的上下文到内核堆栈中,调用这个表项中指明的中断处理函数。

4.执行中断处理函数,恢复被中断的进程的上下文,返回用户态,继续运行这个进程。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

    实验内容

编辑一下trapentry.S 和 trap.c 文件,并且实现上面所说的功能。宏定义 TRAPHANDLER 和 TRAPHANDLER_NOEC ,以及定义在inc/trap.h中的T_*,会有帮助。在 trapentry.S文件中为在inc/trap.h文件中的每一个trap加入一个入口指针, 对于在inc/trap.h中定义的每个陷阱,必须提供TRAPHANDLER宏引用的_alltraps。还需要修改trap_init()来初始化idt,以指向trapentry中定义的每个入口指针,SETGATE宏在这里会很有帮助。

  需要修改trap_init()函数来初始化idt表,使表中每一项指向定义在trapentry.S中的入口指针,SETGATE宏定义在这里用得上。  

实验过程

第一步,处理trap.c和trapentry.S文件,尝试将中断处理函数加入到中断向量表中。

查看一下inc/trap.h中定义的T_*和在inc/mmu.h中的SETGATE宏定义。

打开kern/trap.c文件,按照上图中的中断向量在trap_init()函数中定义中断处理函数。参考SETGATE宏定义,把中断向量处理函数加入到中断向量表中。

在SETGATE宏定义中,gate是IDT表的索引入口;

istrap:1代表Exception是trap,可在这个手册中查询。https://pdos.csail.mit.edu/6.828/2018/readings/i386/s09_09.htm

sel是代码段选择子,GD_KT就是kernel text.

off是代码段偏移。

dpl:描述符特权级。

 // LAB 3: Your code here.
        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();

        void syscall_handler();

SETGATE(idt[T_DIVIDE], 0, GD_KT, divide_handler, 0);
    SETGATE(idt[T_DEBUG], 0, GD_KT, debug_handler, 0);
    SETGATE(idt[T_NMI], 0, GD_KT, nmi_handler, 0);
    SETGATE(idt[T_BRKPT], 1, GD_KT, brkpt_handler, 0);
    SETGATE(idt[T_OFLOW], 1, GD_KT, oflow_handler, 0);
    SETGATE(idt[T_BOUND], 0, GD_KT, bound_handler, 0);
    SETGATE(idt[T_DEVICE], 0, GD_KT, device_handler, 0);
    SETGATE(idt[T_ILLOP], 0, GD_KT, illop_handler, 0);
    SETGATE(idt[T_DBLFLT], 0, GD_KT, dblflt_handler, 0);
    SETGATE(idt[T_TSS], 0, GD_KT, tss_handler, 0);
    SETGATE(idt[T_SEGNP], 0, GD_KT, segnp_handler, 0);
    SETGATE(idt[T_STACK], 0, GD_KT, stack_handler, 0);
    SETGATE(idt[T_GPFLT], 0, GD_KT, gpflt_handler, 0);
    SETGATE(idt[T_PGFLT], 0, GD_KT, pgflt_handler, 0);
    SETGATE(idt[T_FPERR], 0, 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);

 然后参考宏定义TRAPHANDLER 和 TRAPHANDLER_NOEC,在 trapentry.S文件中为在inc/trap.h文件中的每一个trap加入一个入口指针。这两个宏的差别在于执行的trap是否有error code, 参考官方给出的参考手册可以获知。

/*
 * Lab 3: Your code here for generating entry points for the different traps.
 */
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(align_handler,T_ALIGN);
TRAPHANDLER_NOEC(mchk_handler,T_MCHK);
TRAPHANDLER_NOEC(simderr_handler,T_SIMDERR);
TRAPHANDLER_NOEC(syscall_handler,T_SYSCALL);

 然后调用 _alltraps,_alltraps函数其实就是为了能够让程序在之后调用trap.c中的trap函数时,能够正确的访问到输入的参数。

实现的 _alltraps 应该:

  1. 把值压入堆栈使堆栈看起来像一个结构体 Trapframe。

  2. 加载 GD_KD 的值到 %ds, %es寄存器中

  3. 把%esp的值压入,并且传递一个指向Trapframe的指针到trap()函数中。

  4. 调用trap

  考虑使用pushal指令,他会很好的和结构体 Trapframe 的布局配合好。

首先那就打开查看一下Trapframe结构体(在inc/trap.h里)的内容。压入堆栈的顺序是从下到上。ss,esp已经存在在堆栈里了,cs, eip等通过硬件压入,剩下只有ds,es和tf_regs; 因此通过pushl依次压入,pushal压入的是tf_regs。接下来照着步骤来即可,代码在下方。

还没有特别搞懂官方给出的每一步的原理.....

_alltraps:
pushl %ds
pushl %es
pushal

movl $GD_KD, %eax
movl %eax, %ds
movl %eax, %es

pushl %esp

call trap

make qemu,没出现tripple fault了,make grade

QUESTION

问题2:

需要做什么才能使user/softint程序正常运行? 评分脚本期望它产生一般保护错误(trap 13),但softint的代码为int $14。 为什么这会产生中断向量13? 如果内核实际上允许softint的int $14指令调用内核的页面错误处理程序(中断向量14)会发生什么?

问题没有理解...也先占个坑吧.....

猜你喜欢

转载自blog.csdn.net/qq_43012789/article/details/107788801