启用分页机制
1.为什么要分页
内存碎片,导致利用率不高。
段是连续的,不好换进换出
2.一级页表
(1)分页机制原理
经过段部件处理后,保护模式的寻址空间是4GB,此寻址空间指的是线性地址空间,他在逻辑上是连续的。分页机制的思想是:通过映射,可以使连续的线性地址与任意物理内存地址相关联,逻辑上连续的线性地址对应的物理地址不一定连续。
分页机制打开前要将页表地址加载到控制寄存器 cr3 中,这是启用分页机制的先决条件之一,所以,在打开分页机制前加载到寄存器 cr3 中的是页表的物理地址,页表中页表项的地址自然也是物理地址了
虽然内存分页机制的作用是将虚拟地址转换成物理地址,但其转换过程相当于在关闭分页机制下进行,过程中所涉及到的页表及页表项的寻址,它们的地址都被 CPU 当作最终的物理地址(本来也是物理地址)直接送上地址总线,不会被分页机制再次转换(否则会递归转换下去)。
分页机制作用主要有两方面。
1.将线性地址转换位物理地址
![在这里插入图片描述](https://img-blog.csdnimg.cn/0ec9cbf4a8eb4c02abff0894057649c2.png
2.将大小相等的页代替大小不等的段
我们所指的虚拟地址就是上面的线性地址。
在内存地址中,最简单的映射方法是逐字节映射,即一个线性地址对应一个物理地址。我们需要找个地方来存储这种映射关系, 这个地方就是页表( PageTable )。页表就是个 列的表格,页表中的每一行(只有一个单元格)称为页表项(PTE ),其大小是 字节,页表项的作用是存储内存物理地址。当访问一个线性地址时,实际上就是在访问页表项中所记录的物理内存地址
显然,这种映射并不划算,将 4GB间划分成 4G 个内存块,每个内存块大小是 宇节。页表也是存储在内存中的,为了表示 32 位地址,每
个页表项必须要 4字节,若按此方案,光是页表就要占 16GB 内存.
因此现在一般,页的大小为4KB。也就是说有1MB的内存块
得注意一下,页是地址空间的计量单位,并不是专属的物理地址或线性地址,只要是4KB的地址空间都可以叫做一页。
页表存储这个页的起始物理地址。
使用页表
页表地址根据CR3寄存器得出
一个页表项对应一个页,所以,用线性地址的高 20 位作为页表项的索引,每个页表项要占用4 字节
大小,所以这高 20 位的索引乘以 4后才是该页表项相对于页表物理地址的字节偏移量。
举个例子 0x00001234
这是我们的线性地址 32位对吧
我们一看就能得到 页表中对应的页表项是第0x00001个
而在页面的相对应的偏移位置是 0x234
那直接访问其地址可以通过 第0x10100*4字节
通过cr3中的页表地址 + (0x10100)乘上4 即可得到
3.二级页表
(1)衍生二级页表的原因
(a)一级页表中最多可容纳 IM ( 1048576 )个页表项,每个页表项是 字节,如果页表项全满的话,
便是 画面大小。
(b)一级页表中所有页表项必须要提前建好,原因是操作系统要占用 4GB 虚拟地址空间的高 IGB,
用户进程要占用低 3GB
(c)每个进程都有自己的页表,进程一多,光是页表占用的空间就很可观了
(2)结构
无论是几级页表,标准页的尺寸都是4KB 固定不变的,这一点是不变的。所以 4GB 线性地址空间最多有 1M标准页 一级页表是将这 1M 个标准页放置到 1张页表中,二级页表是将这 1M 个标准页平均放置 1K页表中,每个页表中包含有 1K 个页表项。这1k个页表存储再页目录表中。无论是页目录项还是页表项其大小都是4字节,因此页表和页目录表都是4KB。
(3)求实际物理地址
(a)用虚拟地址的高10位乘以4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和,便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
(b)用虚拟地址的中间10位乘以4,作为页表内的偏移地址,加上在第1步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
(c)虚拟地址的高10位和中间10位分别是PDE和PTE的索引值,所以它们需要乘以4。但低12位就不是索引值啦,其表示的范围是0~~Oxff,作为页内偏移最合适,所以虚拟地址的低12位加上第2步中得到的物理页地址,所得的和便是最终转换的物理地址。
(4)页表项和页目录项
其高二十位用来表示物理地址,其低12位用来表示属性
P->存在位,是否存在于物理内存
RW->读写位
US->表示用户权限
PWT->意为页级通写位,也称页级写透位 若为 表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存。
PCD->意为页级高速缓存 止位 若为 表示该页启用高速缓存
A->访问位。若为1表示该页被CPU访问过。
D->脏页位, CPU 个页面执行写操作时,就会设置对应页表项的 位为 此项仅针对页表项有效,并不会修改页目录项中的 位。
G->全局位,若为1表示该页为全局页,会将转化结果存入TLB中
其他位不太用得着,就不介绍了。
不过,真是奇妙呢。明明一个项(32位)正好可以存储一个地址,却分成20(物理地址)+12(属性),低十二位用虚拟地址的一部分表示,说时虚拟地址,却承载了一定的信息。物尽其用!!!
3.启用页表
(1)准备好页目录表及页表。
(2 )将页表地址写入控制寄存器 cr3
(3 )寄存器 cr0的 PG 位置1
cr3寄存器又称页目录基址寄存器
4.代码
%include "boot.inc"
section loader vstart=LOADER_BASE_ADDR
LOADER_STACK_TOP equ LOADER_BASE_ADDR
jmp loader_start
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
times 59 dq 0
times 5 db 0
total_mem_bytes dd 0 ; save memory capacity
; gdt's pointer
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
ards_buf times 244 db 0 ; buffer_size
ards_nr dw 0 ; buffer_num
SELECTOR_CODE equ (0x0001 << 3) + TI_GDT + RPL0
SELECTOR_DATA equ (0x0002 << 3) + TI_GDT + RPL0
SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
loader_start:
;int 15h eax=0000E820h,edx=534D4150h('SMAP'),获取内存布局
xor ebx,ebx ;第一次调用时,ebx为0
mov edx,0x534d4150 ;edx标志值
mov di,ards_buf ;ards结构缓冲区
.e820_mem_get_loop: ;执行int 0x15后,eax值变为上面edx的值
mov eax,0x0000e820
;所以每次执行int前都要更新为子功能号
mov ecx,20
int 0x15
jc .e820_failed_so_try_e801
;若cf位为1则有错误发生,尝试0xe801子功能
add di,cx ;使di增加20字节指向缓冲区中的新的ARDS结构位置
inc word [ards_nr] ;记录ARDS数量
cmp ebx,0 ;若ebx为0且cf不为1,这说明ards全部返回
jnz .e820_mem_get_loop
;在所有ards结构中
;找出(base_add_low + length_low)的最大值,即内存的容量
mov cx,[ards_nr]
;遍历每一个ARDS结构体,循环次数为ARDS的数目
mov ebx,ards_buf
xor edx,edx
.find_max_mem_area:
;无需判断type是否为1,最大内存块一定可被使用
mov eax,[ebx]
add eax,[ebx+8]
add ebx,20
cmp edx,eax
;冒泡排序,找出最大,edx寄存器始终是最大的内存容量
jge .next_ards
mov edx,eax
.next_ards:
loop .find_max_mem_area
jmp .mem_get_ok
;E801获取内存大小,最大支持4G
;返回后,ax cx 值一样,以KB为单位,bx dx值一样,以64kb为单位
;在ax和cx寄存器中为低16MB,在bx和dx寄存器中为16MB-4GB
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若当前e801方法失败,就尝试0x88方法
;1算出低15MB的内存
;ax和cx是以KB为单位的内存数量,将其转换为byte为单位
mov cx,0x400
mul cx
shl edx,16
and eax,0x000FFFF
or edx,eax
add edx,0x100000
mov esi,edx
;2 再将16MB以上的内存转换为byte为单位
;寄存器bx和dx中是以64KB为单位的内存数量
xor eax,eax
mov ax,bx
mov ecx,0x10000
mul ecx
add esi,eax
;由于此方法只能测出4GB以内的内存,故32位eax足够了
;edx肯定为0,只有eax便可
mov edx,esi
jmp .mem_get_ok
.e801_failed_so_try88:
;int 15后,ax存入的是KB为单位的内存容量
mov ah,0x88
int 0x15
jc .error_hlt
add eax,0x0000FFFF
;16位乘法,被乘数ax,积在32位。积的高16位在dx中,积的低16位在ax中
mov cx,0x400
mul cx
shl edx,16
or edx,eax
add edx,0x100000
.error_hlt:
hlt
.mem_get_ok:
mov [total_mem_bytes],edx
; 1 打开A20 gate
; 2 加载gdt
; 3 将cr0 的 pe位置1
in al,0x92 ;端口号0x92 中 第1位变成1即可
or al,0000_0010b
out 0x92,al
lgdt [gdt_ptr]
mov eax,cr0 ;cr0寄存器第0位设置位1
or eax,0x00000001
mov cr0,eax ;位置为0x0cbe
;-------------------------------- 已经打开保护模式 ---------------------------------------
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
mov byte [gs:800], 'P'
;-----------------------------启动分页--------------------------------------
;创建页目录及页表并初始化页内存位图
call setup_page
;将描述符表地址及偏移量写入内存gdt_ptr,一会用新地址重新加载
sgdt [gdt_ptr]
;将gdt描述符中视频段描述符中的段基址+0xc0000000
mov ebx,[gdt_ptr+2]
or dword [ebx+0x18+4],0xc0000000
;视频段是第三段描述符,每段描述符是8字节,故0x18
;段描述符的高四字节的最高位是段基址的31-24位
;将gdt的基址加上0xc0000000使其成为内核所在的高地址p
add dword[gdt_ptr+2],0xc0000000
add esp,0xc0000000 ;栈指针同样映射到内核地址
;将页目录地址赋给cr3
mov eax,PAGE_DIR_TABLE_POS
mov cr3,eax
;打开cr0的pg位
mov eax,cr0
or eax,0x80000000
mov cr0,eax
;开启分页后,用gdt的新地址重新加载
lgdt [gdt_ptr] ;重新
mov byte [gs:880],'V'
jmp $
;------------------------创建页目录及页表---------------------------------
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
;开始创建页目录项
.create_pde:
mov eax,PAGE_DIR_TABLE_POS
add eax,0x1000 ;eax为第一个页表·位置以及属性
mov ebx,eax ;为.vreate_pte做准备,ebx为基址
;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4M
;这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表
;这是为将地址映射为内核地址做准备
or eax,PG_US_U | PG_RW_W | PG_P
;页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
mov [PAGE_DIR_TABLE_POS + 0x0],eax ;第一个目录项
;将页目录表中的第一个目录项写入第一个页表位置及属性
mov [PAGE_DIR_TABLE_POS + 0xc00],eax ;0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间
;也就是页表的0xc0000000-0xffffffff共1G属于内核
sub eax,0x1000
mov [PAGE_DIR_TABLE_POS + 4092],eax
;使最后一个目录项指向页目录表自己的地址
;下面创建页表项
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 ;页目录项的属性
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