什么是页
所谓页,就是一块内存,在80386中,页的大小是固定的4096字节(4KB),在Pentium中,页的大小还可以是2MB 或者 4 MB,并且可以访问到多于4GB 的内存,这个超出我们讨论范畴! 只讨论页大小为4KB!
逻辑地址、线性地址、物理地址
在未打开分页机制的时候,线性地址等同于物理地址,于是可以认为,逻辑地址通过分段机制直接转换成物理地址。但当分页开启时,情况发生变化,分段机制将逻辑地址转换成线性地址,线性地址再通过分页机制转换成物理地址。
传统上,段地址和偏移地址称为逻辑地址!
段的管理是由处理器的段部件负责进行的,段部件将段地址和偏移地址相加,得到访问内存的地址。一般来说,段部件产生的地址就是物理地址。 IA-32 处理器支持多任务。在多任务环境下,任务的创建需要分配内存空间;当任务终止后, 还要回收它所占用的内存空间。在分段模型下,内存的分配是不定长的,程序大时,就分配一大块内存;程序小时,就分配一小块。时间长了,内存空间就会碎片化,就有可能出现一种情况:内存空间是有的,但都是小块,无法分配给某个任务。为了解决这个问题, IA-32 处理器支持分页功能, 分页功能将物理内存空间划分成逻辑上的页。页的大小是固定的,一般为 4KB,通过使用页,可以简化内存管理。 如图 10-3 所示,当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(Linear Address),线性地址还要经页部件转换后,才是物理地址。
为什么要分页呢?
分段管理机制已经提供了很好的保护机制,为啥还要加分页管理机制!除了管理内存,主要目的是在于实现虚拟存储器。 可以看出,线性地址中任意一个页都能映射到物理地址的中的任何一个页,这样使得内存管理变得相当灵活!
分页机制概述
我们可以知道分页机制就像一个函数!
物理地址 = f(线性地址)
如图所示:
转换使用两级页表,第一级叫做页目录,大小为4kb,存储在一个物理页中,每个表项4字节长,共有1024个表项。每个表项对应第二级的一个页表,第二级的每一个页表也有1024个表项,每一个表项对应一个物理页。
页目录表的表项简称PDE (Page Directory ENtry),页表的表项简称PTE (Page Table Entry)。
进行转换时,先是从由寄存器cr3 指定的页目录中根据线性地址的高10位得到页表地址,然后在页表中根据线性地址的第12到21位得到物理页首地址,将这个首地址加上线性地址低12位便得到了物理地址。
分页机制是否生效的开关位于cr0的最高位PG位。 如果PG= 1,则分页机制生效。 所以,当我们准备好了页目录表和页表,并将cr3指向页目录表之后,只需要置PG位,分页机制就开始工作了。下面我们就来写一段代码试验一下。
在 80386中,页的大小固定为 4K 字节,每一页的边界地址必须是4K 的倍数。因此,4G大小的地址空间被划分为 1M 个页,页的开始地址具有“XXXXX000H”的形式。为此,我们把页开始地址的高20 位 XXXXXH称为页码。线性地址空间页的页码也就是页开始边界线性地址的高20 位;物理地址空间页的页码也就是页开始边界物理地址的高20 位。可见,页码左移 12位就是页的开始地址,所以页码规定了页。
由于页的大小固定为 4K字节,且页的边界是 4K 的倍数,所以在把32 位线性地址转换成 32位物理地址的过程中,低 12 位地址保持不变。也就是说,线性地址的低12 位就是物理地址的低 12位。
线性地址空间的页到物理地址空间的页之间的映射用表来描述。由于4G 的地址空间划分为 1M个页,因此,如果用一张表来描述这种映射,那么该映射表就要有1M 个表项,若每个表项占用 4个字节,那么该映射表就要占用 4M 字节。为避免映射表占用如此巨大的存储器资源,所以80386 把页映射表分为两级。
页映射表的第一级称为页目录表,存储在一个 4K 字节的物理页中。页目录表共有1K 个表项,其中,每个表项为 4字节长,包含对应第二级表所在物理地址空间页的页码。页映射表的第二级称为页表,每张页表也安排在一个4K 字节的页中。每张页表都有 1K个表
项,每个表项为 4字节长,包含对应物理地址空间页的页码。由于页目录表和页表均由1K 个表项组成,所以使用 10位的索引就能指定表项,即用 10 位的索引值乘以4 (每个表项4字节)加基地址就得到了表项的物理地址。
控制寄存器CR3 指定页目录表;页目录表可以指定 1K个页表,这些页表可以分散存放在任意的物理页中,而不需要连续存放;每张页表可以指定1K 个物理地址空间的页,这些物理地址空间的页可以任意地分散在物理地址空间中。需要注意的是,存储页目录表和页表的基地址是对齐在4K 字节边界上的。
编辑代码启动分页机制
书上是以pmtest2.asm的基础上进行修改, 将试验内存写入和读取的描述符、代码以及数据都去掉,添加一个函数 SetupPaging !
代码如下:
1 ; ==========================================
2 ; pmtest6.asm
3 ; 编译方法:nasm pmtest6.asm -o pmtest6.com
4 ; ==========================================
5
6 %include "pm.inc" ; 常量, 宏, 以及一些说明
7
8 PageDirBase equ 200000h ; 页目录开始地址: 2M
9 PageTblBase equ 201000h ; 页表开始地址: 2M+4K
10
11 org 0100h
12 jmp LABEL_BEGIN
13
14 [SECTION .gdt]
15 ; GDT
16 ; 段基址, 段界限, 属性
17 LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
18 LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
19 LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, DA_DRW;Page Directory
20 LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, DA_DRW|DA_LIMIT_4K;Page Tables
21 LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32 ; 非一致代码段, 32
22 LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
23 LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
24 LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位
25 LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
26 ; GDT 结束
27
28 GdtLen equ $ - LABEL_GDT ; GDT长度
29 GdtPtr dw GdtLen - 1 ; GDT界限
30 dd 0 ; GDT基地址
31
32 ; GDT 选择子
33 SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
34 SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
35 SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
36 SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
37 SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
38 SelectorData equ LABEL_DESC_DATA - LABEL_GDT
39 SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
40 SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
41 ; END of [SECTION .gdt]
42
43 [SECTION .data1] ; 数据段
44 ALIGN 32
45 [BITS 32]
46 LABEL_DATA:
47 SPValueInRealMode dw 0
48 ; 字符串
49 PMMessage: db "In Protect Mode now. ^-^", 0 ; 进入保护模式后显示此字符串
50 OffsetPMMessage equ PMMessage - $$
51 DataLen equ $ - LABEL_DATA
52 ; END of [SECTION .data1]
53
54
55 ; 全局堆栈段
56 [SECTION .gs]
57 ALIGN 32
58 [BITS 32]
59 LABEL_STACK:
60 times 512 db 0
61
62 TopOfStack equ $ - LABEL_STACK - 1
63
64 ; END of [SECTION .gs]
65
66
67 [SECTION .s16]
68 [BITS 16]
69 LABEL_BEGIN:
70 mov ax, cs
71 mov ds, ax
72 mov es, ax
73 mov ss, ax
74 mov sp, 0100h
75
76 mov [LABEL_GO_BACK_TO_REAL+3], ax
77 mov [SPValueInRealMode], sp
78
79 ; 初始化 16 位代码段描述符
80 mov ax, cs
81 movzx eax, ax
82 shl eax, 4
83 add eax, LABEL_SEG_CODE16
84 mov word [LABEL_DESC_CODE16 + 2], ax
85 shr eax, 16
86 mov byte [LABEL_DESC_CODE16 + 4], al
87 mov byte [LABEL_DESC_CODE16 + 7], ah
88
89 ; 初始化 32 位代码段描述符
90 xor eax, eax
91 mov ax, cs
92 shl eax, 4
93 add eax, LABEL_SEG_CODE32
94 mov word [LABEL_DESC_CODE32 + 2], ax
95 shr eax, 16
96 mov byte [LABEL_DESC_CODE32 + 4], al
97 mov byte [LABEL_DESC_CODE32 + 7], ah
98
99 ; 初始化数据段描述符
100 xor eax, eax
101 mov ax, ds
102 shl eax, 4
103 add eax, LABEL_DATA
104 mov word [LABEL_DESC_DATA + 2], ax
105 shr eax, 16
106 mov byte [LABEL_DESC_DATA + 4], al
107 mov byte [LABEL_DESC_DATA + 7], ah
108
109 ; 初始化堆栈段描述符
110 xor eax, eax
111 mov ax, ds
112 shl eax, 4
113 add eax, LABEL_STACK
114 mov word [LABEL_DESC_STACK + 2], ax
115 shr eax, 16
116 mov byte [LABEL_DESC_STACK + 4], al
117 mov byte [LABEL_DESC_STACK + 7], ah
118
119 ; 为加载 GDTR 作准备
120 xor eax, eax
121 mov ax, ds
122 shl eax, 4
123 add eax, LABEL_GDT ; eax <- gdt 基地址
124 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
125
126 ; 加载 GDTR
127 lgdt [GdtPtr]
128
129 ; 关中断
130 cli
131
132 ; 打开地址线A20
133 in al, 92h
134 or al, 00000010b
135 out 92h, al
136
137 ; 准备切换到保护模式
138 mov eax, cr0
139 or eax, 1
140 mov cr0, eax
141
142 ; 真正进入保护模式
143 jmp dword SelectorCode32:0 ; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
144
145 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
146
147 LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
148 mov ax, cs
149 mov ds, ax
150 mov es, ax
151 mov ss, ax
152
153 mov sp, [SPValueInRealMode]
154
155 in al, 92h ; ┓
156 and al, 11111101b ; ┣ 关闭 A20 地址线
157 out 92h, al ; ┛
158
159 sti ; 开中断
160
161 mov ax, 4c00h ; ┓
162 int 21h ; ┛回到 DOS
163 ; END of [SECTION .s16]
164
165
166 [SECTION .s32]; 32 位代码段. 由实模式跳入.
167 [BITS 32]
168
169 LABEL_SEG_CODE32:
170 call SetupPaging
171
172 mov ax, SelectorData
173 mov ds, ax ; 数据段选择子
174 mov ax, SelectorVideo
175 mov gs, ax ; 视频段选择子
176
177 mov ax, SelectorStack
178 mov ss, ax ; 堆栈段选择子
179
180 mov esp, TopOfStack
181
182
183 ; 下面显示一个字符串
184 mov ah, 0Ch ; 0000: 黑底 1100: 红字
185 xor esi, esi
186 xor edi, edi
187 mov esi, OffsetPMMessage ; 源数据偏移
188 mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕第 10 行, 第 0 列。
189 cld
190 .1:
191 lodsb
192 test al, al
193 jz .2
194 mov [gs:edi], ax
195 add edi, 2
196 jmp .1
197 .2: ; 显示完毕
198
199 ; 到此停止
200 jmp SelectorCode16:0
201
202 ; 启动分页机制 --------------------------------------------------------------
203 SetupPaging:
204 ; 为简化处理, 所有线性地址对应相等的物理地址.
205
206 ; 首先初始化页目录
207 mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
208 mov es, ax
209 mov ecx, 1024 ; 共 1K 个表项
210 xor edi, edi
211 xor eax, eax
212 mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
213 .1:
214 stosd
215 add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
216 loop .1
217
218 ; 再初始化所有页表 (1K 个, 4M 内存空间)
219 mov ax, SelectorPageTbl ; 此段首地址为 PageTblBase
220 mov es, ax
221 mov ecx, 1024 * 1024 ; 共 1M 个页表项, 也即有 1M 个页
222 xor edi, edi
223 xor eax, eax
224 mov eax, PG_P | PG_USU | PG_RWW
225 .2:
226 stosd
227 add eax, 4096 ; 每一页指向 4K 的空间
228 loop .2
229
230 mov eax, PageDirBase
231 mov cr3, eax
232 mov eax, cr0
233 or eax, 80000000h
234 mov cr0, eax
235 jmp short .3
236 .3:
237 nop
238
239 ret
240 ; 分页机制启动完毕 ----------------------------------------------------------
241
242 SegCode32Len equ $ - LABEL_SEG_CODE32
243 ; END of [SECTION .s32]
244
245
246 ; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
247 [SECTION .s16code]
248 ALIGN 32
249 [BITS 16]
250 LABEL_SEG_CODE16:
251 ; 跳回实模式:
252 mov ax, SelectorNormal
253 mov ds, ax
254 mov es, ax
255 mov fs, ax
256 mov gs, ax
257 mov ss, ax
258
259 mov eax, cr0
260 and eax, 7FFFFFFEh ; PE=0, PG=0
261 mov cr0, eax
262
263 LABEL_GO_BACK_TO_REAL:
264 jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
265
266 Code16Len equ $ - LABEL_SEG_CODE16
267
268 ; END of [SECTION .s16code]
269
可以看出代码增加了两个宏:
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, DA_DRW;Page Directory
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, DA_DRW|DA_LIMIT_4K;Page Tables
PageDirBase (PDE)指定了页目录表的位置。PageTblBase (PTE)指定了页表在内存中的位置!
页目录表位于地址2MB处,有1024个表项,占用4kb空间,紧接着页目录表便是页表,位于地址2MB + 4 kb处。 在这里,我们假定最大的可能,共有1024个页表。由于每个页表占用4096字节,所以这些页表共占用4MB空间!
也就是说,本程序所需的内存至少大于6MB! 可能至少有两个表项吧,因为一个表项有1024个页表,每个页表4096字节!
为了逻辑清晰和代码编写简便,分别定义两个段,用来存放页目录表和页表,大小分别是4kb 和 4 mb!
简单起见,程序将所有的线性地址映射到相同的物理地址,于是线性地址和物理地址的关系符合下面的公式:
物理地址 = f(线性地址) = 线性地址
所以程序大部分代码都是写入页目录表和页表,以便于上面的公式成立。
PDE 和 PTR
页目录表和页表中的表项都采用如下图所示的格式。从图中可见,最高20 位(位12—位31)包含物理地址空间页的页码,也就是物理地址的高20 位。低 12位包含页的属性。下图所示的属性中内容为0 的位是 Intel公司为 80486等处理器所保留的位,在为 80386 编程使用到它们时必须设置为 0。在位9 至位 11的 AVL字段供软件使用。表项的最低位是存在属性位,记作P。P位表示该表项是否有效。P=1表项有效;P=0表项无效,此时表项中的其余各位均可供软件使用,80386不解释 P=0的表项中的任何其它的位。在通过页目录表和页表进行的线性地址到物理地址的转换过程中,无论在页目录表还是在页表中遇到无效表项,都会引起页故障。其它属性位的作用在下文中介绍。
线性地址到物理地址的转换
分页管理机制通过上述页目录表和页表实现 32 位线性地址到32 位物理地址的转换。控制寄存器 CR3的高 20位作为页目录表所在物理页的页码。首先把线性地址的最高10 位(即位22 至位 31)作为页目录表的索引,对应表项所包含的页码指定页表;然后,再把线性地址的中间10 位(即位12 至位 21)作为所指定的页目录表中的页表项的索引,对应表项所包含的页码指定物理地址空间中的一页;最后,把所指定的物理页的页码作为高20 位,把线性地址的低 12位不加改变地作为 32位物理地址的低 12位。
页目录和页表中分别存放为页目录项和页表项, 它们的格式如下:
分页机制只区分两种特权级。特权级0、1和 2统称为系统特权级,特权级 3 称为用户特权级。在上图所示页目录表和页表的表项中的保护属性位R/W 和 U/S就是用于对页进行保护。
位0 –目录表项中的存在位 P表明对应页表是否有效。如果 P=1,表明对应页表有效,可利用它进行地址转换;如果P=0,表明对应页表无效。
表项的位 1是读写属性位,记作 R/W。R/W位指示该表项所指定的页是否可读、写或执行。若R/W=1,对表项所指定的页可进行读、写或执行;若R/W=0,对表项所指定的页可读或执行,但不能对该指定的页写入。但是,R/W位对页的写保护只在处理器处于用户特权级时发挥作用;当处理器处于系统特权级时,R/W位被忽略,即总可以读、写或执行。
表项的位 2是用户/系统属性位,记作U/S。U/S位指示该表项所指定的页是否是用户级页。若U/S=1,表项所指定的页是用户级页,可由任何特权级下执行的程序访问;如果U/S=0,表项所指定的页是系统级页,只能由系统特权级下执行的程序访问。下表列出了上述属性位R/W 和 U/S所确定的页级保护下,用户级程序和系统级程序分别具有的对用户级页和系统级页进行操作的权限。
由上表可见,用户级页可以规定为只允许读/执行或规定为读/写/执行。系统级页对于系统级程序总是可读/写/执行,而对用户级程序总是不可访问的。于分段机制一样,外层用户级执行的程序只能访问用户级的页,而内层系统级执行的程序,既可访问系统级页,也可访问用户级页。与分段机制不同的是,在内层系统级执行的程序,对任何页都有读/写/执行访问权,即使规定为只允许读/执行的用户页,内层系统级程序也对该页有写访问权。
页目录表项中的保护属性位 R/W和 U/S对由该表项指定页表所指定的全部 1K 各页起到保护作用。所以,对页访问时引用的保护属性位R/W 和 U/S的值是组合计算页目录表项和页表项中的保护属性位的值所得。下表列出了组合计算前后的保护属性位的值,组合计算是“与”操作。
正如在 80386地址转换机制中分页机制在分段机制之后起作用一样,由分页机制支持的页级保护也在由分段机制支持的段级保护之后起作用。先测试有关的段级保护,如果启用分页机制,那么在检查通过后,再测试页级保护。如果段的类型为读/写,而页规定为只允许读/执行,那么不允许写;如果段的类型为只读/执行,那么不论页保护如何,也不允许写。
页级保护的检查是在线性地址转换为物理地址的过程中进行的,如果违反页保护属性的规定,对页进行访问(读/写/执行),那么将引起页异常。
cr3的结构如下:
cr3又叫PDBR(Page-Directory Base Register)它的高20位将是页目录表首地址的高20位,页目录表首地址的低12位会是零,也就是说,页目录表会是4KB对齐的。
回头看看代码:
第207行 - 208行
; 首先初始化页目录
207 mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
208 mov es, ax
将es段寄存器指向了对应页目录表段,下面让edi等于0,于是es:edi 只想了页目录表的开始。
第214行的指令 stosd 第一次执行时就把eax 中的PageTblBase | PG_P | PG_USU | PG_RWW 存入了页目录表的第一个PDE中!
214 stosd ; 字符串处理
215 add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
216 loop .1
看看PDE 是什么值,PageTblBase | PG_P | PG_USU | PG_RWW 第212行 ` mov eax, PageTblBase | PG_P | PG_USU | PG_RWW` 让当前(第一个)PDE 对应的页表首地址编程PageTblBase , 而且属性显示其指向的是存在可读可写的用户级别页表!
实际上,当为页目录表的第一个PDE赋值时,一个循环就已经开始了。
循环的每一次执行中,es:edi会自动指向下一个PDE ,而第215行也将下一个页表的首地址增加4096字节,以便与上一个页表首尾相接。这样,经过1024次循环(第209行由ecs指定 mov ecx, 1024 ; 共 1K 个表项
)之后,页目录表中的所有PDE 都被赋值完毕,他们的属性相同,都为指向可读可写的用户级别页表,并且所有的页表连续排列在以PageTblBase为首地址的4MB (4096 × 1024 ) 的空间中。
接下来的工作是初始化所有页表中的PTE (第218行-228行)。由于总共有1024的2次幂 个PTE,也就是1024 × 1024 ,以便让循环进行1024 × 1024 次。 开始对es和edi的处理让es:edi 指向了页表段的首地址,即地址PageTblBase处,也是第一个页表的首地址!
第一个页表中的第一个PTE 被赋值为PG_P | PG_USU | PG_RWW, 不难理解,它表示此PTE指示的页首地址为0,并且是个可读可写的用户级别页! |
同时也意味着第0个页表中的第0个PTE指示的页的首地址是0,于是线性地址0~0FFFh将被映射到物理地址0~0FFFh,即f(x) = x,其中0<=x<=0FFFh。
接下来的进行的循环初始化了剩下的所有页表中的PTE,将4GB空间的线性地址映射到相同的物理地址。如图所示
这样,页目录表和所有的页表都被初始化完毕。接下来就是正式启动分页机制了!
第230-231行
230 mov eax, PageDirBase
231 mov cr3, eax
第232-234行 设置cr3的PG位!
232 mov eax, cr0
233 or eax, 80000000h
234 mov cr0, eax
这样分页机制就启动完成了!
编译运行!ok!
界面没有pmtest2试验显示的字符串,其他没啥变化!