0x00

关于原理和机制很多我直接在注释中写明了,所以就不过多阐述,更多的是总体上简单提一嘴。

0x01 为何分页

在有新进程需要内存而连续内存不够时,要么等某个进程使用内存完毕再给新进程使用,要么把某些进程使用的某些段内存先换出到硬盘,腾出一片连续内存来给新进程使用。

当然不可能采取第一种方法,不然新进程要等半天才能运行。第二种方法的思想是正确的,但是受限于段式内存管理,十分笨重,一次就要换出一整段,效率不高。并且如果内存很小甚至装不下任何一个进程的全部段,那不是啥都运行不了?

直接引用《操作系统真象还原》的原话: 问题的本质是在目前只分段的情况下,CPU 认为线性地址等 于物理地址。而线性地址是由编译器编译出来的,它本身是连续的,所以物理地址也必须要连续才行,但 我们可用的物理地址不连续。换句话说,如果线性地址连续,而物理地址可以不连续,不就解决了吗。

所以分页机制产生了。每一页不大不小 0x1000 字节。分页解除了线性地址与物理地址一一对应的关系,然后将它们的关系重新建立。通过某种映射关系,可以将线性地址映射到任意物理地址。也就是虚拟地址到物理地址的映射。这个乱序映射操作的单位就是页。如下图所示

0x02 虚拟地址空间的管理

以32位地址空间为例。

简单粗暴使用一级页表的话,也就是

也就是把 32 位虚拟地址的高 20 位划分为页表的索引,低 12 位划分为页内偏移进行一个映射。通过索引在一级页表中找到真实的物理地址的高 20 位,也就是物理页框号,在加上 12 位页内偏移就唯一确定了一个物理地址。

使用二级页表则是多划分一次。

把 32 位虚拟地址的高 10 位作为页目录表(PDE)的索引, 中间 10 位作为页表(PTE)的索引,低 12 位仍然是页内偏移。通过页目录表找到对应页表的物理地址,然后再对应页表项确定物理页的地址,加上页内偏移确定一个物理地址。

一级页表简单易懂,但是缺点是:每一个进程都要创建一个页表,而一个页表如果完全映射完所有物理内存,就需要 1M 个表项,每个表项是 4 字节,一张一级页表就要 4MB 内存。占用太大。

二级页表则可以动态的创建页表,并且每一个页表大小也只需一页也就是 0x1000 字节(4KB)。以及不提前创建好所有页表,也就是不提前分配物理页到虚拟地址空间,在程序访问某个虚拟地址时检索 PDE,如果对应的页表不存在,则现场创建一张页表,然后加入到页目录表;检索 PTE ,如果对应页面不存在,则现场分配或者替换一个物理页到页表中。这些功能在后续我们实现内核时需要实现:也就是缺页中断。

还有一个重要的问题:PDE 和 PTE 本身也处于内存中,如何通过虚拟地址访问他们本身呢? 解决方案是在 PDE 的最后一个表项放入 PDE 本身的物理地址。这样就实现了一个回环,可以通过虚拟地址访问 PDE 和 PTE 本身。

0x03 实现

还有进入保护模式前调用 BIOS 中断获取内存大小属性等信息的部分就不多赘述了,也就是调用几个 BIOS 提供的中断接口来获取内存信息。实现也就不放了。

代码注释有部分的细节机理。可能错误,请斧正。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
; =================================== 创建内核页表和页目录表
setup_page:
; 虚拟地址32位划分
; [10位][10位][12位]
; ↓ ↓ ↓
; 目录 页表 页内偏移

