目录
一、前景回顾
我们已经介绍了分页机制的工作原理,那么如何开启分页机制也很简单。分为以下三个步骤:
1、创建页目录表并初始化页内存。
2、将页目录表的地址赋值给CR3。
3、开启CR0寄存器的PG位。
可见页表是分页机制的核心,接下来我们开始在我们的系统上实现一个二级页表。
二、规划页面
设计页表其实就是设计内存布局,但是在规划内存布局之前,我们需要了解用户进程和操作系统的关系。
在操作系统中,用户进程始终以低权限级别运行以确保计算机安全。当用户进程需要访问硬件相关资源时,需要向操作系统申请,然后通过系统调用落入操作系统,操作系统去做,并将结果返回给用户进程。可以有多个进程,但操作系统只有一个。所以操作系统必须对每个进程“共享”,它们的关系如图所示。
对于每一个进程来说,不仅仅是运行一个程序这样简单的事情,它需要被调度、阻塞,需要被困在内核中的时候需要操作系统的帮助等等。因此,一个完成过程需要与操作系统配合才能完成正常工作。也就是说,每个进程都应该包含操作系统部分。对于Linux下的每个进程,上面的1GB空间是为操作系统保留的,下面的3GB空间是为进程用户空间本身保留的。在我们的系统中,也遵循这种安排。对于这么高的 1GB 空间,我们不会在每次创建新进程时都将操作系统代码复制到这 1GB 空间中,这样比较麻烦,而且随着进程数量的增加,占用的内存也更多。实际上,操作系统代码只有一份副本。每次我们新建一个进程,让进程的上1GB空间指向操作系统。
现在让我们回过头来看看我们的系统。让我提前透露一下。后来内核完善之后,我们整个操作系统的代码不到1MB,所以我们这里假设最终操作系统的代码只有1MB。也就是说整个操作系统代码的存储区域是从0x0到0xFFFFF。上面说的划分1GB的空间来存放操作系统代码有很大的差距,很大一部分是没有用的,因为我们的操作系统会比较简单,不会用到这么多空间。不过内存划分还是按照Linux下的格式,比较容易学。
所以页目录表的地址存放在物理地址0x100000。为了使页目录表紧挨着页目录表,页目录表本身占用4KB,所以第一个页表的物理地址为0x101000。我还展示了其他计划以及它们的物理内存布局。
这张图一目了然地展示了我们的整个页表计划,让我解释一下细节。
首先,我们知道页表是用来映射虚拟地址的。使用虚拟地址的前提是启用分页机制。如果没有启用分页机制,那么页表就没用了。现在假设我们开启分页机制,那么注意我们使用的地址不再是之前的线性地址,而是一个虚拟地址。怎么说呢,以前想要CPU访问一个地址,只需要反汇编这个地址,分配给CS和段中的偏移量,但是开启分页机制后,反汇编的地址就可以不被访问。期望的地址,因为开启分页机制后,CPU得到这个地址,会根据CR3寄存器中存储的页目录表的地址进行寻址,最终的物理地址就是CPU实际访问的地址具体步骤请参考之前的内容。
那么现在如果我们要访问操作系统的代码,也就是低端的1MB内存,怎么访问呢?前面我们说过,Linux使用用户进程的上1GB作为操作系统的空间,所以我们可以知道,在用户进程中,虚拟地址0xc0000000~0xffffffff映射到操作系统的1GB空间,而在我们的系统中,操作系统代码总共占用1MB内存,所以从0xc0000000~0xc00fffff是映射到我们操作系统的1MB空间,0xc0000000虚拟地址对应的页目录项应该是第768个,也就是容易计算,0xc0000000的高10位为0x300,即十进制的768。这个目录项可以表示的内存空间是4MB,所以我们指定一个页表来管理这4MB的空间,所以我们在页目录表的第768页目录项中填写页表的物理地址0x101000,我们将查看地址0x101000,页表的页表项0到255所指向的物理内存就是我们操作系统的1MB空间。
从页目录项的第769到第1022页目录项,我们只指定页表的地址,并没有初始化实际的页表,因为我们的操作系统只占用1MB的空间,而多余的空间也是没用的,就是来占个位子的。
页目录项的第1023项可能会好奇为什么该项指向的地址是页目录表本身的地址。这是通过虚拟地址访问页目录表本身。如果后面需要修改页目录表,我们可以通过0xffc000~0xfffff000访问页目录表的0到1023项。有兴趣的朋友可以自己试一试,看看这个虚拟地址最终能不能转换成页目录表项的物理地址。当然,可能有人会说,这种情况下,操作系统实际上并没有占用1GB的内存空间,反而损失了4MB的空间。其实这是真的,但其实问题不大吧?
最后我们来看看为什么页目录表第0项的内容是0x101000。原因是在我们加载内核之前,程序一直是一个运行的加载器,它自己的代码在低端的1MB以内。我们要保证前段机制下的线性地址和分页后虚拟地址对应的物理地址一致。
三、实现页表
前面说了这么多,下面来实现我们的页目录表和页表。在loader.S文件中添加如下代码:
1 %include "boot.inc"
2 section loader vstart=LOADER_BASE_ADDR
3 LOADER_STACK_TOP equ LOADER_BASE_ADDR
4 jmp loader_start
5
6 ;构建gdt及其内部描述符
7 GDT_BASE: dd 0x00000000
8 dd 0x00000000
9 CODE_DESC: dd 0x0000FFFF
10 dd DESC_CODE_HIGH4
11 DATA_STACK_DESC: dd 0x0000FFFF
12 dd DESC_DATA_HIGH4
13 VIDEO_DESC: dd 0x80000007
14 dd DESC_VIDEO_HIGH4
15
16 GDT_SIZE equ $-GDT_BASE
17 GDT_LIMIT equ GDT_SIZE-1
18 times 60 dq 0 ;此处预留60个描述符的空位
19
20 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
21 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
22 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
23
24 ;以下是gdt指针,前2个字节是gdt界限,后4个字节是gdt的起始地址
25 gdt_ptr dw GDT_LIMIT
26 dd GDT_BASE
27
28 ;---------------------进入保护模式------------
29 loader_start:
30 ;一、打开A20地址线
31 in al, 0x92
32 or al, 0000_0010B
33 out 0x92, al
34
35 ;二、加载GDT
36 lgdt [gdt_ptr]
37
38 ;三、cr0第0位(pe)置1
39 mov eax, cr0
40 or eax, 0x00000001
41 mov cr0, eax
42
43 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水线
44
45 [bits 32]
46 p_mode_start:
47 mov ax, SELECTOR_DATA
48 mov ds, ax
49 mov es, ax
50 mov ss, ax
51 mov esp, LOADER_STACK_TOP
52 mov ax, SELECTOR_VIDEO
53 mov gs, ax
54
55 mov byte [gs:160], 'p'
56
57 ;------------------开启分页机制-----------------
58 ;一、创建页目录表并初始化页内存位图
59 call setup_page
60
61 ;将描述符表地址及偏移量写入内存gdt_ptr,一会儿用新地址重新加载
62 sgdt [gdt_ptr]
63 ;将gdt描述符中视频段描述符中的段基址+0xc0000000
64 mov ebx, [gdt_ptr + 2]
65 or dword [ebx + 0x18 + 4], 0xc0000000
66
67 ;将gdt的基址加上0xc0000000使其成为内核所在的高地址
68 add dword [gdt_ptr + 2], 0xc0000000
69
70 add esp, 0xc0000000 ;将栈指针同样映射到内核地址
71
72 ;二、将页目录表地址赋值给cr3
73 mov eax, PAGE_DIR_TABLE_POS
74 mov cr3, eax
75
76 ;三、打开cr0的pg位
77 mov eax, cr0
78 or eax, 0x80000000
79 mov cr0, eax
80
81 ;在开启分页后,用gdt新的地址重新加载
82 lgdt [gdt_ptr]
83 mov byte [gs:160], 'H'
84 mov byte [gs:162], 'E'
85 mov byte [gs:164], 'L'
86 mov byte [gs:166], 'L'
87 mov byte [gs:168], 'O'
88 mov byte [gs:170], ' '
89 mov byte [gs:172], 'P'
90 mov byte [gs:174], 'A'
91 mov byte [gs:176], 'G'
92 mov byte [gs:178], 'E'
93
94 jmp $
95 ;---------------------------------------------
96
97 ;--------------函数声明------------------------
98 ;setup_page:(功能)设置分页------------
99 setup_page:
100 ;先把页目录占用的空间逐字节清0
101 mov ecx, 4096
102 mov esi, 0
103 .clear_page_dir:
104 mov byte [PAGE_DIR_TABLE_POS + esi], 0
105 inc esi
106 loop .clear_page_dir
107
108 ;开始创建页目录项
109 .create_pde:
110 mov eax, PAGE_DIR_TABLE_POS
111 add eax, 0x1000 ;此时eax为第一个页表的位置
112 mov ebx, eax
113
114 ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存
115 ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问
116 or eax, PG_US_U | PG_RW_W | PG_P
117
118 ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性
119 mov [PAGE_DIR_TABLE_POS + 0x0], eax
120
121 mov [PAGE_DIR_TABLE_POS + 0xc00], eax
122
123 ;使最后一个目录项指向页目录表自己的地址
124 sub eax, 0x1000
125 mov [PAGE_DIR_TABLE_POS + 4092], eax
126
127 ;下面创建页表项(PTE)
128 mov ecx, 256 ;1M低端内存/每页大小4K=256
129 mov esi, 0
130 mov edx, PG_US_U | PG_RW_W | PG_P
131 .create_pte: ;创建page table entry
132 mov [ebx + esi*4], edx
133 add edx, 4096
134 inc esi
135 loop .create_pte
136
137 ;创建内核其他页表的PDE
138 mov eax, PAGE_DIR_TABLE_POS
139 add eax, 0x2000 ;此时eax为第二个页表的位置
140 or eax, PG_US_U | PG_RW_W | PG_P
141 mov ebx, PAGE_DIR_TABLE_POS
142 mov ecx, 254 ;范围为第769~1022的所有目录项数量
143 mov esi, 769
144 .create_kernel_pde:
145 mov [ebx + esi*4], eax
146 inc esi
147 add eax, 0x1000
148 loop .create_kernel_pde
149 ret
加载器.S
为了便于理解,我还是附上之前的代码。新增的是开放分页机制和函数声明部分。
让我们关注 setup_page 函数。该函数的作用是创建页目录表并初始化页内存。 PAGE_DIR_TABLE_POS在boot.inc文件中定义为0x100000,就是我们前面提到的页目录表的存储地址。
先将PAGE_DIR_TABLE_POS的4096字节作为起始地址,即一个物理页大小的内存空间清空,然后进行初始化。代码中有很多注释,这里不再赘述。
四、运行测试
这里不再赘述。和之前一样,通过nasm和dd命令编译loader.S并写入硬盘,运行boch得到如下画面。在boch控制台输入info tab命令,查看生成的页表。
左边是虚拟地址,右边是映射后的真实物理地址。和我们之前设计的页表相比,没有问题,说明我们的程序没有问题。
到此结束,接下来我们将开始进入内核,开始用熟悉的 C 语言编写程序。要了解接下来发生的事情,请参阅下一个细分。
暂无评论内容