前面我们已经学习了操作系统的分页机制,现在就来体验一下分页的妙处。
如果我们有一个自己写的程序,现在复制一份到其它目录下,再对两个程序同时调试,这时候会发现,其中的寄存器和变量地址都是一样的。但是,在这样的情况下,它们的功能并没有因为“一样的”地址而混淆却各司其职,这就是分页机制的功劳,那么我们就来模拟这一效果吧。
先执行某个线性地址的模块,然后通过改变cr3来转变地址映射关系,再执行同一个线性地址处的模块,由于地址映射已经改变,所以两次得到的应该是不同的输出。
映射关系转换前的情形如下图所示:
开始,我们让ProcPagingDemo中的代码实现向LinearAddrDemo这个线性地址的转移,而LinearAddrDemo映射到物理地址空间中的ProcFoo处。我们让ProcFoo打印出红色的字符串Foo,所以执行时我们应该可以看到红色的Foo。随后我们改变地址映射关系,让其变化成如下形式:
页目录表和页表的切换让LinearAddrDemo映射到ProcBar(物理地址空间)处,所以当我们再一次调用过程ProcPagingDemo时,程序将转移到ProcBar处执行,我们将看到红色的字符串Bar。
下面我们就主要的代码来进行讨论。首先,我们将要用到另外的一套页目录表和页表,所以原先的页目录段和页表段已经不够用了。上一个程序用两个段分别存放页目录表和页表,是为了更加直观和形象。现在可以把它们放到同一个段中,同时把增加的一套页目录和页表也放到这个段中。
为了操作方便,增加一个段,其线性地址空间为0~4GB。由于分页机制启动之前线性地址等同于物理地址,所以通过这个段可以方便地存取特定的物理地址。段定义如下:
LABEL_DESC_FLAT_C: Descriptor 0,0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0,0fffffh, DA_DRW|DA_LIMIT_4K ; 0~4G
…
SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT
SelectorFlatRW equ LABEL_DESC_FLAT_RW- LABEL_GDT
DA_CR是可执行可读代码段;DA_DRW是可读写数据段;DA_32表示32位段;DA_LIMIT_4K表示段界限粒度为4KB,之所以可以表示4GB就是由2^20*4KB得到的。
之所以用了两个描述符来描述这个段,是因为不仅仅要读写这段内存,而且要执行其中的代码,而这对描述符的属性要求时不一样的(一个是数据段,一个是代码段)。这两个段的段基址都是0,长度都是4GB。
接下来将启动分页的代码做如下的修改:
SetupPaging:
; 根据内存大小计算应初始化多少PDE以及多少页表
……
.no_remainder:
mov [PageTableNumber], ecx ; 暂存页表个数
; 为简化处理, 所有线性地址对应相等的物理地址. 并且不考虑内存空洞.
; 首先初始化页目录
mov ax, SelectorFlatRW
mov es, ax
mov edi, PageDirBase0 ; 此段首地址为 PageDirBase0
xor eax, eax
mov eax, PageTblBase0 | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .1
; 再初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE个数 = 页表个数 * 1024
mov edi, PageTblBase0 ; 此段首地址为 PageTblBase0
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .2
mov eax, PageDirBase0
mov cr3, eax
原来我们并没有把页表个数保存起来,但是现在情况发生了变化,手上有两个页表,为了初始化另外的页表时方便起见,在这里增加了一个变量PageTableNumber,页表的个数就在这里。
在整个初始化页目录和页表的过程中,ES始终未SelectorFlatRW。这样,想存钱物理地址的时候,只需要将地址赋值给EDI,那么ES:EDI指向的就是相应的物理地址。为了让程序结构看起来更清晰,我们把所有与分页有关的内容全都放进一个新建的函数PagingDemo中。
在本例中会用到几个函数,但实际上就是内存中指定的地址,我们把它们定义为常量:
LinearAddrDemo equ 00401000h
ProcFoo equ 00401000h
ProcBar equ 00501000h
ProcPagingDemo equ 00301000h
因为程序开始时LinearAddrDemo指向ProcFoo并且线性地址和物理地址是对等的,所以这两个函数的地址相等。而ProcFoo和ProcBar应该是指定的物理地址,所以LinearAddrDemo也应该是指定的物理地址。也正因为如此,我们使用它们时应该确保使用的是FLAT段(线性地址对应物理地址),即段选择子是SelectorFlatC或者SelectorFlatRW。
为了将我们的代码放置在ProcFoo和ProcBar这两个地方,我们先写两个函数,在程序运行时将这两个函数的执行码赋值过去就可以了。
ProcPagingDemo要调用FLAT段中的LinearAddrDemo,所以如果不想使用段间转移,我们需要把ProcPagingDemo也放进FLAT段中。我们需要写一个函数,然后把代码复制到ProcPagingDemo处。
将代码填充进这些内存地址的实现如下:
; 测试分页机制 ------------------------------------------------------
PagingDemo:
mov ax, cs
mov ds, ax
mov ax, SelectorFlatRW
mov es, ax
push LenFoo
push OffsetFoo
push ProcFoo
call MemCpy
add esp, 12
push LenBar
push OffsetBar
push ProcBar
call MemCpy
add esp, 12
push LenPagingDemoAll
push OffsetPagingDemoProc
push ProcPagingDemo
call MemCpy
add esp, 12
mov ax, SelectorData
mov ds, ax ; 数据段选择子
mov es, ax
call SetupPaging ; 启动分页
call SelectorFlatC:ProcPagingDemo
call PSwitch ; 切换页目录,改变地址映射关系
call SelectorFlatC:ProcPagingDemo
ret
; -------------------------------------------------------------------
其中用到了名为MemCpy的函数,它复制三个过程到指定的内存地址,类似于C语言中的memcpy。但有一点不同,它假设源数据放在DS段中,而目的段在ES中。所以在函数的开头对ES和DS进行了赋值。MemCpy函数的定义如下:
; -------------------------------------------------------------------
; 内存拷贝,仿 memcpy
; -------------------------------------------------------------------
; void* MemCpy(void* es:pDest, void* ds:pSrc, int iSize);
; -------------------------------------------------------------------
MemCpy:
push ebp
mov ebp, esp
push esi
push edi
push ecx
mov edi, [ebp + 8] ; Destination对应调用处Proc*
mov esi, [ebp + 12] ; Source对应调用处offset*
mov ecx, [ebp + 16] ; Counter对应调用处Len*
.1:
cmp ecx, 0 ; 判断计数器
jz .2 ; 计数器为零时跳出
mov al, [ds:esi] ; ┓
inc esi ; ┃
; ┣ 逐字节移动
mov byte [es:edi], al ; ┃
inc edi ; ┛
dec ecx ; 计数器减一
jmp .1 ; 循环
.2:
mov eax, [ebp + 8] ; 返回值
pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp
ret ; 函数结束,返回
; MemCpy 结束--------------------------------------------------------
[ebp + 8]是对应的调用处最后一个push的参数,因为函数开头push了ebp,而且调用时call指令会push一下CS和IP,所以此处执行了+8的操作。
其中,被复制的三个过程的代码如下:
PagingDemoProc:
OffsetPagingDemoProc equ PagingDemoProc - $$
mov eax, LinearAddrDemo
call eax
retf
LenPagingDemoAll equ $ - PagingDemoProc
foo:
OffsetFoo equ foo - $$
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'F'
mov [gs:((80 * 17 + 0) * 2)], ax ; 屏幕第 17 行, 第 0 列。
mov al, 'o'
mov [gs:((80 * 17 + 1) * 2)], ax ; 屏幕第 17 行, 第 1 列。
mov [gs:((80 * 17 + 2) * 2)], ax ; 屏幕第 17 行, 第 2 列。
ret
LenFoo equ $ - foo
bar:
OffsetBar equ bar - $$
mov ah, 0Ch ; 0000: 黑底 1100: 红字
mov al, 'B'
mov [gs:((80 * 18 + 0) * 2)], ax ; 屏幕第 18 行, 第 0 列。
mov al, 'a'
mov [gs:((80 * 18 + 1) * 2)], ax ; 屏幕第 18 行, 第 1 列。
mov al, 'r'
mov [gs:((80 * 18 + 2) * 2)], ax ; 屏幕第 18 行, 第 2 列。
ret
LenBar equ $ - bar
下面是切换页表目录的核心函数PSwitch:
; 切换页表 ----------------------------------------------------------
PSwitch:
; 初始化页目录
mov ax, SelectorFlatRW
mov es, ax
mov edi, PageDirBase1 ; 此段首地址为 PageDirBase1
xor eax, eax
mov eax, PageTblBase1 | PG_P | PG_USU | PG_RWW
mov ecx, [PageTableNumber]
.1:
stosd
add eax, 4096 ; 为了简化, 所有页表在内存中是连续的.
loop .1
; 再初始化所有页表
mov eax, [PageTableNumber] ; 页表个数
mov ebx, 1024 ; 每个页表 1024 个 PTE
mul ebx
mov ecx, eax ; PTE个数 = 页表个数 * 1024
mov edi, PageTblBase1 ; 此段首地址为 PageTblBase1
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一页指向 4K 的空间
loop .2
; 在此假设内存是大于 8M 的
;这段代码在求LinearAddrDemo在分页后的物理地址
mov eax, LinearAddrDemo
shr eax, 22
mov ebx, 4096
mul ebx
mov ecx, eax
mov eax, LinearAddrDemo
shr eax, 12
and eax, 03FFh ; 1111111111b (10 bits)
mov ebx, 4
mul ebx
add eax, ecx
add eax, PageTblBase1
;给LinearAddrDemo重新赋值
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
;设置cr3
mov eax, PageDirBase1
mov cr3, eax
jmp short .3
.3:
nop
ret
; -------------------------------------------------------------------
该函数前面部分与SetupPaging一样,都是完成初始化页目录表和页表的过程,只是后面又增加了改变线性地址LinearAddrDemo对应的物理地址的语句。改变以后,LinearAddrDemo将不再对应ProcFoo,而是对应ProcBar。在后半部分,我们把cr3的值改成了PageDirBase1,这个切换过程宣告完成。
最后代码的执行效果如下(完整代码chapter3/h/pmtest8.asm):
我们看到执行结果中红色的Foo和Bar,这说明我们的页表切换起作用了。其实这和之前在上一篇中提到的不同进程有相同的地址原理是类似的,也是在任务切换时通过改变cr3的值来切换页目录,从而改变地址映射关系。