为了计算机安全,用户进程必须运行在低特权级,当用户进程需要访问硬件相关的资源时,需要向操作系统申请,由操作系统去做,之后将结果返回给用户进程。进程可以有无限多个,而操作系统只有一个,所以操作系统必须共享给所有用户进程。
也就是,用户的代码加上所需要的操作系统中的部分代码才算完整的程序。
用户进程要共享操作系统,如何共享呢?只要操作系统属于用户进程的虚拟地址空间即可。
我们学习 Linux 操作系统,在用户进程 4GB 虚拟地址空间中。3GB-4GB
划分给操作系统;0-3GB
是用户进程自己的虚拟空间。因此,为了实现操作系统,让所有用户进程的 3GB-4GB
的虚拟地址空间都指向同一个操作系统,指向同一片物理页地址,这片物理页地址是操作系统的实体代码。
一、分页机制
分页机制有页目录表,页目录表中是页目录项,其中记录的是页表的物理地址以及相关属性。
我们页目录表的位置,放在物理地址 0x100000
处,刚好在物理地址 1MB 处。

因为我们的 mbr、loader、操作系统内核是加载到物理地址 1MB 以下空间。后面我们会把虚拟地址的 3GB-4GB
(从虚拟地址 0xc000_0000 之上的 1MB 地址)映射到物理内存 1MB 之内。
1. 准备好页目录和页表
1 | setup_page: |
我们会在页目录项的第 0 项和第 768 项都指向页表地址 0x101000
。为什么呢?
- 为什么放在第 0 项,原因是我们在加载内核之前,程序中运行的一直是 loader,它本身的代码都是在 1MB 之内,必须保证之前段机制下的线性地址和分页后的虚拟地址对应的物理地址一致。第 0 个页目录项代表的页表,其表示的空间是
0 - 0x3fffff
,(4GB被页目录表分成了 1024 份,一份 4M 大小)包括了 1MB(0-0xfffff
)。所以用了第 0 项来保证 loader 在分页机制下依然运行正确。 - 为什么放在第 768 项,我们会把操作系统内核放在低端 1M 物理内存空间,但操作系统的虚拟地址是
0xc000_0000
以上,该虚拟地址对应的页目录项是第 768 个。因为0xc000_0000
的高 10 位是0x300
,即十进制的 768。这样虚拟地址0xc000_0000 - 0xc03f_ffff
之间的内存都指向的是低端 4MB 之内的物理地址,这自然包括操作系统所占的低端 1MB 物理内存。从而实现了操作系统高 3GB 以上的虚拟地址对应了低端 1MB。
接下来,我们在页目录的最后一个页目录项中写入页目录表自己的物理地址(0x100007
前 20 位是物理地址,后 12 位是属性)。注意此页目录表的权限是 PG_US_U、PG_RW_W
可读可写、User 级别(任意级别特权的程序都可以访问)。也就是说我们用户代码暂时是可以更改页目录表的。
接下来,填充第一个页表,因为我们目前只用到了第一个 1MB 空间(操作系统内核所处位置),所以我们只为这 1MB 空间对应的页表项赋值。一个页表 1024 项,一项可以对应一个物理页 4K,因此一个页表对应 4M 的物理空间。因此填充第一个页表,相当于映射了 4M 的物理空间。
接下来,我们需要填充内核空间(虚拟地址 3G-4G,页目录项:[768, 1022]
,页目录 1023 项已经分配了)。本来我们内核只在 1M 空间内,一个页表可以映射的空间就够了?但是还是将所有的 256 个页表都进行填充,为什么呢?
假设某个进程陷入内核时,内核为了某些需求为内核空间新增页表(通过是神奇大量内存),因此还需要把心内核页表同步到其他进程的页表中,否则内核无法被完全共享。所以实现内核完全共享最简单的办法是提前把内核的所有页目录项定下来,也就是提前把内核的页表固定下来。
因此,我们总结下我们创建的页表。
- 页目录表在 1M 位置,占用 4KB
- 页表从
0x101000
位置开始,一共有 1024 个页表,每个页表 4KB。所有页表占 4MB - 第 0 个页目录表项和第 768 个页目录表项指向第 0 个页表
- 页目录表项
[769, 1022]
指向[1, 254]
页表(页目录表和页表从 0 开始) - 第 1023 个页目录表项指向地址 1M 处,也就是页目录表自身。
- 第 0 个页表中的 1024 个页表项,指向物理地址:
[0, 4M]
处。也就是内核加载的位置。但是目前只填充了 256 个页表项,也就是映射了[0, 1M]
物理地址,其他页表项还没有映射
二、进入分页模式
1 | ; 创建页目录及页表并初始化页内存位图 |
我们在页表中会将内核放置到 3GB 以上的地址,我们也把 GDT 放在内核的地址空间。
我们先使用 sgdt 指令将 GDT 表加载到原来的内存位置。然后修改视频段(显存段)的段描述符的段基址,因为将来内核运行在 3GB 以上,打印功能将来也是在内核中实现,不能让用户进程直接控制显存。因此显存段的段基址要改为 3GB 以上。
然后我们将 GDT 的基址移到内核空间,同时栈指针也映射到内核地址。最后打开分页,重新加载 GDT。