接上一篇 GDT 与保护模式,这一篇将是 loader 的重点。首先咱们须要创建 kernel 空间的虚拟内存。若是你对虚拟内存的原理还不熟悉,请务必先自学,这里能够提供一个文档供参考。html
到目前为止咱们始终在物理内存上操做,确切地说是在 1MB
的低地址空间内操做,这一切都很简单直接。可是接下来 loader 即将为加载 kernel 作准备,咱们须要在更广阔的 4GB
虚拟内存空间上规划数据和代码。java
仿照 Linux 系统,咱们将使用 3GB
以上的高地址空间做为内核空间来开展后续全部工做。例如最基本的,目前的物理低地址 1MB 会被映射到 virtual 地址 0 ~ 1MB
以及 3GB 以上空间 0xC0000000 ~ (0xC0000000 + 1MB)
处:git
进入 kernel 之后,对低 1MB 空间的访问将会使用 0xC0000000 ~ (0xC0000000 + 1MB)
虚拟地址,这里主要包括当前使用的 stack,以及显示器对应的内存映射:web
因此 video 内存基地址将从 virtual 地址 0xC00B8000
开始,不过目前没必要深究,后续将会在显示与打印一篇中详解。shell
除了最基本的低 1MB 内存空间,loader 还须要进一步在 0xC0000000
以上的 virtual 空间中开疆拓土,这主要包括两部分:segmentfault
page directory
)和页表(page table
);下面给出整个 loader 阶段将要搭建的 virtual-to-physical
内存映射关系图:多线程
这张图是本篇最重要的全局图,其中第二行是第一行通过“扭曲”比例的图示,咱们将 3GB 如下的用户空间缩小显示,当前重点只关注 3GB 以上的内核空间(粗框部分)。因为是 virtual 地址空间,咱们的空间划分能够比较随意和“奢侈”,咱们以 4MB 为单位,从 0xC0000000
开始在 virtual 空间切割划分出如下几个区域:框架
page tables
;0xC0800000
开始,做为加载、存放 kernel
代码和数据的空间,也就是说 kernel
从该处开始编址;这里要说一句,实现一个 OS 并无固定的方式,以上只是我我的的实现方式。实际上对于内存的规划是很灵活的,就像这个项目的名字 scroll
同样,内存就是一幅画卷,CPU 则是画笔,在遵循必定规则的前提下,能够作自由发挥。ide
下面咱们首先开始橙色部分,即内核 page directory
和 page tables
的创建。函数
在开始这一段以前,咱们仍是回顾一下页目录(page directory
)和页表(page table
)的相关原理。
有一些关键数字须要记住:
page
)的大小为 4096;pde (page directory entry)
和页表项 pte (page table entry)
,本质上是同样的结构,大小为 4 bytes
;page direcotry
一共有 1024 项,指向总共 1024 张 page table
,一共 4MB
;page table
都有 1024 项,指向 1024 张 pages
,管理着 1024 * 4KB = 4MB
的 virtual 空间;pde
管理着 4MB
的 virtual 空间;好了,下面咱们开始创建 kernel 空间的页表。按照惯例给出代码连接:这一部分相关的代码从函数 setup_page 开始,供你参考。
从这里开始如下,按照术语惯例,virtual 页我将用 page
表述,而 physical 页将用 frame
来表述。
首先咱们须要拿出一个 frame
,用来做为 page directory
。回到 physical 内存分布的那张图,目前 1MB 如下的部分已被占用,咱们可使用的部分就从 1MB 即 0x100000
开始。
我选择的是 0x100000 + 4KB
,即 0x100000
后的第 2 个 frame 做为 page directory
,固然这彻底是我的选择;0x100000
后的第 1 个 frame 我选择将它做为第一个 page table
:
再次强调,这是个人我的选择;frame 的选择是很是自由的,只要是还没被占用的均可以使用,固然了你要记住本身用过了哪些 frames,合理紧凑而且尽可能“美观”地规划使用。
值得注意的是,第 0 和第 768 个 pde
都指向了同一个 page table
,这个 page table 咱们将用它映射 0 ~ 1MB
低内存,即咱们目前所处的 1MB 内存空间。固然这个 page table 能够管理 4MB 的空间,咱们只映射了其中的 1MB,剩余 3MB 的 virtual 空间就闲置了,不过这没有关系,闲置就闲置,反正这是 virtual 空间。
下图展现了低 1MB 内存在页表中被映射的方式:
pde[0]
管理的是 virtual 空间最低的 4MB,其中的起始 1MB,被映射到了 physical 的低 1MB 上,这是一一对应的映射,virtual 地址彻底等于 physical 地址,这样在打开 paging 以后,咱们对 1MB 低内存的访问变为使用 virtual 地址,和以前的 physical 地址访问同样,不会感知到任何变化。
pde[768]
管理的是 0xC0000000
即 3GB 开始的第一个 4MB 空间,回到本篇开始的第一张图,其起始的 1MB 也被映射到低 1MB 内存上。在打开 paging 并进入 kernel 后,咱们将使用 0xC0000000 ~0xC0000000 + 1MB
的空间访问低 1MB 内存:
这里是本节的重点和难点。咱们知道 page directory
和 page tables
所指向的都是 physical 页,而一旦打开了 paging 模式,咱们之后全部对内存的访问将所有经过 virtual 地址,没法再直接操做 physical 地址。那么问题来了,咱们如何访问并修改 page directory
和 page tables
自己呢?
一种方法固然是在须要时关闭 paging,直接访问 physical 地址,以前推荐的教程 JamesM's kernel development tutorials 在不少地方都是这么作的,不过这并非一种好的作法,缘由有如下几点:
一个更合理的作法是,咱们将 page directory
和 page tables
自己也映射到 virtual 空间,这样就能够像访问其余正常内存同样访问它们。从本质上说 page directory
和 page tables
无非也是一些 page,彻底能够和其它内存访问一视同仁。问题就是,应该如何创建这种映射?来看下图:
咱们将 pde[769]
指向了 page directory
这个 frame 自己。这样 page direcotry
实际上同时也充当了一个 page table
,它所管理的正好是 1024 张 page tables 自己,一共 4MB。这 1024 张 page tables,其中有一张就是 page direcotry
它本身。
是否是有点绕?换言之,因为 pde[769]
指向了 page directory
它本身,所以 0xC0400000 ~ 0xC0800000
这 4MB 的 virtual 空间,如今被映射到了 1024 张 page tables
上,并且更好的是,它们的 virtual 地址是彻底连续地,紧密地排布在这 4MB 空间里。
由此,上面的问题已经解决,page tables 对应的 virtual 地址空间为:
0xC0400000 ~ 0xC0800000
这是 4GB 空间中第 769
个 4MB 空间 (总共 1024 个 4MB 空间,组成 4GB)。
而且咱们同时还获得了 page directory
它本身的 virtual 地址为:
0xC0701000
即 0xC0400000 ~ 0xC0800000
这 4MB 空间中的第 769
个 page,是否是很巧妙:)
这里的核心思想是,page directory
其实本质上是一个特殊的 page table
,它和其它 page table
同样,都管理着 4MB 的空间。
若是感受仍是有点绕的话,你不妨反过来验证一下,从上面给出的 virtual 地址开始,推导实际指向的 physical 地址是哪里,我想很快就能理清这里面的逻辑。
若是你进一步思考的话,就会发现这并非惟一的实现方式。你彻底能够不选择 pde[769]
,而使用其它 virtual 空间来映射 page tables,例如用 pde[770]
也能够,这样全部 page tables 对应的 virtual 空间就变成了 0xC0800000 ~ 0xC0C00000
。用 pde[769]
只是我我的的选择,由于它是 0xC0000000
后的第二个 4MB 空间,这样的安排,virtual 空间的使用能比较紧凑整齐一点。
到目前为止,pde 768 和 769 已经被使用,即 0xC0000000 ~ 0xC0400000
和 0xC0400000 ~ 0xC0800000
这两块 4MB 空间已被征用。剩下的 pde[770] ~ pde[1023]
对应的 254 个 page tables
,咱们依次为它们安排上 frames。这样咱们最终征用了 256 个 pages & frames,总共 1MB 的内存(virtual & physical),来创建 kernel 空间(3GB ~ 4GB)的 page tables
,管理这 1GB 的空间。
咱们将本章开始的那个 virtual-to-physical
内存映射关系图中的橙色部分抽出放大,展现 kernel 的 256 张 page tables 的内存分布:
注意到咱们只分配了 kernel 空间即 3GB 以上的 page tables,共 256 张,占地 1MB,它们映射的也是 0xC0400000 ~ 0xC0800000
空间的后 1/4 部分即 0xC0700000 ~ 0xC0800000
;而 3GB 如下的用户空间此时并无分配 page tables,由于目前咱们并无使用到。
这 256 张 kernel 页表(其中有一张是 page directory
自己),是咱们编写 kernel 期间最核心的 page tables,而且在 page directory 里创建了 pde[768] ~ pde[1023]
这所有的 256 个表项,指向了这些 page tables。
其实除了前两个 page table,后面 254 个目前都是空的,没有被用到,咱们只是为它们安排好了 frame 而已。这里用去了足足 1MB 的 physical 内存,这看上有点奢侈了,毕竟这个项目配置里 physical 内存总共只有 32 MB(见 bochsrc.txt
,固然如今的计算机内存远不止 32 MB,这已经不是个问题)。这样作有一个很是重要的缘由,那就是这 256 张 kernel page tables 后面将被全部的进程(process
)共享,也就是说对于用户 process 而言,3GB 如下的空间是隔离的,而 3GB 以上的 kernel 的空间是共享的,这也是理所固然的,不然就有多个 kernel 在内存中独立运行了。
每次 fork
出一个新的 process,它的 page directory
的后 1/4 即 768 ~ 1023 项将会直接复制 kernel 的 page directory
的 768 ~ 1023 项,共同指向这 256 张 kernel page tables
。因此咱们要求这 256 张 page tables
对应的 frames
从一开始就固定下来,后面也再也不变化,这样才能实现全部 process 共享的效果。
page tables
都准备就绪之后,就能够打开 paging
了:
enable_page: sgdt [gdt_ptr] ; move the video segment to > 0xC0000000 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xC0000000 ; move gdt to > 0xC0000000 add dword [gdt_ptr + 2], 0xC0000000 ; move stack to > 0xC0000000 mov eax, [esp] add esp, 0xc0000000 mov [esp], eax ; set page directory address to cr3 register mov eax, PAGE_DIR_PHYSICAL_ADDR mov cr3, eax ; enable paging on cr0 register mov eax, cr0 or eax, 0x80000000 mov cr0, eax
这里最重要的就是设置 CR3
寄存器,使之指向 page directory
的 frame (注意是 physical 地址),而后打开 CR0
寄存器上的 paging 比特位开关。
至此,loader 阶段关于 kernel 虚拟内存初始化的部分就结束了。这一段的代码并不长,核心仅仅是 setup_page 这一个函数,可是其背后的原理倒是很是深入复杂。在 loader 阶段初步创建起 virtual memory
的框架,这对后面进入 kernel 以后的内存管理打下了良好的基础。
在当前阶段咱们全部的 virtual-to-physical 的内存分配和映射都是提早规划,预先分配再使用的,每一块 physical frame 都是手动安排。这其实并无彻底发挥出 virtual memory
的做用。在后面进入 kernel 以后,咱们将进一步完善 virtual memory
相关的工做,这将包括缺页异常 (page fault)
的处理,进程 page directory
的复制等。
virtual memory
的处理是贯穿 kernel 实现和运行的底层核心工做,必须保证绝对的正确和稳定。一旦出错,系统会马上出现各类难以预知的奇怪错误甚至崩溃,而且 debug 很是困难。
下一篇咱们将会加载真正的 kernel
到内存而且转到 kernel 开始执行代码,这将是进入 kernel 前的最后一道关卡。