LAB3_Part A User Environments and Exception Handling
前言
记录一下自己的学习过程
实验内容翻译:
https://gitee.com/cherrydance/mit6.828
该翻译仅供参考
先得到lab3的代码:
练习1
请在kern/pmap.c中修改mem_init()函数,以分配并映射envs数组。这个数组包含了正好NENV个Env结构的实例,分配方式与你分配pages数组的方式非常相似。与pages数组类似,支持envs的内存也应该以只读方式映射到UENVS(在inc/memlayout.h中定义),这样用户进程就可以从这个数组中读取数据。
你应该运行你的代码,确保check_kern_pgdir()函数执行成功。
让我们分配并映射envs数组。这个lab2完成了就很简单,照着写就行了。代码如下:
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);
结果如图:
check_kern_pgdir() succeeded。
练习2
在env.c文件中,完成以下函数的编写:
env_init()
初始化envs数组中的所有Env结构,并将它们添加到env_free_list。还调用env_init_percpu函数,为特权级0(内核)和特权级3(用户)配置分段硬件。
env_setup_vm()
为新环境分配一个页目录,并初始化新环境地址空间的内核部分。
region_alloc()
为环境分配和映射物理内存。
load_icode()
你需要解析一个ELF二进制映像,类似于引导加载程序已经做的那样,并将其内容加载到新环境的用户地址空间中。
env_create()
使用env_alloc分配一个环境,并调用load_icode将ELF二进制文件加载到其中。
env_run()
以用户模式启动给定的环境。
在编写这些函数时,你可能会发现新的cprintf转义符%e很有用——它打印与错误代码相对应的描述。例如,
r = -E_NO_MEM;
panic(“env_alloc: %e”, r);
将会导致panic,并输出消息"env_alloc: out of memory"(内存不足)。
env_init()函数:
初始化envs数组的Env结构体并添加到env_free_list链表中。关键点在于第一个分配的env要是envs[0],其次就是考虑初始化需要初始化哪些参数。
代码如下:
int i = NENV - 1;
env_free_list = NULL;
for(; 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];
}
env_setup_vm()函数:
作用是为环境e分配一个页目录即为e->env_pgdir设置值,在提示中有说明需要将页面的pp_ref++,此外,也说明了可以使用kern_pgdir作为模板。
++p->pp_ref;
e->env_pgdir = (pde_t *)page2kva(p);
for(i = 0; i < PDX(UTOP); ++i){//UTOP以下为空
e->env_pgdir[i] = 0;
}
for(i = PDX(UTOP); i < NPDENTRIES; ++i){//以上赋值kern_pgdir
e->env_pgdir[i] = kern_pgdir[i];
}
region_alloc()函数:
为环境env分配len字节的物理内存,并将其映射到环境地址空间中的虚拟地址va处。
不以任何方式对映射的页面进行清零或初始化。页面应该由用户和内核可写。您应该将va向下取整,并将(va + len)向上取整。
代码如下:
va = ROUNDDOWN(va, PGSIZE);//向下取整
void *end = ROUNDUP(va + len, PGSIZE);//向上取整
struct PageInfo *new_page = NULL;
for(; va < end; va += PGSIZE){
new_page = page_alloc(0);//分配一个页面
if(!new_page){
panic("no free page\n");
}
//把页面映射到va
if(page_insert(e->env_pgdir, new_page, va, PTE_U | PTE_W) == -E_NO_MEM){
panic("page insert fail in region_alloc\n");
}
}
load_icode()函数:
解析一个ELF二进制映像,类似于引导加载程序已经做的那样,并将其内容加载到新环境的用户地址空间中。该函数从ELF二进制映像中加载所有可加载的段到环境的用户内存中,从ELF程序头中指示的适当虚拟地址开始。同时,它会将程序头中标记为映射但实际上不在ELF文件中的部分清零 - 即程序的bss段。
关于bootmain函数的解析在前面已经做过了,这里就是仿照bootmain来写代码。
代码如下:
struct Elf *elf = (struct Elf *)binary;//得到elf文件
if(elf->e_magic != ELF_MAGIC){
panic("illegal ELF format");
}
lcr3(PADDR(e->env_pgdir));
//把地址空间转化成当前用户空间,这是在env_run()的第五条提示给出来的。
struct Proghdr *ph = (struct Proghdr *)((uint8_t *)(elf) + elf->e_phoff);
struct Proghdr *eph = ph + elf->e_phnum;
for (; ph < eph; ++ph) {
if (ph->p_type == ELF_PROG_LOAD) {//如果p_type==ELF_PROG_LOAD才加载
region_alloc(e, (void *)ph->p_va, ph->p_memsz);
memmove((void *)ph->p_pa, binary + ph->p_offset, ph->p_filesz);
memset((void *)(ph->p_pa + ph->p_filesz), 0, ph->p_memsz - ph-> p_filesz);
}
}
//tf_eip表示下一条指令的地址,根据bootmain在elf加载之后就是进入e_entry
e->env_tf.tf_eip = elf->e_entry;
lcr3(PADDR(kern_pgdir));
// Now map one page for the program's initial stack
// at virtual address USTACKTOP - PGSIZE.
region_alloc(e, (void *)(USTACKTOP - PGSIZE), PGSIZE);
这个函数的实现有点不太明白,关键就是地址空间的切换。什么时候是内核空间,什么时候是用户空间,这点要搞明白。我目前的想法就是如果用到内核的页目录就是内核空间,要用到用户的页目录就是用户地址空间。不是很清楚。
env_create()函数
使用env_alloc分配一个环境,并调用load_icode将ELF二进制文件加载到其中。
首先得清楚env_alloc函数是将分配一个新环境初始化并存储到函数的第一个参数之中。
代码如下:
struct Env *new_env = NULL;
int flag = env_alloc(&new_env, 0);
if(flag == -E_NO_FREE_ENV){
panic("env_alloc: %e", flag);
}
if(flag == -E_NO_MEM){
panic("env_alloc: %e", flag);
}
load_icode(new_env, binary);
new_env->env_type = type;
env_run()
以用户模式启动给定的环境。代码如下:
if(curenv && ENV_RUNNING == curenv->env_status){
curenv->env_status = ENV_RUNNABLE;
}
curenv = e;
curenv->env_status = ENV_RUNNING;
++curenv->env_runs;
lcr3(PADDR(e->env_pgdir));
env_pop_tf(&curenv->env_tf);
以下是代码的调用图,展示了在调用用户代码之前的代码执行过程。确保你理解每个步骤的目的。
entry.S进行一些初始化操作跳转到c语言执行入口。cons_init初始化窗口,mem_init初始化内核内存进行分页,env_init初始化用户环境,trap_init初始化中断和异常处理。env_create创造一个用户环境,env_run执行用户环境。env_pop_tf是恢复环境的各种寄存器状态,退出内核执行某个环境的代码。
根据练习内容打开gdb,b env_pop_tf
设置断点,运行到该函数,接着使用si运行到iret进入到用户模式。
找到要求的int $0x30
指令,设置断点b *0x800bcd
运行到断点处,程序停在了该指令说明正确,如果无法执行到int指令,那么你的地址空间设置或程序加载代码可能存在问题,请返回并修复它们后再继续。
总结
完成了lab3 partA的前半部分,这部分主要是完成对用户环境操作的一些函数。