分页
分页是将线性地址转换成物理地址的机制,当关闭分页的时候线性地址直接映射成物理地址,开启分页之后线性地址要经过一系列的转换才能获得物理地址。分页的好处很多,有了分页,OS可以对页面进行调度,这样OS可以实现虚拟内存以及多任务。
将线性地址转换成物理地址的过程中主要关心下面这些方面:
- 线性地址宽度。
- 物理地址宽度。
- 页大小。
- 访问权限。
- 被缓存的能力。
Intel CPU支持很多种分页模式,32-Bit,PAE等等,这些分页模式本质上都是相同的,不同的是它们在上述的各个方面各有不同。这篇文章主要记录了32-Bit Paging模式。
32-Bit Paging模式
32-Bit Paging是最基本的一种分页模式,我们也是通过上面描述地址转换的几个方面来学习32-Bit Paging,但是这里仅仅关心线性地址宽度,物理地址宽度以及页大小,访问权限和缓存能力以后学习。
开启32-Bit Paging
当CR0.PG=1的时候就开启了分页机制,这个时候处在什么样的分页模式还要看CR4.PAE以及IA32_PEER.LME的状态,只有当CR0.PG=1,CR4.PAE=0,IA32_PEER.LME=0的时候才是32-Bit Paging。
这个三寄存器的开启是有一定限制的,不符合限制的开启顺序会触发#GP异常,Intel 手册的4.1.2节专门讲解了这个限制条件。
线性地址宽度
32-Bit Paging的线性地址宽度是32位。
物理地址宽度
32-Bit Paging转换后的物理地址宽度是32位到40位,具体的最大物理地址宽度由两个因素决定,PSE-36以及MAXPHYADDR。
PSE-36是在页大小扩展中使用40位的物理地址宽度,当CPUID.01H:EDX.PSE-36 [bit 17] = 1的时候说明CPU支持PSE-36。
PSE-36仅仅是说明CPU支持40位的物理地址宽度,但是实际能不能达到40位还要看MAXPHYADDR,MAXPHYADDR是CPU支持的物理地址宽度,由CPUID.80000008H:EAX[7:0]来决定,这个值最大是52。
物理地址除了受到这两个因素的影响外还要受到页大小的影响,Intel 手册4.3节有一段注释,大体的意思是除非开启了4-MByte Page,否则物理地址的39:32位是0。如果CPU不支持PSE-36,那么开启4-MByte Page后物理地址的39:32位也是0。当CPU支持PSE-36,并且MAXPHYADDR<40,那么在开启4-MByte Page的情况下,物理地址的39:MAXPHYADDR是0。
从这段注释中可以看出只有开启4-MByte Page的时候才可能使用大于32位的物理地址。
页大小
页大小由CR4.PSE决定,如果CR4.PSE=1则在32-Bit Paging中使用4-MByte Page,否则使用4-KByte Page。但是并不是所有的CPU都支持CR4.PSE,必须通过CPUID来查询。如果CPUID.01H:EDX.PSE [bit 3]=1表明当前CPU支持CR4.PSE,这样可以通过设置CR4.PSE=1来开启4-MByte Page,否则表明CPU在不支持4-MByte Page。
是否开启4-MByte Page是由PDE.PS决定的,当PDE.PS=1时才开启4-MByte Page,否则使用4-KByte Page。
分页机制
分页机制是如何将线性地址转换成物理地址的呢?分页机制采用一个带有层次的页结构,然后将线性地址分为几个部分,每个部分对应的是在相应层次结构中的偏移量,从对应层次结构中获得物理地址,这个地址或是下一个层次结构的地址,或者是映射的实际物理地址。
第一个分页结构的物理地址放在CR3中,所以所有的地址转换都要从CR3寄存器开始。
根据CR4.PSE可以确定也的大小是4-KByte还是4-MByte,这两种也大小在分页层次结构上也是不同的,在地址转换机制上也是不同的。
4-MByte Page
当CR4.PSE=1的时候,32-Bit Paging使用4-MByte Page。
上图是Intel手册中关于4-MByte Page的转换过程。这里只用到了一种分页结构Page Directory。结构中的每一项叫做PDE,PDE的首地址保存在CR3中,下面是CR3和4-MByte Page时PDE的结构:
PDE中也有一项PDE.PS(PDE[7])它也是用来表示是否开启4-MByte Page的,只有CR4.PSE=1 && PDE.PS=1才能启动4-MByte Page。
在4-MByte Page的时候从线性地址转换到物理地址的步骤:
- 通过CR3和线性地址计算出对应的PDE的地址:
- 39:32位是0
- 31:12来自于CR3
- 11:2来自于线性地址的31:22位
- 1:0是0
- 通过PDE和线性地址计算出实际的物理地址:
- 39:32位来源于PDE的20:13位
- 31:22位来源于PDE的31:22位
- 21:0位来源于线性地址的21:0位
4-KByte Page
当CR4.PSE=0或者CR4.PSE=1&&PDE.PS=0的时候,32-Bit Paging使用4-KByte Page。
这是Intel手册中关于4-KByte Page的转换过程,与4-MByte Page相比,多了一个Page Table的分页结构,Page Table结构中的项是PTE,下图是4-KByte Page时CR3, PDE, PTE的结构:
4-KByte Page时线性地址转换成物理地址的步骤:
- 通过CR3和线性地址计算出对应的PDE的地址:
- 39:32位是0
- 31:12位来自于CR3
- 11:2位来自于线性地址的31:22位
- 1:0位是0
- 通过PDE和线性地址计算出PTE的地址:
- 39:32位是0
- 31:12位来自于PDE的31:12位
- 11:2位来自于线性地址的21:12位
- 1:0位是0
- 通过PTE和线性地址计算出物理地址:
- 39:32位是0
- 31:12位来自于PTE的31:12位
- 11:0位来自于线性地址的11:0位
无效的分页结构
PDE和PTE除了有相应结构或者映射的物理地址外还有很多位来表示很多不同的含义,实现很多不同的限制。关系到地址转换的有PDE.P以及PTE.P,PDE.P本PDE保存的Page Table地址是否有效。PDE.P=1表示下一级PageTable地址有效,否则无效。PTE.P表示PTE保存的4-K的页是否有效,PTE.P=1表示它保存的4K的页有效,否则无效。此外PDE和PTE中的保留位必须是0。如果引用的PDE或者PTE的P位是0或者其中的某些保留位是1,就会引发#PF异常。
示例
这里的代码都是根据《x86/x64体系探索及编程》一书的配套代码修改而来的,并且托管到github上。
首先通过CPUID来查询PSE-36以及MAXPHYADDR:
__get_maxphyadd:
pushl %ecx
movl $32, %ecx
movl $0x80000000, %eax
cpuid
cmpl $0x80000008, %eax
jb test_pse36
movl $0x80000008, %eax
cpuid
movzx %al, %ecx
jmp do_get_maxpyhadd_done
test_pse36:
movl $0x01, %eax
cpuid
btl $17, %edx
jnc do_get_maxpyhadd_done
movl $36, %ecx
do_get_maxpyhadd_done:
movl %ecx, %eax
popl %ecx
ret
然后初始化分页结构并且开启PSE:
###############################################################
# init_32bit_paging
# 0x000000-0x3fffff maped to 0x00 page frame using 4M page
# 0x400000-0x400fff maped to 0x400000 page frame using 4K page
init_32bit_paging:
pushl %eax
movl $PDT32_BASE, %eax
movl $0x00000087, (%eax) # set PDT[0]
movl $0x00201001, 4(%eax) # set PDT[1]
movl $PTT32_BASE, %eax
movl $0x00400001, (%eax)
popl %eax
ret
pse_enable:
movl $1, %eax
cpuid
bt $3, %edx
jnc pse_enable_done
movl %cr4, %eax
bts $4, %eax
movl %eax, %cr4
pse_enable_done:
ret
这里设置的分页转换结构很简单,如下图:
转换结构设置完成之后可以开启分页了,并且在分页之后调用dump_page函数打印出两个地址的转换过程,整个32-Bit Paging的测试代码如下:
movl $msg1, %esi
call puts
call get_maxphyadd
movl %eax, %esi
call print_int_decimal
movl $msg2, %esi
call puts
call println
call println
call init_32bit_paging
movl $PDT32_BASE, %eax
movl %eax, %cr3
call pse_enable
movl %cr0, %eax
bts $31, %eax
movl %eax, %cr0
movl $msg3, %esi
call puts
call println
movl $msg4, %esi
call puts
call println
movl $0x00200000, %esi
call dump_page
call println
movl $msg5, %esi
call puts
call println
movl $0x00400000, %esi
call dump_page
代码运行结果:
从图中可以看到bochs模拟器的最大物理地址是40位,线性地址0x00200000通过4M页结构进行转换,0x00400000地址通过4K的页结构进行转换。
值得注意的地方
这里有一个很值得注意的地方,就是在开启CR0.PG=1之后,CPU立即进入分页模式,所以分页结构一定要在这之前准备好并且保存到CR3中。还有就是当CR0.PG=1之后%eip中的线性地址马上会使用分页机制来转换,如果在前面的分页结构没有考虑到这一点很可能把当前执行的代码的线性地址映射到一个不同的地方。在分页前线性地址与物理地址是对应的,例如当前代码在0x9000,那么物理地址也是0x9000,但是分页后,会根据预先设置的分页转换结构将0x9000的线性地址转换成物理地址,如果转换后的物理地址不是0x9000或者是开启分页前没有把代码拷贝到相应的物理地址上,那么分页之后就会找不到继续执行的正确的代码,所以要实现考虑这个因素。
参考
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》
《x86/x64体系探索及编程》