今天是2016年1月20日,距离回家过年不到10天了,我要尽快把这个lab做完,任务很艰巨!(真是呵呵了,现在已经是2月21日了,我还在写。。。)
注意:文中trap 在有的地方被认为是 Exception,特别是在与Interrupt平行出现时。
Part A: User Environments and Exception Handling
首先是如下三个变量:kern/env.c
然后我们需要对 struct Env 有个较细致的理解:
inc/env.h
Allocating the Environments Array
这部分完全仿照 pages 即可。如下:
kern/pmap.c
Creating and Running Environments
要知道到目前为止,JOS是没有文件系统的,那么如果在JOS上运行用户程序怎么办?——直接和内核镜像结合在一起,在编译时期就做好。后面lab4就会让我们自己完善好文件系统.
首先是 env_init() 函数:
接着是 env_setup_vm() 函数:
需要注意的是:
1)可以看出使用了 page2kva() 函数转化地址,也即默认当前 p 是在 0 ~ 256MB 的物理页上(注意 mem_init()函数将 256MB 的物理页映射到虚拟地址 0xf0000000 ~ 0xffffffff)。
2)拷贝从虚拟地址 kern_pgdir 开始的 PGSIZE 大小的内容到新的PD e->env_pgdir。
然后是 region_alloc() 函数:
开始我是这样写的,回头发现有问题,比如当 va 页不对齐时,而 le 刚好为一页时,此时可知实际分配了 ROUNDUP(len, PGSIZE) = PGSIZE ,不过是从 ROUNDDOWN(va, PGSIZE) 开始的,但是实际在使用时我们从虚拟地址 va 开始使用,也即此时我们分配的物理大小是不够用的。因此我们应该参照上面注释说的方法。如下:
这里需要注意区分与(static)静态映射概念,静态映射是指没有分配实际的物理页,通过boot_map_region()执行而非 page_insert()。boot_map_region()
的操作空间是内核虚拟地址空间,它提供的映射是静态映射,不涉及物理页的分配。而 page_alloc() 则是要对实际的物理页面分配映射到当前用户的虚拟地址空间中。(看了下代码,感觉没太多差别,主要是 page_insert() 函数会为新映射的 page 的pp_ref加一,而boot_map_region()不会)
接着是 load_icode() :
这部分代码相对陌生些,涉及到了 ELF 文件。这部分代码基本就是抄抄抄,很多地方还不懂。。
JOS到现在为止还没有文件系统,所以为了测试我们能运行用户程序,现在的做法是将用户程序编译以后和内核链接到一起(即用户程序紧接着内核后面放置)。所以这个函数的作用就是将嵌入在内核中的用户程序取出释放到相应链接器指定好的用户虚拟空间里。这里的binary指针,就是用户程序在内核中的开始位置的虚拟地址。
按照注释的提示,我们可以参照boot/main.c来完成相应的载入,但是有几个地方需要注意:
1、对于用户程序ELF文件的每个程序头ph,ph→p_memsz和ph→p_filesz是两个概念,前者是该程序头应在内存中占用的空间大小,而后者是实际该程序头占用的空间大小。它们俩的区别就是ELF文件中BSS节中那些没有被初始化的静态变量,这些变量不会被分配文件储存空间,但是在实际载入后,需要在内存中给与相应的空间,并且全部初始化为0。所以具体来讲,就是每个程序段ph,总共占用p_memsz的内存,前面p_filesz的空间从binary的对应内存复制过来,后面剩下的空间全部清0。
2、ph→p_va是该程序段应该被放入的虚拟空间地址,但是注意,在这个时候,虚拟地址空间应该是用户环境Env的虚拟地址空间。可是,在进入 load_icode() 时,是内核态进入的,所以虚拟地址空间还是内核的空间。所以我们使用 lcr3(PADDR(e->env_pgdir)) 指令载入用户环境的PD。其中的 e->env_pgdir 是在 env_setup_vm() 函数里面设置好的。但是仍要小心的是,对于ELF载入完毕以后,我们就不需要对用户空间进行操作了,所以在函数的最后要重新切回到内核虚拟地址空间来。
3、注释中还提到了要对程序的入口地址作一定的设置,这里对应的操作是 e->env_tf.tf_eip = elfhdr->e_entry 这里涉及到对struct Trapframe 结构的具体介绍,我们留到下一个函数 env_create() 的时候进行详细介绍。
还需要注意的是指针的计算。需要注意的是:void * 指针加一时,其值就是加一,同理uint8_t * 也是这样。所以以上的写法的结果与下面一样,但是下面写法才是对的,因为其应该是指针计算,而上面的写法在计算时ph->p_va类型是整型。
或者我们也可以使用这种写法:
这部分我也纠结了好久。。一定小心指针计算!
下面是 env_creat() 函数:
这个相对简单,注意 env_alloc() 函数里已经运行过 env_setup_vm()。
最后是 env_run() 函数:
这里的env_pop_tf实现了进程的真正切换,原理就是依据之前进程已经设置好的trapframe,然后把这个进程保存好的属于自己的trapframe通过弹栈的形式,输出到各个寄存器当中,实现进程环境的替换,而这里面也包括 eip,也就意味着,当从env_pop_tf里面的iret返回的时候,就开始从调用结构体e描述的进程开始运行了。
完成以上代码后,make 之后会发生 Triple fault 错误,不要怕,正常的。。。
在MIT的课程材料上解释了这样的原因。因为我们没有对中断表进行相应的设置,以至于用户程序在调用系统终端输出字符时产生了错误。但是我们需要认为的确认一下是否真的错误是由中断而不是其他设置造成的,所以我们启动GDB调试,选择在 env_pop_tf() 函数停下:
擦,今天新学一招,原来可以这样设置断点: b 函数名
好便利。如下两种断点设置方法(都怪当初不仔细看说明):
从这里开始单步跟踪,在 IRET 指令之前停下来,我们在这里查看寄存器的信息看是否都被设置好了:
从EAX、ECX等寄存器中看到都被清0了,DS,ES寄存器内容为0x23,这个和我们在 env_alloc() 中看到的设置是一致的,但是在IRET执行之前CS和EIP两个寄存器都还看不到,不过没有关系,我们知道栈顶的接下来三个DWORD分别为EIP、CS和EFLAGS,我们查看一下栈顶的这三个DWORD:
可以看到EIP的值为0x00800020即用户程序的入口地址,我们可以打开user/user.ld文件查看一下:
发现是符合的。这就说明我们正确的将入口地址加载进来了,接下来我们看看是否正确载入了用户程序的ELF文件:
实际的用户程序hello的汇编代码可以在obj/user/hello.asm中找到:
发现是一致的,从这里可以知道我们的 load_icode() 的载入是正常工作的。