写在前面:自制操作系统Gos 第二章第七篇:主要内容是如何获取开启x86体系下的分页机制
相关页表的概念在程序地址转换这篇博客中已经详细介绍过了,本篇文章将不会重复介绍
Gos完整代码:Github
页目录项和页表项
想要启动分页机制,我们首先要谈的就是存储物理页地址的页目录项和页表项了。其都是4字节,这32位被人为的规定成为如下表示:
(守约可以说的上是操作系统设计的精髓了)
可以明显的看出来页表项和页目录项的差别其实就在于第7位,其也叫PAT位,它指定了这个32位的空间是页表的物理地址还是物理页的地址。
除此之外,其他位也被赋予了不同的含义:
- P:存在位,其表示该页是否存在于物理内存
- RW:读写位,表示此页是否可读写
- US:权限位,如果是1表示这个进程归属于User级,特权级
0~3
均可访问;如果是0表示Supervisor级,特权级0~2
可以访问 - PWT:页级写透位,标识这个不仅是普通内存,还是高速缓存,可以用于改善读写速率,Gos用不上这个
- PCD:页级高速缓存禁止位,如果是1标识该页启用高速缓存,为0标识禁止将该页缓存
- A:访问位,若为1标识该页被CPU访问过了,这个是由CPU设置的
- D:脏页位,也是CPU去设置,当CPU对一个页面执行写操作时,就会设置对应页表项的D位为1
- G:全局位,为了提高获取物理地址的速度,将虚拟地址于物理地址转换结果存储在TLB中。G为1标识该页为全局页,会一直被保存在TLB表中。
- AVL:可用位,标识操作系统、软件是否可用该页
- 地址32~12位:这里只有20位,这是因为页表其实就是4KB大小了。
开启分页机制
现在我们已经掌握了页目录表和页表的的创建规则了。但是,页目录表其本身跟描述符表是一样的,是个内存中的数据结构。CPU想要访问它就必须有个专门的寄存器来存储其结构,这就是控制寄存器cr0~cr7
。其中cr3寄存器便存储了页目录表的起始物理地址,所以其又被称为页目录基址寄存器PDBR。其结构如下:
而我们要开启分页机制,其实就是将页目录表的物理地址的高20位写入cr3寄存器就可以了。
而打开分页机制的开关其实我们早就接触过了,其就是cr0寄存器的PG
位,将其置为 1 就可以了。
总结一下如何开启分页机制:
- 准备好页目录表以及页表
- 将页目录表的地址写入控制寄存器cr3
- 寄存器 cr0的 PG位置为1
设计页表
在开启分页机制之前,其实我们还有一件事情没有做,那就是规划页表啦!
页表的基本思想就是共享,共享于操作系统和用户进程之间,这就要求我们将其置于内核的1G空间中。话不多说,我们上代码:
首先我们要做的第一件事情就是将页目录表中的内容清空。为什么呢?你想想你每次开机之后,系统都没有运行进程是吧。这其实就是在开机自启动的时候创建了一个空的页目录表PDE。
;先把页目录占用的空间逐字节清0
;这里会循环4096=4KB次,将每个字节都清空
;PAGE_DIR_TABLE_POS指的是页目录表的基址
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
之后呢,我们要做的事情就是创建页目录表PDE啦,这个页目录项我们的目标是按照Linux那样分布,也就是3G空间属于自己,1G空间属于内核。按照每个页目录项表示4MB的空间,那么其实内核和用户进程的分界线就是第768项。
而我们这里设计页目录表和页表紧邻,规定页目录表的起始位置是0x100000,那么页表的起始位置就是0x101000。那么我们要做的第一件事情就是把0x101000写入页目录表的第一项和第768项。之后使页目录表的最后一项指向页目录表的地址,这样使方便我们拿到页目录表的地址。
注:页目录项每个元素占4字节,那么整个页目录表就是占4*1024=4096字节,转换为16进制就是0x1000
这样之后,页目录表中的内容如下:
;开始创建页目录项(PDE)
.create_pde: ; 创建Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性,第0项要指向实际物理内存的基址位置
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)及属性(3)
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 ; 使最后一个目录项指向页目录表自己的地址
之后,我们要做的事情就是填充一张页表的256项中的内容了。这里为什么是256项呢,其实我们主要就是因为我们的内核放在哪里这个问题。我们这里的策略就是将内核放在低端1M内存中。所以我们要填充这256项,至于其他的页表项,那当然是由用户进程来填充啦。
;下面创建页表项(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
这样之后,我们的内存布局又变了。主要是页表中的会变成如下内容:
好了,现在我们的内核程序有了地方存放了。但是我们还有一件事情没有做哦!
那就是初始化我们内核要用到的内存空间,也就是页目录表的769~1024
项,这部分我们需要将其的P、RW以及U三位都置为1并且给他们分配页表。这个页表的分配范围便是从第二张页表之后总共256个页表,每个页表可用表示4M的空间,总共就是1G内核空间啦。
;创建内核其它页表的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
这样之后呢,我们的内存中又出现变化啦,现在的内存布局如下:
写入cr3
现在我们的页表已经设计好了,那么其实我们就可以进入第二步了。非常简单:
; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax
开启分页
这一步也简单的不得了,就是把cr0寄存器的pg位置为1就可以了。
; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
至此,我们便进入到了分页机制了。完整的代码在loader.S文件中,之后的博客中我会逐步解析全部的loader.S
参考文献
[1] 操作系统真相还原