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)会发生什么?
问题没有理解...也先占个坑吧.....