Lab3 - Exercise1~3

引用说明:代码来自hyuuko这位大佬的:
https://www.cnblogs.com/zsmumu/p/12729463.html

知识回顾

我们运行JOS的的系统是基于x86结构的,所以寻址需要经过段翻译和页翻译。

段翻译主要是根据段选择子去匹配全局描述符表中的段描述符,根据段描述符可以得到段翻译后的地址,称为线性地址或者虚拟地址。段选择子来自代码段寄存器,例如CS寄存器。

段描述符主要用于记录段的各种信息(包括权限,地址等)

在Lab 1 的boot.S中,我们建立了一个有3个段描述符的全局描述符表:


/*宏SEG介绍:SEG代表一个64位长的段描述符
	SEG(type, base, lim, dpl)
	type:访问权限(可执行STA_X,可读STA_R,可写STA_W)
	base:基地址,在JOS中所有段描述符的基地址都定义为0
	lim:长度,都定义为0xFFFFFFFF
	dpl : DESCRIPTOR PRIVILEGE LEVEL(描述符权限级别),在*nix系统中分为0-3(越低权限越高),0为kernel态,1-3为用户态。在JOS中定义了0(kernel态)和3(user态)(Linux也是这样定义的)。
*/
gdt:
  SEG_NULL				# null seg
  SEG(STA_X|STA_R, 0x0, 0xffffffff)	# code seg
  SEG(STA_W, 0x0, 0xffffffff)	        # data seg

第一个段为空,是CPU的要求。接下来是代码段,然后是数据段。这两个段的段基址都是0,界限都是4G。可以发现这两段是重叠的,唯一不同的是权限,

cpu在切换环境的时候,CS等寄存器的值也会变化,导致环境切换之后段选择子发生变化,匹配到的段描述符也不同,不同的段描述符权限也不同,从而实现权限控制(我猜的)

这里这个gdt的作用主要是将逻辑地址转换成虚拟地址,但是值不变。从而内核可以运行C代码。

在Lab 3 中我们新增了“用户环境”这个概念,gdt也需要进行更改:

	struct Segdesc gdt[] =
	{
		// 0x0 - unused (always faults -- for trapping NULL far pointers) 下标为0的数据设为NULL
		SEG_NULL,

		// 0x8 - kernel code segment 二进制1000 >> 3 = 1 设置下标为0x8的段描述符
		[GD_KT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 0),

		// 0x10 - kernel data segment 二进制10000 >> 3 = 10 十进制为2
		//下标为2的gdt[2]设置为SEG(...)
		[GD_KD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 0),

		// 0x18 - user code segment   二进制11000 >> 3 = 11 十进制为3
		[GD_UT >> 3] = SEG(STA_X | STA_R, 0x0, 0xffffffff, 3),

		// 0x20 - user data segment   二进制100000 >> 3 = 100 十进制为4
		[GD_UD >> 3] = SEG(STA_W, 0x0, 0xffffffff, 3),

		// 0x28 - tss, initialized in trap_init_percpu() 二进制101000 >> 3 = 101 十进制为5
		[GD_TSS0 >> 3] = SEG_NULL
	};

可以看到增加了三个段描述符,用户代码段和用户数据段,还有一个不知道是干什么的。

Exercise 1

修改 kern/mmap.c 中的 mem_init() 来分配和映射envs数组。

注意:分配是指 envs 数组和 pages 建立关系,映射是指与 envs 关联的 pages 与 虚拟地址建立关系。
修改kern/pmap.c中的mem_init():

	// Make 'envs' point to an array of size 'NENV' of 'struct Env'.
	// LAB 3: Your code here.
	//为envs分配空间
	envs = (struct Env*)boot_alloc(NENV * sizeof(struct Env));
	memset(envs,0,NENV * sizeof(struct Env));//初始化envs指向的值为0
	
	............省略.............
	
	// Map the 'envs' array read-only by the user at linear address UENVS
	// LAB 3: Your code here.
	//将 ‘envs’ 数组物理地址映射到线性地址UENVS,PTE_U表示用户态软件可读物理内存页内容
	boot_map_region(kern_pgdir, UENVS, NENV * sizeof(struct Env), PADDR(envs), PTE_U);

运行,会出现check_kern_pgdir() succeeded!
check_kern_pgdir函数的作用是检查虚拟地址空间的内核部分是否正确地设置了。

Exercise 2

下面是系统启动之后,到调用用户代码为止的代码调用图
image

env_init()调用了env_init_percpu( )
env_create()先后调用了env_alloc()和load_icode()
env_alloc()调用了env_setup_vm()
load_icode()调用了region_alloc()

意味着我们首先创建了一个env环境,并将某个elf文件装载到该环境。然后我们再运行这个环境。
这个环境实际上就是用户环境
这就是Exercise 2的含义,我们接下来要做的就是实现这些函数。

完成以下/kern/env.c中的几个函数确保系统正常运行

  • env_init()
    初始化envs数组所有env的env_id和env_link
    将所有env设置为空闲环境
    在调用的env_init_percpu()中重新加载了新的gdt,并设置了各个段选择子

  • env_setup_vm()
    负责创建进程自己的页目录,并初始化内核地址空间。它不需要为内核地址空间另外创建页表,只要先将内核页目录kern_pgdir的所有目录项复制过来即可,以后再设置用户地址空间

  • region_alloc()
    为len长度的字节分配物理内存,并且将分配得到的物理内存与虚拟地址va建立映射关系(通过e->env_pgdir)

  • load_icode()
    load_icode比较繁琐。由于我们还没有实现文件系统,甚至连磁盘都没有,所以当然不可能从磁盘上加载一个用户程序到内存中。因此,我们暂时将ELF可执行文件嵌入内核,并从内存中加载这样的ELF文件,以此模拟从磁盘加载用户程序的过程,这个函数的作用就是将ELF可执行文件嵌入内核。
    ELF文件加载可参考:https://blog.csdn.net/yangguoyu8023/article/details/101429305
    主要流程:

    1. 根据ELF文件获取需要加载的段
    2. 为这些段分配物理内存,与虚拟内存建立映射关系
    3. 将当前环境的eip寄存器数据设置为ELF文件的入口地址
  • env_create()
    创建一个新的env环境,为env环境分配物理内存,并在该环境中执行程序(elf文件)

  • env_run()
    给定一个环境,并运行该环境
    步骤:

    1. 更改旧环境(如果存在)和新环境的运行状态
    2. e->env_runs++ (环境已运行次数)
    3. 切换页目录
      4.恢复新环境的寄存器

实现代码:env.c
虽然是抄的,但是我还是理解并写了一点注释的哈哈

Exercise 2 完成之后,编译并运行内核,如果代码没错,你的系统将会进入用户空间,并且执行 user/hello.c 二进制文件,一直执行到 int 系统调用指令,然后报错。报错的原因是我们在用户环境下执行了内核指令,但是我们没有设置硬件使得在用户环境下可以访问内核。

当cpu发现还没有设置好硬件去处理这个系统调用指令,cpu会生成一个常规的具有保护性质的异常,然后cpu发现自己处理不了这个异常,会生成一个 double fault exception,然后cpu发现还是处理不了,最后放弃处理了,挂掉了,这就是所谓的三重故障。一般来讲,来到这个地步,我们可以看到cpu重置,系统重启。

Exercise 2运行实测:
修改env.c之后
在lab 目录下 运行命令 make qemu,得到结果:
image

可以看到如预测一样发生了 Triple fault。
同时CS的值为 001b = 0001 1011,指向GDT中的第三个描述符(user code segment),权限为3(用户态)。
CS的值一开始应该为 0x8 = 00001 000,指向GDT第一个段,内核段。在执行env_pop_tf()中的 iret 命令之后变成了001b。
可以通过在env_pop_tf()打断点,查看iret执行前后寄存器CS的值来验证。
这位大神做了:
https://www.cnblogs.com/oasisyang/p/15520180.html

posted @ 2022-04-15 15:43  Pril  阅读(55)  评论(0)    收藏  举报