如果还不了解分页机制,请先看这里:分页机制讲解
注:该程序(及相关博文)源于《80X86汇编程序设计 杨季文》
下面给出一个演示如何启用分页管理机制的实例。该实例的逻辑功能是,在屏幕上显示一条表示已启用分页管理机制的提示信息。该实例演示内容包括:初始化页目录表和部分页表;启用分页管理机制;关闭分页管理机制等。该实例假设系统至少有4M字节物理内存。
1.演示步骤和源程序清单
.386P
;----------------------------------------------------------------------------
;打开A20地址线
;----------------------------------------------------------------------------
EnableA20 MACRO
push ax
in al,92h
or al,00000010b
out 92h,al
pop ax
ENDM
;----------------------------------------------------------------------------
;关闭A20地址线
;----------------------------------------------------------------------------
DisableA20 MACRO
push ax
in al,92h
and al,11111101b
out 92h,al
pop ax
ENDM
;----------------------------------------------------------------------------
;16位偏移的段间直接转移指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
JUMP16 MACRO Selector,Offset
DB 0eah ;操作码
DW Offset ;16位偏移量
DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;存储段描述符结构类型定义
;----------------------------------------------------------------------------
Desc STRUC
LimitL DW 0 ;段界限(BIT0-15)
BaseL DW 0 ;段基地址(BIT0-15)
BaseM DB 0 ;段基地址(BIT16-23)
Attributes DB 0 ;段属性
LimitH DB 0 ;段界限(BIT16-19)(含段属性的高4位)
BaseH DB 0 ;段基地址(BIT24-31)
Desc ENDS
;----------------------------------------------------------------------------
;伪描述符结构类型定义(用于装入全局或中断描述符表寄存器)
;----------------------------------------------------------------------------
PDesc STRUC
Limit DW 0 ;16位界限
Base DD 0 ;32位基地址
PDesc ENDS
;----------------------------------------------------------------------------
;存储段描述符类型值说明
;----------------------------------------------------------------------------
ATDR EQU 90h ;存在的只读数据段类型值
ATDW EQU 92h ;存在的可读写数据段属性值
ATDWA EQU 93h ;存在的已访问可读写数据段类型值
ATCE EQU 98h ;存在的只执行代码段属性值
;----------------------------------------------------------------------------
;----------------------------------------------------------------------------
;DPL值说明
;----------------------------------------------------------------------------
DPL0 EQU 00h ;DPL=0
;----------------------------------------------------------------------------
;----------------------------------------------------------------------------
;----------------------------------------------------------------------------
;分页机制使用的常量说明
;----------------------------------------------------------------------------
PL EQU 1 ;页存在属性位
RWR EQU 0 ;R/W属性位值,读/执行
RWW EQU 2 ;R/W属性位值,读/写/执行
USS EQU 0 ;U/S属性位值,系统级
USU EQU 4 ;U/S属性位值,用户级
;----------------------------------------------------------------------------
;============================================================================
PDT_AD = 200000h ;页目录表所在物理页的地址
PT0_AD = 202000h ;页表0所在物理页的地址
PT1_AD = 201000h ;页表1所在物理页的地址
PhVB_AD = 0b8000h ;物理视频缓冲区地址
LoVB_AD = 0f0000h ;程序使用的逻辑视频缓冲区地址
MPVB_AD = 301000h ;线性地址0B8000H所映射的物理地址
PhSC_AD = 303000h ;部分演示代码所在内存的物理地址
LoSC_AD = 402000h ;部分演示代码的逻辑地址
;============================================================================
GDTSeg SEGMENT PARA USE16 ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
;全局描述符表GDT
GDT LABEL BYTE
;空描述符
DUMMY Desc <>
;规范段描述符及选择子
Normal Desc <0ffffh,,,ATDW,,>
Normal_Sel = Normal-GDT
;页目录表所在段描述符(在保护方式下初始化时用)及选择子
PDT Desc <0fffh,PDT_AD AND 0ffffh,PDT_AD SHR 16,ATDW,,>
PDT_Sel = PDT-GDT
;页表0所在段描述符(在保护方式下初始化时用)及选择子
PT0 Desc <0fffh,PT0_AD AND 0ffffh,PT0_AD SHR 16,ATDW,,>
PT0_Sel = PT0-GDT
;页表1所在段描述符(在保护方式下初始化时用)及选择子
PT1 Desc <0fffh,PT1_AD AND 0ffffh,PT1_AD SHR 16,ATDW,,>
PT1_Sel = PT1-GDT
;逻辑视频缓冲区段描述符及选择子
LoVideo Desc <3999,LoVB_AD AND 0ffffh,LoVB_AD SHR 16,ATDW,,>
LoVideo_Sel = LoVideo-GDT
;逻辑上的部分演示代码段的描述符及选择子
LoCode Desc <SCodeLen-1,LoSC_AD AND 0ffffh,LoSC_AD SHR 16,ATCE,,>
LoCode_Sel = LoCode-GDT
;预定内存区域(用于部分演示代码)的段描述符及选择子
TPSCode Desc <SCodeLen-1,PhSC_AD AND 0ffffh,PhSC_AD SHR 16,ATDW,,>
TPSCode_Sel = TPSCode-GDT
;----------------------------------------------------------------------------
;以下是需额外初始化的描述符
EFFGDT LABEL BYTE
;临时代码段描述符及选择子
TempCode Desc <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel = TempCode-GDT
;演示代码段描述符及选择子
DemoCode Desc <DemoCodeLen-1,DemoCodeSeg,,ATCE,,>
DemoCode_Sel = DemoCode-GDT
;演示任务数据段描述符及选择子
DemoData Desc <DemoDataLen-1,DemoDataSeg,,ATDW,,>
DemoData_Sel = DemoData-GDT
;初始化时要移动的代码段描述符及选择子(移动时作为数据对待)
SCode Desc <SCodeLen-1,SCodeSeg,,ATDR,,>
SCode_Sel = SCode-GDT
;----------------------------------------------------------------------------
GDTLen = $-GDT ;全局描述符表长度
GDNum = ($-EFFGDT)/(SIZE Desc) ;需特殊处理的描述符数
;----------------------------------------------------------------------------
GDTSeg ENDS ;全局描述符表段定义结束
;============================================================================
;这部分代码在初始化时被复制到预定的内存区域,其功能是在屏幕上显示提示信息
;----------------------------------------------------------------------------
SCodeSeg SEGMENT PARA USE16
ASSUME CS:SCodeSeg,DS:DemoDataSeg
;----------------------------------------------------------------------------
SBegin PROC FAR
mov ax,LoVideo_Sel
mov es,ax
mov di,0
mov ah,17h
mov cx,MessLen
S1: lodsb
stosw
loop S1
JUMP16 DemoCode_Sel,Demo3
SBegin ENDP
;----------------------------------------------------------------------------
MLen = $-SBegin
SCodeLen = $-SCodeSeg
SCodeSeg ENDS
;============================================================================
DemoDataSeg SEGMENT PARA USE16 ;演示任务数据段
Mess DB 'Page is OK!'
MessLen = $-Mess
DemoDataLen = $-DemoDataSeg
DemoDataSeg ENDS
;============================================================================
DemoCodeSeg SEGMENT PARA USE16 ;演示任务代码段
ASSUME CS:DemoCodeSeg
;----------------------------------------------------------------------------
DemoBegin PROC FAR
mov ax,PDT_Sel
mov es,ax
xor di,di
mov cx,1024
xor eax,eax ;先把全部表项置成无效
rep stosd ;再置表项0和表项1
mov DWORD PTR es:[0],PT0_AD OR (USU+RWW+PL)
mov DWORD PTR es:[4],PT1_AD OR (USU+RWW+PL)
mov ax,PT0_Sel ;初始化页表0
mov es,ax
xor di,di
mov cx,1024
xor eax,eax
or eax,USU+RWW+PL
Demo1: stosd
add eax,1000h ;先全部置成对应等地址的
loop Demo1 ;物理页,再特别设置两个表项 1000h=4096
mov di,(PhVB_AD SHR 12)*4
mov DWORD PTR es:[di],MPVB_AD or (USS+RWW+PL)
mov di,(LoVB_AD SHR 12)*4
mov DWORD PTR es:[di],PhVB_AD or (USU+RWR+PL)
mov ax,PT1_Sel ;初始化页表1
mov es,ax
xor di,di
mov cx,1024
mov eax,400000h
Demo2: stosd ;先把全部表项设置为无效
add eax,1000h
loop Demo2 ;再特别设置1项
mov di,((LoSC_AD SHR 12)AND 3ffh)*4
mov DWORD PTR es:[di],PhSC_AD or (USU+RWR+PL)
mov eax,PDT_AD
mov cr3,eax
mov eax,cr0
or eax,80000000h
mov cr0,eax
jmp SHORT PageE
PageE: mov ax,DemoData_Sel
mov ds,ax
mov si,OFFSET Mess
JUMP16 LoCode_Sel,SBegin
Demo3: mov eax,cr0
and eax,7fffffffh ;关闭分页机制
mov cr0,eax
jmp SHORT PageD
PageD: mov ax,Normal_Sel
JUMP16 TempCode_Sel,ToDOS
DemoBegin ENDP
;----------------------------------------------------------------------------
DemoCodeLen = $-DemoCodeSeg
DemoCodeSeg ENDS
;============================================================================
TempCodeSeg SEGMENT PARA USE16 ;临时任务的代码段
ASSUME CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual PROC FAR
cld ;为演示在启用分页机制后执
mov ax,SCode_Sel ;行位于较高线性地址空间中
mov ds,ax ;的代码作准备
mov ax,TPSCode_Sel
mov es,ax
mov si,OFFSET SBegin
mov di,si
mov cx,MLen ;把分页演示代码复制到预定
rep movsb ;内存
JUMP16 DemoCode_Sel,DemoBegin
ToDOS: mov ds,ax
mov es,ax
mov eax,cr0 ;准备返回实模式
and al,11111110b
mov cr0,eax
JUMP16 <SEG Real>,<OFFSET Real>
Virtual ENDP
;----------------------------------------------------------------------------
TempCodeSeg ENDS
;============================================================================
RCodeSeg SEGMENT PARA USE16 ;实方式的初始化代码和数据
ASSUME CS:RCodeSeg,DS:RCodeSeg
;----------------------------------------------------------------------------
VGDTR PDesc <GDTLen-1,>
;----------------------------------------------------------------------------
Start PROC
push cs
pop ds
cld
call InitGDT ;初始化全局描述符表GDT
EnableA20
lgdt FWORD PTR VGDTR ;装载GDTR
cli ;关中断
mov eax,cr0
or al,1
mov cr0,eax
JUMP16 <TempCode_Sel>,<OFFSET Virtual>
Real: DisableA20
sti
mov ax,4c00h
int 21h
Start ENDP
;----------------------------------------------------------------------------
InitGDT PROC
push ds
mov ax,GDTSeg
mov ds,ax
mov cx,GDNum
mov si,OFFSET EFFGDT
InitG: mov ax,[si].BaseL
movzx eax,ax
shl eax,4
shld edx,eax,16
mov WORD PTR [si].BaseL,ax
mov BYTE PTR [si].BaseM,dl
mov BYTE PTR [si].BaseH,dh
add si,SIZE Desc
loop InitG
pop ds
mov bx,16
mov ax,GDTSeg
mul bx
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
ret
InitGDT ENDP
;----------------------------------------------------------------------------
RCodeSeg ENDS
END Start
下面仅就演示分页管理机制方面的内容作些说明:
(1)部分演示代码的移动
为了充分说明分页机制所实现的线性地址到物理地址的转换,在初始化时把部分演示代码移动到预定的内存区域。预定的内存区域从 00303000H 开始,即页码为 00303H 的物理页。该部分演示代码的功能是显示指定的字符串。在进入保护模式后做此初始化工作的原因是预定的内存区域在扩展内存中,注意初始化时还没有启用分页机制。
(2)页映射表的初始化
页目录表安排在页码为 00200H 的物理页中,页表 0 安排在页码为 00202H 的物理页中,页表 1 安排在页码为 00201H 的物理页中。演示程序涉及的线性地址空间不超过 007FFFFFH,所以只使用两张页表,为此页目录表中的其它项被置为无效(P=0)。
页表 0 把线性地址空间中的 00000000H—003FFFFFH 映射到物理地址空间中。实例在初始化页表 0 时,使该线性地址空间直接映射到相同地址的物理地址空间,除线性地址空间中页码为 000B8H 和 000F0H 这两页以外。000B8H 页被映射到页码为 00301H 的物理页,而 000F0H 页被映射到页码为 000B8H 的物理页。
页表 1 把线性地址空间中的 00400000H—007FFFFFH 映射到物理地址空间中。实例在初始化页表 1 时,似乎使该线性地址空间直接映射到相同地址的物理地址空间,但是处理对应线性地址空间中 00402H 的表项被另外设置外,其它表项中的 P 位为 0,也即表示对应物理页不存在。初始化后,页表 1 的第 2 项把线性地址空间中的 00402H 页映射到页码为 00303H 的物理页,也就是存放部分演示代码的指定内存区域.下面用一个粗略的示意图展示:
中间为页目录表(其表项为PDT),两边为页表(其表项为PTE),a,b是根据相应的线性地址算出的索引值,其对应索引值里面存了一个地址(为了简便没有设置部分属性(or运算)),指向一个4KB的页(1000h=4096=4KB,所以需要ADD eax ,1000h)。对于图,自己最好把数据填上去,这样更直观,这里为了方便,我没有填上数据。
TempCodeSeg SEGMENT PARA USE16 ;临时任务的代码段
ASSUME CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual PROC FAR
cld ;为演示在启用分页机制后执
mov ax,SCode_Sel ;行位于较高线性地址空间中
mov ds,ax ;的代码作准备
mov ax,TPSCode_Sel
mov es,ax
mov si,OFFSET SBegin
mov di,si
mov cx,MLen ;把分页演示代码复制到预定
rep movsb ;内存
JUMP16 DemoCode_Sel,DemoBegin
上面这段代码 将演示代码送到TPSCode段选择子所指示的地址(PhSC_AD).
mov eax,PDT_AD
mov cr3,eax
mov eax,cr0
or eax,80000000h
mov cr0,eax
jmp SHORT PageE
PageE: mov ax,DemoData_Sel
mov ds,ax
mov si,OFFSET Mess
JUMP16 LoCode_Sel,SBegin
根据图片左边分析可得知: 线性地址空间中的 00402H 页映射到页码为 00303H 的物理页,所以接下来跳转到线性地址00402的页,这样就映射到了演示代码的页了。
mov di,(PhVB_AD SHR 12)*4
mov DWORD PTR es:[di],MPVB_AD or (USS+RWW+PL)
mov di,(LoVB_AD SHR 12)*4
mov DWORD PTR es:[di],PhVB_AD or (USU+RWR+PL)
对于这段代码,书上用的不是 or 而是加,这里我觉得or 更正确,这里低位为0,所以加和or 没有区别,但是如果低位不是0这样就不对了。(这是我自己的分析,如果不对还请指教)。
(3)启动分页管理机制
在建立好页映射表后,启用分页机制所要做的操作是简单的,只要把控制寄存器 CR0 中的最高位,也就是 PG 位置 1。具体指令如下:
or eax,80000000h
mov cr0,eax
jmp SHORT PageE
PageE:
在启用分页机制前,线性地址就是物理地址;在启用分页机制后,线性地址要通过分页机制的转换,才成为物理地址。尽管使用一
条转移指令,可清除预取队列,但随后在取指令时使用的线性地址就要经过分页机制转换才成为物理地址。为了顺利过渡,在启用分页机制之后的过渡代码段,仍要维持线性地址等同于物理地址。为了作到这一点,在建立也映射表时,必须使实现过渡的代码所在的线性地址空间页映射到具有相同地址的物理地址空间页。实例中页表 0 就做到了这一点。
(4)关闭分页管理机制
只要把控制寄存器 CR0 中的 PG 位清 0,便关闭了分页机制。在这一过渡阶段,也要保持地址转换前后的一致。
(5)地址转换的演示
在启用分页机制之后,就转移到位于线性地址空间中 00402000H 处开始的代码,该部分代码的功能是显示提示信息"Page is OK!"。实际上这部分代码存放在从物理地址 00303000H 开始的物理内存区域中,是在初始化时被移到此区域的。
在显示提示信息时,要把显示的 ASCII 字符和显示属性填到线性地址空间中 000F0000H 开始的区域中,而不是 000B8000H 开始的区域。从初始化时建立的映射表可见,线性地址空间中的 000F0H 页,被映射到物理地址空间中的 000B8H 页。所以,向线性地址空间中的 000F0H 页写,实际上是向物理地址空间中的 000B8H 页写,也就是真正显示。
(6)页级保护的说明
在进入保护模式之后,特权级一直是 0,所以,无论系统级和用户级页,无论只能读/执行,还是读/执行/写,总是可进行各种形式
的访问。
程序运行结果展示: