概论
当我面对着五百多行的汇编时, 我的内心几乎是崩溃的。
幸运的是,最终我还是明白了。
前言
首先要先感谢这两篇博客,给了我莫大的帮助。 汇编真的是一门非常有意思的语言,每次都以为自己学明白了,结果每每再看一次,却都会发现自己还是有地方不懂,根据问题再去搜索,思考一番,每每都有新的收获。
正文
在pmtest8里,我们对分页机制进行了切换页目录的操作。简单的说,对于同一个线性地址,根据不同的页表会映射到不同的物理地址上。那么如果在内存空间中,不同的物理地址上存储了不同的程序,那么同一个线性地址的调用就会调用不同的函数。那么为了能达到这种效果,就是在两次分页机制启动时,加载了不同的页表即可。
所以PMTEST8的本质内容就是在32位保护模式下,启动2次分页机制,这2次分页机制中,cr3所指定的页表基址不同,但所调用的线性地址相同。在这两次的同一线性地址的调用中,调用了两个不同的函数。
在学习PMTEST8中,切换页表的操作让我感到十分困惑,这也暴露除了我汇编基础不扎实的弱点。
想要看懂PMTEST8,首先要搞明白这几个变量
PageDirBase0 equ 200000h ; 页目录开始地址: 2M
PageTblBase0 equ 201000h ; 页表开始地址: 2M + 4K
PageDirBase1 equ 210000h ; 页目录开始地址: 2M + 64K
PageTblBase1 equ 211000h ; 页表开始地址: 2M + 64K + 4K
这些代表了两个PDE与PTE的所在地址。
LinearAddrDemo equ 00401000h
ProcFoo equ 00401000h
ProcBar equ 00501000h
ProcPagingDemo equ 00301000h
这四个变量显示了线性地址的值, 以及接下来三个变量都各自对应三个程序的物理地址。
接着,我们通过以下程序将相对应的程序写到上面所标出的物理地址中。那么在真实的物理地址内,程序就被写好了
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
第一个分页机制启动
现在,我们根据MemSize的值来确定PDE的个数,然后初始化第一个页表
; 启动分页机制 --------------------------------------------------------------
SetupPaging:
; 根据内存大小计算应初始化多少PDE以及多少页表
xor edx, edx
mov eax, [dwMemSize]
mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一个页表对应的内存大小
div ebx
mov ecx, eax ; 此时 ecx 为页表的个数,也即 PDE 应该的个数
test edx, edx
jz .no_remainder
inc ecx ; 如果余数不为 0 就需增加一个页表
.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
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
其实上面这段程序不重要,我们首先要明白一个PTE表的PTE有4B这么大,有1024项,所以一个PTE表有4KB这么大。 现在我们来看线性地址与第一个页表的首地址
PageDirBase0 equ 200000h
LinearAddrDemo equ 00401000h
我们将线性地址拆成32位的二进制码,然后根据分页机制的可以得出结果为偏移一个PDE后再偏移一个PTE
在本例中,为了方便起见,依旧是采取线性地址=物理地址的映射方法。
也就是说我们第一个偏移PDE过后,物理页的起始地址为400000H,即4MB。
为何是4MB呢? 因为我们的映射方法是线性地址等于物理地址,而我们在PDE表中偏移了一个PDE,那么说明我们物理地址之前存在着一个PTE表所对应的物理页表们,一个物理页表有4KB,一个PTE表是1024项,那么总共的大小就是4MB。 然后回到主题,我们的PTE表也偏移了一个PTE,也就是说我们所需要的物理表距离400000H偏离了一个物理表的大小。
一个物理表为4KB,所以我们得到的转换地址同样为401000H。即Foo方法所在的物理地址。
切换页表
现在我们来着重看切换页表的代码
; 切换页表 ------------------------------------------------------------------
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 的
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
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
mov eax, PageDirBase1
mov cr3, eax
jmp short .3
.3:
nop
ret
; ---------------------------------------------------------------------------
这段代码可以大体上理解为初始化第二个页表,同样这里也是线性地址=物理地址的映射。
唯一不同的是在处理完初始化页表的内容后,他将本应该指向00401000H的地址内的值改成了另外一个物理地址。
怎么理解呢?
假设我们第二个页表的初始化后不去修改,那么如果我们现在依旧通过00401000这个线性地址来获得物理地址,那么首先是从页表基址来偏移一个PDE和PTE,因为偏移了一个PDE,所以我们页表的起始地址就是从211000加了4KB变成了212000.然后再偏移一个PTE,所以就再加了4B变成了212004H。 也就是说,对于第二个页表的来说,他所偏移一个PTE和一个PDE在他所在段的偏移地址最终计算得到是212004H。
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
所以这段代码的本质就是将eax的值变成212004
然后再将通过
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
这句代码修改了[es:eax]处内的值,使得原来的物理地址00401000变成了00501000.
那么我们在第二次分页机制启动后,我们再一次通过线性地址调用程序,就从原来的
f(线性地址 : 00401000 ) = 物理地址: 00401000
变成了
f(线性地址 : 00401000 ) = 物理地址: 00501000
所以自然就达到了通过同样的线性地址,却访问到了不同物理地址的效果,使得应用层只需要将线性地址提供给操作系统后,这种分页机制使得应用层屏蔽了物理层的物理地址。