MIT6.828——Lab2内存管理准备知识

保护模式内存管理机制

MIT6.828——Lab1 PartA

MIT6.828——Lab1 PartB

分段机制的问题

​ 分段的主要问题,出现在内存不足或者内存碎片过多的情况下。对于一个程序而言,例如其代码段长度就和其代码的长度直接相关,各个段的大小是不固定的,不能拆分的,要装入内存便一次性将一个整段都装入,因此在内存紧张时,就会出现问题。可以想象到的解决方法有这样一种:将段换出到磁盘上,从而空出一部分的物理内存空间。但是同样的,如果段长度过长,内存过小,在频繁的换入换出也无济于事。

​ 问题的本质在于,分段机制下产生的连续线性地址,被认为在物理内存上也是连续的,线性地址就是物理地址。但是我们可用的物理地址并不是连续的,因此就会产生冲突。所以解决这个问题的关键在于,是否可以:线性地址连续,对应的物理地址不连续?为了解除这种一一映射的关系,便可以通过地址映射。

​ 即 线性地址(虚拟地址)——页表——> 物理地址

​ 为了效率的问题,这种映射关系写在页表里,页表在内存中,查表的工作由硬件完成。

分页机制的思想:通过映射,可以使得连续的线性地址与任意的物理地址相关联,逻辑上连续的线性地址对应的物理地址可以不连续。

分段机制的作用:将线性地址转化为物理地址;用大小相等(4KB)的页代替大小不等的段。

页表结构

一级页表

首先针对一级页表而言,寻址过程可以由下图表示

可以看到,分页机制仍旧是基于分段基础上的。将分段形成的线性地址,进行划分,利用高20位作为在页表内寻址的偏移量,寻址页表项,其中页表项的基址(物理地址)放在CR3寄存器中。再利用低12位,结合页表项给出的基址,合成物理地址,送上地址总线,即可寻址物理内存单元。可以看到,因为划分了低12位为页内偏移,因此页表的大小也就是4KB,这是一个常用的页大小值。而高20位,则说明,页表中含1M项页表项,占内存位4MB。

二级页表

​ 在一级页表的铺垫下,便有了另一个问题。页表的大小为4M,且必须提前建立好,每个进程都有自己的页表,如果进程数很多,页表的内存开销便很可观,因此是否可以动态的创建页表项呢?解决这个问题的答案就是二级页表。二级页表的思想是,将1M页平均放到1K个页表中,每个页表1K个页表项,占据内存位4KB,刚好为一页的大小。为了存储这些页表,引入了页目录。每个页表的物理地址,都在页目录中以页目录项的形式存储。因为最多1K页表,因此页目录大小也为4KB,一页的大小。

​ 二级页表下的寻址过程如下:

​ 现在需要了解一下,页表项和页目录项的详细信息了。这部分信息,可以在Intel系统开发手册上得到详细说明。

这里直接截取部分说明了:

开启分页机制

开启分页机制,需要做三件事:

  • 准备好页表和页目录
  • 将页目录的物理地址写入CR3
  • 寄存器CR0的PG位置1

编程实例

在之前的mit6.828实验1中,已经看到了一个比较基本的实例,如何进入保护模式,并进行分页操作。为了对于lab2有一个更好的理解,这里截取一部分《操作系统真象还原》的代码进行解释说明。这里可以和lab1部分结合来看

建立GDT进入保护模式

GDT_BASE:   dd    0x00000000 
			dd    0x00000000

CODE_DESC:  dd    0x0000FFFF 
			dd    DESC_CODE_HIGH4

DATA_STACK_DESC:dd    0x0000FFFF
				dd    DESC_DATA_HIGH4

VIDEO_DESC: dd    0x80000007
			dd    DESC_VIDEO_HIGH4  

GDT_SIZE    equ   $ - GDT_BASE
GDT_LIMIT   equ   GDT_SIZE -1

gdt_ptr  dw  GDT_LIMIT 
		 dd  GDT_BASE
		 
;-----------------  打开A20  ----------------
in al,0x92
or al,0000_0010B
out 0x92,al
;-----------------  加载GDT  ----------------
lgdt [gdt_ptr]
;-----------------  cr0第0位置1  ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax

jmp dword SELECTOR_CODE:p_mode_start	     ; 刷新流水线
[bits 32]
p_mode_start:
   mov ax, SELECTOR_DATA
   mov ds, ax
   mov es, ax
   mov ss, ax
   mov esp,LOADER_STACK_TOP
   mov ax, SELECTOR_VIDEO
   mov gs, ax

按约定,GDT的第一个段描述符为空,这里建立了三个段,都是按照段描述符的规格进行性质填充,对照相关位的含义既可以知道段的信息。为了装载GDT,使用命令lgdt即可。进入保护模式后,寻址就需要使用段选择子,这在之前的lab1中也说到了。

开启分页

首先规划内存的整体布局,可以先画出下面这张图

人为规定的,将页目录放在了物理地址0x100000处,将第一个页表,放在了物理地址0x101000处。同时划分进程的虚拟地址空间位高端1GB内核空间和低端3GB用户控件。首先需要注意的是,在虚拟内存空间中,将高端1GB完全分给了内核。这对于每个进程都是一样的,为了实现所有进程的内核共享,这部分空间固定占据了页目录项的第0xc00项至第1023项。对于页目录而言,第0项存储了第0个页表的位置,最后一项存储了页目录自身在物理内存中的位置。

值得注意的是,页表0和页表c00都映射到了物理内存的低端1MB。这么做的原因是,在内核加载到内存空间之前,运行的一直是loader程序,它运行在低端1MB。为了保证之前段机制下的地址和现在分页后的地址一致,内核的前1MB也需要映射到物理内存低端1MB空间。低端的1MB=256*4KB,因此占据了256页,需要256个页表项。

在这部分说明之后,下面便是具体的实现:

setup_page:
;先把页目录占用的空间逐字节清0
   mov ecx, 4096
   mov esi, 0
.clear_page_dir:
   mov byte [PAGE_DIR_TABLE_POS + esi], 0
   inc esi
   loop .clear_page_dir

;开始创建页目录项(PDE)
.create_pde:				     ; 创建Page Directory Entry
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x1000 			     ; 此时eax为第一个页表的位置及属性
   mov ebx, eax				     ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

;   下面将页目录项0和0xc00都存为第一个页表的地址,
;   一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
;   这是为将地址映射为内核地址做准备
   or eax, PG_US_U | PG_RW_W | PG_P	         ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
   mov [PAGE_DIR_TABLE_POS + 0x0], eax       ; 第1个目录项,在页目录表中的第1个目录项写入第一个页表的位置(0x101000)及属性
   mov [PAGE_DIR_TABLE_POS + 0xc00], eax     ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
					     				     ; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
   sub eax, 0x1000
   mov [PAGE_DIR_TABLE_POS + 4092], eax	     ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
   mov ecx, 256				                 ; 1M低端内存 / 每页大小4k = 256
   mov esi, 0
   mov edx, PG_US_U | PG_RW_W | PG_P	     ; 属性为7,US=1,RW=1,P=1
.create_pte:				                 ; 创建Page Table Entry
   mov [ebx+esi*4],edx			             ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址 
   add edx,4096
   inc esi
   loop .create_pte

;创建内核其它页表的PDE
   mov eax, PAGE_DIR_TABLE_POS
   add eax, 0x2000 		     ; 此时eax为第二个页表的位置
   or eax, PG_US_U | PG_RW_W | PG_P  ; 页目录项的属性RW和P位为1,US为0
   mov ebx, PAGE_DIR_TABLE_POS
   mov ecx, 254			     ; 范围为第769~1022的所有目录项数量
   mov esi, 769
.create_kernel_pde:
   mov [ebx+esi*4], eax
   inc esi
   add eax, 0x1000
   loop .create_kernel_pde
   ret

在建立好了虚拟内存的布局之后,就可以正式开启分页机制

call setup_page

;要将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
sgdt [gdt_ptr]	      ; 存储到原来gdt所有的位置

;将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx, [gdt_ptr + 2]  
or dword [ebx + 0x18 + 4], 0xc0000000      ;视频段是第3个段描述符,每个描述符是8字节,故0x18。
;段描述符的高4字节的最高位是段基址的31~24位

;将gdt的基址加上0xc0000000使其成为内核所在的高地址
add dword [gdt_ptr + 2], 0xc0000000

add esp, 0xc0000000        ; 将栈指针同样映射到内核地址

; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax

;在开启分页后,用gdt新的地址重新加载
lgdt [gdt_ptr]             ; 重新加载
posted @ 2021-10-18 19:31  OasisYang  阅读(433)  评论(0编辑  收藏  举报