; ==== 先把页目录表占用的空间逐字节清0
mov ecx, 0x1000
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir
; ==== 开始创建内核地址空间最低4MB的页目录项(PDE)
.create_pde:
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ;页目录表本身占用0x1000大小,+0x1000就是第一个页表的物理地址
mov ebx, eax ;此处为ebx赋值, 是为.create_pte做准备, ebx为基址
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 ;一个页目录项占用4字节, +0xc00对应第0x300个页目录项
; 一个页目录项对应一张页表,一张页表大小0x1000字节, 一个页表项4字节, 共记录0x400页, 一页大小也是0x1000
; 那么第0x300个页目录项也就对应地址 0x300*0x400*0x1000 = 0xc0000000(内核地址的起始) 开始的 4MB 内存
; 这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表, 也就使用相同的映射方式, 映射到同一个物理地址
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ;使得最后一个目录项地址指向页目录表自己的地址

; ==== 开始创建内核地址空间最低1MB对应的页表项(PTE), 实际映射到物理地址最低的1MB
mov ecx, 256 ;1M低端内存/每页大小4K = 256, 因为物理地址最低的4MB只使用最低的1MB内存来存放kernel, 所以内核地址对应的的页表只需创建 256 页
mov esi, 0 ;虚拟地址0x0~0x3fffff和虚拟地址0xc0000000~0xc03fffff对应的物理页,现在只用了低1MB,此时虚拟地址是等于物理地址的
mov edx, PG_US_U | PG_RW_W | PG_P ; 页表属性。注意进入循环前第一页物理地址是0起始,所以直接用属性赋值即可
.create_pte: ;创建Page Table Entry
mov [ebx + esi*4], edx ;ebx为第一个页表的首地址,此前已赋值
add edx, 0x1000 ; 下一页
inc esi
loop .create_pte

; ==== 创建内核地址空间其他页面的PDE (内核占用1GB, 此前只创建了最低的4MB的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 ;范围为第0x301~0x3fe的所有页目录项数量(第0x3ff项是页目录表自身的物理地址)
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000 ; 下一张页表
loop .create_kernel_pde
; 至此为内核地址空间1GB范围建立了页目录项
ret

最后 qemu 运行效果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
(qemu) xp /16wx 0x100000
0000000000100000: 0x00101027 0x00000000 0x00000000 0x00000000
0000000000100010: 0x00000000 0x00000000 0x00000000 0x00000000
0000000000100020: 0x00000000 0x00000000 0x00000000 0x00000000
0000000000100030: 0x00000000 0x00000000 0x00000000 0x00000000
(qemu) xp /16wx 0x101000
0000000000101000: 0x00000027 0x00001007 0x00002007 0x00003007
0000000000101010: 0x00004007 0x00005007 0x00006007 0x00007007
0000000000101020: 0x00008007 0x00009007 0x0000a007 0x0000b007
0000000000101030: 0x0000c007 0x0000d007 0x0000e007 0x0000f007
(qemu) info mem
0000000000000000-0000000000100000 0000000000100000 urw
00000000c0000000-00000000c0100000 0000000000100000 urw
00000000ffc00000-00000000ffc01000 0000000000001000 urw
00000000fff00000-0000000100000000 0000000000100000 urw
(qemu) x/16wx 0xFFFFF000
fffff000: 0x00101027 0x00000000 0x00000000 0x00000000
fffff010: 0x00000000 0x00000000 0x00000000 0x00000000
fffff020: 0x00000000 0x00000000 0x00000000 0x00000000
fffff030: 0x00000000 0x00000000 0x00000000 0x00000000
(qemu) x/16wx 0xFFC00000
ffc00000: 0x00000027 0x00001007 0x00002007 0x00003007
ffc00010: 0x00004007 0x00005007 0x00006007 0x00007007
ffc00020: 0x00008007 0x00009007 0x0000a007 0x0000b007
ffc00030: 0x0000c007 0x0000d007 0x0000e007 0x0000f007

可以看到虚拟地址空间的映射完成了,并且使用虚拟地址对于 PDE 和 PTE 的访问也没问题。至此就为内核地址空间 (1GB) 都建立了 PDE,然后为其中最低的 1MB 建立好了实际虚拟地址映射,即将虚拟地址 0x000000000x000FFFFF 和 0x000000000x000FFFFF 都映射到了物理地址 0xC00000000xC00FFFFF。其余的 0xC01000000xC03FFFFF 还没有映射。