系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- GDT 和 IDT,中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 创立第一个内核线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
kernel 虚拟内存概览
接上一篇 GDT 与保护模式,这一篇将是 loader 的重点。首先咱们须要建设 kernel 空间的虚拟内存。如果你对虚拟内存的原理还不相熟,请务必先自学,这里能够提供一个文档供参考。
到目前为止咱们始终在物理内存上操作,确切地说是在 1MB 的低地址空间内操作,这所有都很简略间接。然而接下来 loader 行将为加载 kernel 做筹备,咱们须要在更广大的 4GB 虚拟内存空间上布局数据和代码。
仿照 Linux 零碎,咱们将应用 3GB 以上的高地址空间作为内核空间来发展后续所有工作。例如最根本的,目前的物理低地址 1MB 会被映射到 virtual 地址 0 ~ 1MB
以及 3GB 以上空间 0xC0000000 ~ (0xC0000000 + 1MB)
处:
进入 kernel 当前,对低 1MB 空间的拜访将会应用 0xC0000000 ~ (0xC0000000 + 1MB)
虚拟地址,这里次要包含以后应用的 stack,以及显示器对应的内存映射:
所以 video 内存基地址将从 virtual 地址 0xC00B8000
开始,不过目前不用深究,后续将会在显示与打印一篇中详解。
除了最根本的低 1MB 内存空间,loader 还须要进一步在 0xC0000000
以上的 virtual 空间中开疆拓土,这次要包含两局部:
- kernel 所应用的页目录(
page directory
)和页表(page table
); - kernel 二进制镜像的读取,以及代码、数据的加载;
上面给出整个 loader 阶段将要搭建的 virtual-to-physical
内存映射关系图:
这张图是本篇最重要的全局图,其中第二行是第一行通过“扭曲”比例的图示,咱们将 3GB 以下的用户空间放大显示,以后重点只关注 3GB 以上的内核空间(粗框局部)。因为是 virtual 地址空间,咱们的空间划分能够比拟随便和“侈靡”,咱们以 4MB 为单位,从 0xC0000000
开始在 virtual 空间切割划分出以下几个区域:
- 第一个 4MB 保留,其中低 1MB 空间映射到了 physical 地址的低 1MB,这是下面曾经解释过的;
- 第二个 4MB(橙色)用来映射 kernel 的所有
page tables
; - 第三个 4MB(绿色),即从
0xC0800000
开始,作为加载、寄存kernel
代码和数据的空间,也就是说kernel
从该处开始编址;
这里要说一句,实现一个 OS 并没有固定的形式,以上只是我集体的实现形式。实际上对于内存的布局是很灵便的,就像这个我的项目的名字 scroll
一样,内存就是一幅画卷,CPU 则是画笔,在遵循肯定规定的前提下,能够做自由发挥。
上面咱们首先开始橙色局部,即内核 page directory
和 page tables
的建设。
建设 kernel 虚拟内存
在开始这一段之前,咱们还是回顾一下页目录(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
来表述。
建设 page directory
首先咱们须要拿出一个 frame
,用来作为 page directory
。回到 physical 内存散布的那张图,目前 1MB 以下的局部已被占用,咱们能够应用的局部就从 1MB 即 0x100000
开始。
我抉择的是 0x100000 + 4KB
,即 0x100000
后的第 2 个 frame 作为 page directory
,当然这齐全是集体抉择;0x100000
后的第 1 个 frame 我抉择将它作为第一个 page table
:
再次强调,这是我的集体抉择;frame 的抉择是十分自在的,只有是还没被占用的都能够应用,当然了你要记住本人用过了哪些 frames,正当紧凑并且尽量“好看”地布局应用。
映射 1MB 低内存空间
值得注意的是,第 0 和第 768 个 pde
都指向了同一个 page table
,这个 page table 咱们将用它映射 0 ~ 1MB
低内存,即咱们目前所处的 1MB 内存空间。当然这个 page table 能够治理 4MB 的空间,咱们只映射了其中的 1MB,残余 3MB 的 virtual 空间就闲置了,不过这没有关系,闲置就闲置,反正这是 virtual 空间。
第 768 个 pde
治理的是 0xC0000000
即 3GB 开始的第一个 4MB 空间,回到本篇开始的第一张图,这里也将被映射到低 1MB 内存上:
映射 page directory 以及 page tables 自身
这里是本节的重点和难点。咱们晓得 page directory
和 page tables
所指向的都是 physical 页,而一旦关上了 paging 模式,咱们当前所有对内存的拜访将全副通过 virtual 地址,无奈再间接操作 phyical 地址。那么问题来了,咱们如何拜访并批改 page directory
和 page tables
自身呢?
一种办法当然是在须要时敞开 paging,间接拜访 physical 地址,之前举荐的教程 JamesM’s kernel development tutorials 在很多中央都是这么做的,不过这并不是一种好的做法,起因有以下几点:
- 进入简单的 kernel 当前,代码的执行会大量波及到 stack 和 heap,以及其余全局变量等内存拜访,这些全部都是 kernel 空间的 virtual 地址,如果此时忽然敞开 paging,对它们的拜访将无奈进行。你必须十分小心地安顿你的代码对内存的拜访,否则将会呈现不可预知的结果,然而这其实十分难做到;
- 一旦开启多线程,如果在敞开 paging 的状况下产生了中断,CPU 将进行一些主动的 stack 操作以及中断解决,全部都是对 virtual 地址的操作,显然其后果也是灾难性的;
一个更正当的做法是,咱们将 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 空间的应用能比拟紧凑参差一点。
映射 kernel 空间的其它区域
到目前为止,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
的 768 ~ 1023 项将会间接复制 kernel 的 page directory
的 768 ~ 1023 项,独特指向这 256 张 kernel page tables
。所以咱们要求这 256 张 page tables
从一开始就固定下来,前面也不再变动,这样能力实现所有 process 共享的成果。
关上 paging
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_PHYISCAL_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 这一个函数,然而其背地的原理却是十分粗浅简单。在前面进入 kernel 之后,咱们将进一步欠缺虚拟内存相干的工作,这将包含缺页异样 (page fault)
的解决,过程 page directory
的复制等。虚拟内存的解决是贯通 kernel 实现和运行的底层外围工作,必须保障相对的正确和稳固。一旦出错,零碎会立即呈现各种难以预知的奇怪谬误甚至解体,并且 debug 十分艰难。
下一篇咱们将会加载真正的 kernel
到内存并且转到 kernel 开始执行代码,这将是进入 kernel 前的最初一道关卡。