其实分页机制不难,但是不做记录的话又感觉会忘,所以还是做点记录。
分页的主要目的在于实现虚拟存储器。线性地址中任意一个页都能映射到物理地址中的任何一个页,这使得内存管理变得相当灵活。
看图:
在未打开分页机制时,线性地址等同于物理地址,于是可以认为,逻辑地址通过分段机制直接转换成物理地址。但当分页开启时,分段机制将逻辑地址转换成线性地址,线性地址再通过分页机制转换成物理地址。
线性地址通过分页机制进行转换时,先是从由寄存器cr3指定的页目录中根据线性地址的高10位得到页表地址,然后在页表中根据线性地址的第12到21位得到物理页首地址,将这个首地址加上线性地址低12位便得到了物理地址。
分页机制是否生效的开关位于cr0的最高位PG位。如果PG=1,则分页机制生效。当我们准备好了页目录表和页表,并将cr3指向页目录表之后,只需要置PG位,分页机制就开始工作了。
关于页目录(PDE),页表和cr3这里就不记录了,网上的资料一大堆,主要是看对页目录和页表以及cr3的操作。
下面就通过改变地址映射关系执行同一个线性地址处的模块得到两次不同结果这个例子来体会分页机制。
1.分页机制的启动
代码:
; 启动分页机制 --------------------------------------------------------------
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
; 分页机制启动完毕 ----------------------------------------------------------
这段代码第一部分是; 根据内存大小计算应初始化多少PDE以及多少页表,关于这个的具体实现可以参考:根据内存大小计算页表个数,把页表的个数计算好后存储在[PageTableNumber]中,因为PageTableNumber是定义在数据段中的,所以[PageTableNumber]的段是数据段。
第二部分初始化页目录,填充页目录包括填充页目录的结构和页目录的表项,先填充页目录结构,页目录结构包括页表的基址和一些属性位,将页目录结构放在eax中,通过stosd指令写入内存;页目录的表项就是页表,每个表项4字节长,其中存的就是每个页表的地址,一个页表是4KB(4096)大小。代码中所有页表在内存中是连续的,所以每个表项的值相差4096,注意,是每个表项的值相差4096,不是表项的地址相差4096。
第三部分初始化页表,每个页表1024个PTE,将PTE个数算出后,先将页表属性写入页表开始地址,然后写入每个PTE项(每个PTE项指向的是一个页的地址,书中每页同样是连续的)。
页目录和页表初始化完成后,将cr3指向页目录表,然后设置cr3的PG位,这样,分页机制就启动完成了。
2.切换页目录,改变地址映射关系
; 切换页表 ------------------------------------------------------------------
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
; ---------------------------------------------------------------------------
这里初始化页目录和页表和启动分页机制一样,主要的代码就是这段:
; 在此假设内存是大于 8M 的
mov eax, LinearAddrDemo ;LinearAddrDemo equ 00401000h
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 ;PageTblBase1 equ 211000h ; 页表开始地址:2M + 64K + 4K
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
这段代码是改变线性地址LinearAddrDemo对应的物理地址的语句。改变后,LinearAddrDemo将不再对应ProcFoo,而是对应ProcBar。
线性地址的各部分功能如图所示,
mov eax, LinearAddrDemo
shr eax, 22
取线性地址的高10位,即页表在PDE(页目录表)中的索引,
mov ebx, 4096
mul ebx
mov ecx, eax
因为一个页表是4K大小,所以乘以4096来算出对应页表的地址,
mov eax, LinearAddrDemo
shr eax, 12
and eax, 03FFh ; 1111111111b (10 bits)
取中间10位,即对应页在页表中的表项号,
mov ebx, 4
mul ebx
add eax, ecx
add eax, PageTblBase1
因为每页大小4K,所以乘以4,这算出来是一个偏移,然后再加上页表的首地址就算出要修改的地址。
mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW
将这个地址写入ProcBar就成功修改了线性地址LinearAddrDemo对应的物理地址。