这节仍然是从实现的角度来讲述分页机制。
为什么要引入分页机制。我们都知道分段机制是为了提供保护机制,那么为什么还要引入分页机制呢?
为什么引入分页机制
想象一下这样一种情况:假设我们用的计算机物理内存是4GB,但是我们的程序大小是5GB。那么这个时候我们无法将程序全部放到内存中,也就无法运行程序。分页机制引入的原因之一就是为了解决这个问题。分页机制的引入实现了虚拟存储器的机制。
另外,程序执行具有局部性,也就是说一段时间内,只需要程序代码中的一小部分(相对于整个程序)就可以实现程序的执行。那么我们就不用把所有代码和数据都存放在内存中,而是将现在或很近的将来需要的代码和数据放入内存就行了。要实现这个功能需要分页机制。在这种情况下,相同大小的内存在引入分页机制后可以同时存放更多的程序。这由进一步提高了存储器的容量。
在介绍分页机制之前,首先我们需要了解三种地址:
逻辑地址、线性地址、物理地址
这三种地址的关系如下图:
<font color=”red>这里是地址转换图:OS-adressAfterPagingAndSegment
引入段页式存储之后才有完整的三种地址的概念。这时候逻辑地址通过分段机制转换成线性地址,然后再通过分页机制转换成物理地址。
在没有引入页式存储的情况下,逻辑地址通过分段机制转换成的线性地址等于物理地址。
如果段式存储和页式存储都不存在。那么也就不存在逻辑地址和线性地址,我们对内存的所有操作都直接使用物理地址。
通过上面的分析,我们很容易明白分页机器就像一个函数:
物理地址 = f(线性地址) |
接下来,以二级页表机制对分页机制进行描述
分页机制概述
二级页表的分页机制如下图:
上图的转换使用两级页表,第一级叫做页目录,大小为4KB,存储在一个物理页中,每个表项4字节长,共有1024个表项。每个表项对应第二级的一个页表,第二级的每一个页表也有1024个表项,每一个表项对应一个物理页。页目录表的表项简称PDE(Page Directory Entry),页表的表项简称PTE(Page Table Entry)。
进行转换时,先是从由寄存器cr3指定的页目录中根据线性地址的高10位得到页表地址,然后在页表中根据线性地址的第12到21位得到物理页首地址,将这个首地址加上线性地址低12位便得到了物理地址。
分页机制是否生效的开关位于cr0的最高位PG位。如下图:
如果PG=1,则分页机制生效。所以,当我们准备好了页目录表和页表,并将cr3指向页目录表之后,只需要置PG位,分页机制就开始工作了。
接下来描述PDE和PTE的结构和各位的详细解释:
PDE和PTE
下图是PDE的结构:
下图是PTE的结构:
下面是关于PDE和PTE中各位的解释:
P
存在位,表示当前条目所指向的页或页表是否在物理内存中。P=0表示页不在内存中,如果处理器试图访问此页,将会产生页异常(page-fault exception,#PF);P=1表示页在内存中。R / W
指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的读写权限。此位与U/S位和寄存器cr0中的WP位相互作用。R/W=0表示只读;R/W=1表示可读并可写。U / S
指定一个页或者一组页(比如,此条目是指向页表的页目录条目)的特权级。此位与R/W位和寄存器cr0中的WP位相互作用。U/S=0表示系统级别(Supervisor
Privilege Level),如果CPL为0、1或2,那么它便是在此级别;U/S=1表示用户级别(User Privilege Level),如果CPL为3,那么它便是在此级别。如果cr0中的WP位为0,那么即便用户级(User P.L.)页面的R/W=0,系统级(Supervisor P.L.)程序仍然具备写权限;如果WP位为1,那么系统级(Supervisor P.L.)程序也不能写入用户级(User P.L.)只读页。
P W T
用于控制对单个页或者页表的缓冲策略。PWT=0时使用Write-back缓冲策略;PWT=1时使用Write-through缓冲策略。当cr0寄存器的CD(Cache-Disable)位被设置时会被忽略。P C D
用于控制对单个页或者页表的缓冲。PCD=0时页或页表可以被缓冲;PCD=1时页或页表不可以被缓冲。当cr0寄存器的CD(Cache-Disable)位被设置时会被忽略。A
指示页或页表是否被访问。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次访问此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。D
指示页或页表是否被写入。此位往往在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第一次写入此页或页面时设置此位。而且,处理器并不会自动清除此位,只有软件能清除它。A位和D位都是被内存管理程序用来管理页和页表从物理内存中换入和换出的。
P S
决定页大小。PS=0时页大小为4KB,PDE指向页表。P A T
选择PAT(Page Attribute Table)条目。Pentium III以后的CPU开始支持此位,在此不予讨论,并在我们的程序中设为0。G
指示全局页。如果此位被设置,同时cr4中的PGE位被置,那么此页的页表或页目录条目不会在TLB中变得无效,即便cr3被加载或者任务切换时也是如此。
处理器会将最近常用的页目录和页表项保存在一个叫做TLB(Translation Lookaside Buffer)的缓冲区中。只有在TLB中找不到被请求页的转换信息时,才会到内存中去寻找。这样就大大加快了访问页目录和页表的时间。
当页目录或页表项被更改时,操作系统应该马上使TLB中对应的条目无效,以便下次用到此条目时让它获得更新。
当cr3被加载时,所有TLB都会自动无效,除非页或页表条目的G位被设置。
接下来看看cr3的结构:
cr3
cr3的结构如下图:
cr3又叫做PDBR(Page-Directory Base Register)。它的高20位将是页目录表首地址的高20位,页目录表首地址的低12位会是零,也就是说,页目录表会是4KB对齐的。类似地,PDE中的页表基址(PageTable Base Address)以及PTE中的页基址(PageBase Address)也是用高20位来表示4KB对齐的页表和页。
至于第3位和第4位的两个标志,我们暂时可以忽略它们。
对cr0
、cr3
、PDE
、PTE
的结构有了解之后,接下来编写代码启动分页机制:
编写代码启动分页机制
这里不考虑特权级的变化,这样更能专注于分页机制的实现。
这里仅列出新增代码,完整代码会放在本文的最后。
8 PageDirBase equ 200000h ; 页目录开始地址: 2M
9 PageTblBase equ 201000h ; 页表开始地址: 2M+4K
...
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
...
34 SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
35 SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
...
166 [SECTION .s32]; 32 位代码段. 由实模式跳入.
167 [BITS 32]
168
169 LABEL_SEG_CODE32:
170 call SetupPaging
...
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 ; 分页机制启动完毕 ----------------------------------------------------------
上面的指令中,只有stosd
没有学过。类似的指令有stosb
、stosw
、stosd
。这三个指令就是把al
、ax
、eax
的内容存储到edi指向的内存单元中,同时edi的值根据方向标志的值增加或者减少。这里使用的是loop指令。它还可以同rep前缀联合使用。这里我没找到设置方向标志位的指令,难道是初始时候方向标志位已经为0了?
上面的代码实现的功能如下图:
开头的第207行和第208行将段寄存器es对应页目录表段,下面让edi等于0,于是es:edi就指向了页目录表的开始。第214行的指令stosd第一次执行时就把eax中的PageTblBase|PG_P|PG_USU|PG_RWW存入了页目录表的第一个PDE。
那么来看看这个PDE是什么值。PageTblBase|PG_P|PG_USU|PG_RWW(第212行)让当前(第一个)PDE对应的页表首地址变成PageTblBase,而且属性显示其指向的是存在的可读可写的用户级别页表。
实际上,当为页目录表中的第一个PDE赋值时,一个循环就已经开始了。循环的每一次执行中,es:edi会自动指向下一个PDE,而第215行也将下一个页表的首地址增加4096字节,以便与上一个页表首尾相接。这样,经过1024次循环(第209行由ecx指定)之后,页目录表中的所有PDE都被赋值完毕,它们的属性相同,都为指向可读可写的用户级别页表,并且所有的页表连续排列在以
PageTblBase为首地址的4MB(4096×1024)的空间中。
接下来的工作是初始化所有页表中的PTE(第218行到第228行)。由于总共有1024×1024个PTE,于是将ecx赋值为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,其中0x0FFFh。接下来进行的循环初始化了剩下的所有页表中的PTE,将4GB空间的线性地址映射到相同的物理地址。如上图所示
这样,页目录表和所有的页表被初始化完毕。接下来到了正式启动分页机制的时候了。首先让cr3指向页目录表(第230行和第231行),然后设置cr0的PG(第232行到第234行),这样,分页机制就启动完成了。
运行结果如下:
从这里我们看到启动分页机制后。我们无法在屏幕上看到分页机制的影子。这是因为我们只是把所有的线性地址映射到完全相同的物理地址上,而并没有对其做其他的操作。所以我们看不出来表面上的变化。而且这种方式会出现两个问题:一是页表显然浪费得太多了,我们可能根本没有那么大的内存;二是我们除了“实现了”分页,并没有“得益于”分页,也就是说,我们还没有体会到分页的妙处。上面的问题我们在下节介绍。
源代码
; ==========================================
; pmtest6.asm
; 编译方法:nasm pmtest6.asm -o pmtest6.com
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些说明
PageDirBase equ 200000h ; 页目录开始地址: 2M
PageTblBase equ 201000h ; 页表开始地址: 2M+4K
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
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
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32 ; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT ; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .data1] ; 数据段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ^-^", 0 ; 进入保护模式后显示此字符串
OffsetPMMessage equ PMMessage - $$
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
; 全局堆栈段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
; END of [SECTION .gs]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
; 初始化 16 位代码段描述符
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化 32 位代码段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化数据段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆栈段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 为加载 GDTR 作准备
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorCode32:0
; 执行这一句会把 SelectorCode32 装入 cs, 并跳转到 Code32Selector:0 处
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LABEL_REAL_ENTRY: ; 从保护模式跳回到实模式就到了这里
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
in al, 92h ; ┓
and al, 11111101b ; ┣ 关闭 A20 地址线
out 92h, al ; ┛
sti ; 开中断
mov ax, 4c00h ; ┓
int 21h ; ┛回到 DOS
; END of [SECTION .s16]
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
call SetupPaging
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov ax, SelectorVideo
mov gs, ax ; 视频段选择子
mov ax, SelectorStack
mov ss, ax ; 堆栈段选择子
mov esp, TopOfStack
; 下面显示一个字符串
mov ah, 0Ch ; 0000: 黑底 1100: 红字
xor esi, esi
xor edi, edi
mov esi, OffsetPMMessage ; 源数据偏移
mov edi, (80 * 10 + 0) * 2 ; 目的数据偏移。屏幕第 10 行, 第 0 列。
cld
.1:
lodsb
test al, al
jz .2
mov [gs:edi], ax
add edi, 2
jmp .1
.2: ; 显示完毕
; 到此停止
jmp SelectorCode16:0
; 启动分页机制 --------------------------------------------------------------
SetupPaging:
; 为简化处理, 所有线性地址对应相等的物理地址.
; 首先初始化页目录
mov ax, SelectorPageDir ; 此段首地址为 PageDirBase
mov es, ax
mov ecx, 1024 ; 共 1K 个表项
xor edi, edi
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .1
; 再初始化所有页表 (1K 个, 4M 内存空间)
mov ax, SelectorPageTbl ; 此段首地址为 PageTblBase
mov es, ax
mov ecx, 1024 * 1024 ; 共 1M 个页表项, 也即有 1M 个页
xor edi, edi
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .2
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
; 分页机制启动完毕 ----------------------------------------------------------
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
; 16 位代码段. 由 32 位代码段跳入, 跳出后到实模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回实模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址会在程序开始处被设置成正确的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]