实验概览
整个实验的流程如下:
1.首先初始化所有用户环境状态信息envs structure
,初始化IDT
中的entries,分别指向对应handler的入口地址
2.然后创建一个hello用户程序
,信息从二进制文件binary_obj_user_hello_start加载到hello的用户内存中
3.由于现在还在kernel mode
所以通过env_pop_tf(&curenv->env_tf)
加载好所有寄存器值
,iret指令
后进入user mode
运行这个hello程序。
4.进入用户态后,执行的第一条指令从CS:EIP
去找,找到是lib/entry.S/_start
,_start里调用了libmain()
5.进入libmain()后,通过系统调用lib/sys_getenvid()
获得thisenv的值(lib/syscall(…)里int %T_SYSCALL
指令从用户态陷入内核态,内核里调用kern/sys_getenvid()
获得id后又通过env_pop_tf()
返回用户态继续执行)
然后又通过两个系统调用sys_cputs()
打印出“hello world"跟下一条信息。
最后通过系统调用sys_env_destroy()
结束hello程序,然后开始进入monitor
具体调用流程见下方,超有成就感:
env_init()所有env加入env_free_list,env[0]在表头、per-cpu(Load GDT and 段选择器)
-->trap_init()让IDT条目指向对应处理函数入口地址、trap_init_percpu
-->env_create(binary_obj_user_hello_start, type)这里面又包括两个函数
-->env_alloc(&e,0);通过env_setup_vm初始化虚拟内存(页表)、初始化env各信息包括'e->env_tf'、从env_free_list中取出env[0]
-->load_icode(e,binary)从binary中加载程序段到e对应的内存空间
-->env_run(&env[0]);curenv=env[0],改好状态
-->env_pop_tf(&curenv->env_tf)把&env_tf为起始地址的一段空间'当成栈',逐步popa、pop、iret到相应寄存器。且iret结束后进入'user mode'
-->寄存器都赋值了,开始执行eip=env_tf.tf_eip=0x800020 即lib/entry.S/_start,然后call libmain
-->lib/libmain.c/libmain()先通过系统调用赋值thisenv,然后调用umain(),最后exit()
--> thisenv = &envs[ENVX(sys_getenvid())];
//下面开始sys_getenvid系统调用的过程
-->lib/sys_getenvid()赋值thisenv
lib/sys_getenvid()包含了syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0);
-->syscall(...)包含int %T_SYSCALL,中断向量48号,从IDT[48]中找到对应的处理函数入口地址syscall_handler(),陷入'内核态'
此外,还会传入system call num(%eax中),EDX, ECX, EBX, EDI, ESI的值作为参数
-->kern/trapentry.S/TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL);
_alltraps 完善栈,让栈看上去像Trapframe结构,请看上面问题4
call trap
-->trap(tf) 重设方向标志位DF、从栈里copy trap frame到curenv->env_tf,然后tf(很重要)再指向它方便返回curenv继续执行
-->trap_dispatch(tf);
tf->tf_trapno == T_SYSCALL所以调用kern/syscall() //注意跟lib/syscall()区分。这里是内核态下了
-->kern/syscall()
syscallno==SYS_getenvid所以调用kern/sys_getenvid() //注意区分哟
-->sys_getenvid()返回curenv->env_id到'tf->tf_regs.reg_eax'中
-->env_run(curenv) 设置curenv的状态,调用env_pop_tf
-->env_pop_tf(&curenv->env_tf);将tf里的数据读到对应'寄存器'中,然后进入'user mode'
-->'iret'指令后进入user mode,也就是说回到umain()的thisenv处继续往下执行
-->umain()
//下面开始cprintf("hello, world\n");里的sys_cputs()系统调用过程
-->user/hello.c/umain(),里面有lib/cprintf()函数,cprintf函数最内部'系统调用sys_cputs()'组成
cprintf()调用vcprintf()调用vprintmt()调用putch()调用cputchar()调用lib/sys_cputs()调用syscall()
//这里一定要注意区分lib/syscall.c跟kern/syscall.c,只有前者才包含系统调用,从用户态陷入内核态
-->lib/sys_cputs()包含了syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);同样通过'int %T_SYSCALL'进入kern/trapentry.S,即陷入'内核态'
-->kern/trapentry.S/TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL);
_alltraps 完善栈,让栈看上去像Trapframe结构,请看上面问题4
call trap
-->trap(tf) 重设方向标志位DF、从栈里copy trap frame到curenv->env_tf,然后tf(很重要)再指向它方便返回curenv继续执行
-->trap_dispatch(tf);
tf->tf_trapno == T_SYSCALL所以调用kern/syscall() //注意跟lib/syscall()区分。这里是内核态下了
-->kern/syscall()
syscallno==SYS_cputs所以调用kern/sys_cputs() //当前在内核态,注意区分哟
-->sys_cputs()检查内存空间[s,s+len)的权限后调用kern/cprintf()将内容打印到'console'
-->env_run(curenv) 设置curenv的状态,调用env_pop_tf
-->env_pop_tf(&curenv->env_tf);将tf里的数据读到对应'寄存器'中,然后进入'user mode'
-->'iret'指令后进入user mode,也就是说回到umain()的cprintf("hello world")处继续往下执行
-->下面还有一个cprintf("i am environment %08x\n", thisenv->env_id);把上面sys_cputs()系统调用过程再来一遍
-->exit(){sys_env_destroy(0);}//参数是envid=0
//下面是lib/sys_env_destroy()系统调用过程
lib/sys_env_destroy()包含lib/syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0);
-->'int %T_SYSCALL'--> TRAPHANDLER_NOEC(syscall_handler, T_SYSCALL)
-->trap(tf)
-->trap_dispatch(tf);
tf->tf_trapno == T_SYSCALL所以调用kern/syscall()
-->kern/syscall()
syscallno==SYS_env_destroy所以调用kern/sys_env_destroy() //内核态下
-->sys_env_destroy()
-->envid2env(envid, &e, 1);根据envid从env_store中找到对应env的地址,并检查是否有效
//0 on success, -E_BAD_ENV on error.
//On success, sets *env_store to the environment.(即&e)
//On error, sets *env_store to NULL.
-->env_destroy(e);
-->env_free(e);
-->monitor(NULL)
-->while(1) monitor(NULL);
实验信息
在这个lab中,您将实现获得受保护的用户模式环境(“process”)运行。您将增强JOS内核,以设置 the data structures
来跟踪用户环境,创建单个 user environment
,load a program image
并启动运行。您还将使JOS内核能够处理用户环境发出的任何系统调用
并处理它引起的任何其他异常
。
在这个lab中,术语 environment
和 process
意思是一样的。
一旦JOS启动并运行,envs
指针指向表示系统中所有
environment的Env结构数组
在我们的设计中,JOS内核将支持最多NENV
个环境同时属于active
状态。
这种设计允许轻松地分配和释放环境,因为它们只需要添加到或从’ env_free_list '中删除即可。
内核使用curenv
符号来跟踪任何给定时间的当前执行环境
。在引导期间,在第一个环境运行之前,curenv最初被设置为NULL
在内核中 only one JOS environment active
,因此JOS只需要一个内核堆栈。
80386 Programmer’s Manual: Chapter 9 Exceptions and Interrupts(Personal Translation)
Part A: User Environments and Exception Handling
envs
结构体数组保存着所有
用户环境的状态信息env的分配
就是把env_free_list的表头
取出,然后初始化内存空间与状态信息,所有environment地址空间的内核部分都是一样的,至于内存空间的用户部分的加载就得从二进制映像文件(ELF格式)读取。不过不要忘记修改env_status
env的free
也很简单,就是把内存空间释放掉,然后env重新插回env_free_list的表头
- 中断分为可屏蔽中断(
INTR引脚
发出信号)与不可屏蔽中断(NMI引脚
发出信号)。异常可以是处理器发现(可细分为faults、traps and aborts
),也可以是可编程的(软件中断)。不可屏蔽中断与异常应该就是对应中断向量0-31
号。系统调用
对应0x30(48)号 - 当特权级从用户模式向内核模式转换时,内核不能使用用户的栈,因为它可能不是有效的。用户进程可能是恶意的或者包含了一些错误,使得用户的 %esp 指向一个不是用户内存的地方。在内陷发生的时候进行一个
栈切换
,栈切换的方法是让硬件从一个任务段描述符中读出新的栈选择符和一个新的 %esp 的值。通过相关函数把用户进程的内核栈顶地址存入任务段描述符中 - 在JOS中,各个环境不像xv6中的进程那样拥有自己的内核堆栈。一次只能有一个JOS环境活动在内核中,因此JOS只需要
一个内核堆栈
。 - 引发中断或者异常的进程都会有一个
Trapframe
,保存着中断或异常发生前的处理器状态
以便处理完后恢复继续执行。这个很重要,一定要明白tf
保存的是当前执行进程引发中断或者异常时的处理器状态信息(包括寄存器值以及中断向量号)。还得知道只是在内核栈
中。Trapframe的结构是这样的:
+--------------------+ //stack向下增长,留心“-”号. kernel stack
| 0x00000 | old SS | " - 4 <---- ESP
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20
iret将上面出栈----> +--------------------+ <---- 以上在陷入发生时由硬件完成
| err | " - 24
| trapno | " - 28
+--------------------+ <----以上由TRAPHANDLER宏完成
| ds | " - 32
| es | " - 36
(trapframe)old esp-> | regs | " - 不知道多少
| old esp |
+--------------------+ <----以上由_alltraps完成
| ret addr |
new ebp----> | old ebp | <----esp
+--------------------+ <----以上是call trap完成
- 从内核态进入用户态主要是env_pop_tf()里的
iret指令
之后。发生中断、异常、系统调用时会从用户态陷入内核态,主要是通过int指令
- 中断也可能
发生在内核模式
下。在那种情况下硬件不需要进行栈转换,也不需要保存栈指针或栈的段选择符;除此之外的别的步骤都和发生在用户模式下的中断一样。而 iret 会恢复了一个内核模式下的 %cs,处理器也会继续在内核模式下执行。 - 每一个中断/异常都对应一个
中断向量
,在IDT中又作为index对应一个中断向量描述符
,每个中断向量描述符又对应一个中断处理程序
的入口地址。如下:
- 最重要的还是最上面的调用流程。
- 主要函数:
//kern/env.c
void env_init(void); //初始化envs数组(所有用户环境的状态信息)。按顺序插入env_free_list,env[0]作为表头
void env_init_percpu(void); // Load GDT and segment descriptors.应该是初始化处理器状态
int env_alloc(struct Env **e, envid_t parent_id);//将e从env_free_list表头取出,设置好e的用户内存空间(页表)与其状态信息
void env_free(struct Env *e);//释放掉分配给e的所有物理内存,然后将e重新插入env_free_list的表头
static int env_setup_vm(struct Env *e);//初始化e的用户空间(即页目录表值)。内核部分直接等于kern_pgdir[i],用户部分都设0
static void region_alloc(struct Env *e, void *va, size_t len);//为env分配len字节的物理内存,并映射到env的内存地址空间的va处
static void load_icode(struct Env *e, uint8_t *binary); //从二进制映像文件(ELF格式)binary里加载env的program segment内容到env的内存空间
void env_create(uint8_t *binary, enum EnvType type); //包括env_alloc与load_icode
void env_destroy(struct Env *e); // 先env_free掉e然后进入monitor
int envid2env(envid_t envid, struct Env **env_store, bool checkperm);//根据envid找到对应的env地址,并检查是否有效
// The following two functions do not return
void env_run(struct Env *e) __attribute__((noreturn)); //令curenv=e,并更新好状态,然后调用env_pop_tf
void env_pop_tf(struct Trapframe *tf) __attribute__((noreturn)); //用tf设置好处理器的所有寄存器,为用户程序的执行配好环境,然后进入用户态开始执行第一条用户指令
Part B: Page Faults, Breakpoints Exceptions, and System Calls
- 有一个特别重要的地方,就是很多函数在
kern文件
跟lib文件
下都有,而且是非常不同的(比如lib中的syscall是用户态下调用系统调用,而kern中的syscall只是简单的输出之类的,不包括系统调用,因为本身就是内核态下),要注意到底调用的是哪个文件里的,可以通过原理去判断,实在不行就通过头文件
判断吧 - 当处理器收到一个
page fault
,它将导致错误的linear(或virtual) address
存在一个特别的处理器控制寄存器CR2
中在用户态发生页面错误可以陷入内核态去处理,处理完后会直接结束掉报错进程
,但是内核态
却不能
发生页面错误,否则会导致内核重启 - 断点异常(
T_BRKPT
==3)常被用于允许debuggers(调试器)插入一个断点在程序代码中,通过用特别1-byte int3
软件中断指令临时取代相关程序指令。 User processes
(用户进程)通过调用system calls
要求内核为它们做事。当用户进程调用一个系统调用,处理器会进入kernel mode
,处理器和内核合作去报错用户进程的状态。JOS中T_SYSCALL
是48(0x30)。Application(应用程序)会在寄存器中传递系统调用号和系统调用参数
。- A user program开始运行在lib/entry.S的顶部。
Memory protection
是操作系统一个至关重要的特性,确保一个程序的bugs不会破坏其他程序或者破坏操作系统自身。操作系统经常依赖于hardware support
(硬件支持)去实现内存保护。当一个程序试着去access(访问)一个无效地址或者一个它没有权限访问的地址的时候,处理器会停止这个程序在这条导致fault的指令
上,然后带着这尝试操作的信息陷入内核。- 当一个程序向内核传一个指针的时候,内核会检查这个
地址是否在
地址空间的用户部分
,并且页表是否允许
这个内存操作。 - 特权级别是个很重要的点。每个IDT的entries内的中断描述符都为中断处理程序设定了一个
DPL(Descriptor Privilege Level)
。用户程序
的特权级别是3
,内核
的特权级别是0
(可知0级别更高)。如果用户产生的中断/异常需要级别0,那么用户就无权请内核调用这个处理程序,就会产生一个general protection fault
,如果是内核发生中断/异常的话,特权级别总是够的 - 可以通过tf->cs的低两位去判断陷入进来的程序是用户程序还是内核。
tf->cs & 3 == 0 或 3