页级别的保护类型
对于分页结构的保护分为两种,一种是对于访问权限的检查,一种是对于线性地址是否存在转换进行检查。
访问权限
分页结构中的一些位是用来规定页的访问权限的,例如R/W,U/S以及XD。这些位在不同的访问模式下起着不同的限制作用。对于一个线性地址的访问有两种模式:supervisor-mode以及user-mode。当CPL<3的时候是supervisor-mode,CPL==3是user-mode。下面这些规则是Intel手册中列举出来的不同的访问权限:
- supervisor-mode
- 读数据
- 可以从任何线性地址读取数据
- 可以从任何线性地址读取数据
- 写数据
- CR0.WP==0,可以向任何线性地址写数据
- CR0.WP==1,只能向地址转换过程中使用的分页结构中R/W==1的线性地址写数据
- 执行
- 32-Bit Paging或者IA32_EFER.NXE==0,访问权限依赖于CR4.SEMP
- CR4.SEMP==0,可以执行任何线性地址中的指令
- CR4.SEMP==1,只能执行转换过程中使用的分页结构的U/S==0的线性地址中的指令
- PAE Paging或者IA-32e Paging并且IA32_EFER.NXE==1,访问权限也依赖于CR4.SEMP
- CR4.SEMP==0,只能执行转换过程中使用的分页结构的XD==0的线性地址中的指令
- CR4.SEMP==0,只能执行转换过程中使用的分页结构的U/S==0并且XD==0的现行地址中的指令
- 32-Bit Paging或者IA32_EFER.NXE==0,访问权限依赖于CR4.SEMP
- 读数据
- user-mode
- 读数据
- 只能读取转换过程中使用的分页结构的U/S==1的线性地址中的数据
- 只能读取转换过程中使用的分页结构的U/S==1的线性地址中的数据
- 写数据
- 只能向转换过程中使用的分页结构的U/S==1并且R/W==1的线性地址中写数据
- 只能向转换过程中使用的分页结构的U/S==1并且R/W==1的线性地址中写数据
- 执行
- 32-Bit Paging或者IA32_EFER.NXE==0,可以执行地址转换过程中使用的分页结构的U/S==1的线性地址中的指令
- PAE Paging或者IA-32e Paging并且IA32_EFER.NXE==1,可以执行地址转换过程中分页结构的U/S==1并且XD==0的线性地址中的指令
- 读数据
没有地址转换
另一种检查是检查线性地址在现有的分页结构中有没有转换,所谓没有转换就是线性地址在转换过程中使用的分页结构的P==0或者是保留位被设置。
#PF异常
如果对于一个线性地址的转换过程中违反了上述两种检查,那么就会触发#PF异常。下图是#PF的error code,它描述了#PF的类型:
- P:
- 0:异常是由于不存在的页引起的
- 1:异常是由于违反了Page-Level保护引起的
- W/R:
- 0:异常是由于读访问引起的
- 1:异常是由于写访问一起的
- U/S:
- 0:异常是由于supervisor-mode引起的
- 1:异常是由于user-mode引起的
- RSVD:
- 0:异常不是由于保留位被设置引起的
- 1:异常是由于某些分页结构的保留位被设置引起的
- I/D:
- 0:异常不是由于取指引起的
- 1:异常是由于取指引起的
例子
这里的例子主要是模拟由于没有地址转换导致#PF异常,并且在#PF异常处理程序中修改错误的情况。
首先要准备一段可执行的代码,这段代码要拷贝到能够引起#PF异常的线性地址中以便测试使用。
code_start:
movl $puts, %ebx
movl $dump_pae_page, %edx
movl $msg3, %esi
call *%ebx
movl $println, %eax
call *%eax
movl $0x400000, %esi
call *%edx
movl $println, %eax
call *%eax
jmp .
code_end:
这段代码也是通过dump_pae_page来打印一个线性地址的转换过程。
接下来要将这段代码拷贝到测试的地址中,我们使用了0x400000地址为测试地址,不过拷贝的过程要在开启分页之前进行,否则拷贝的过程就会导致#PF。
/*
* copy code to 0x400000
*/
movl $code_start, %esi
movl $0x400000, %edi
movl $code_end, %ecx
subl %esi, %ecx
rep movsb
测试代码准备好了,调用init_pae32_paging来初始化PAE Paging结构,这个结构和
《PAE Paging》中的分页结构相同,不过就是在0x400000地址转换过程中PDE的保留位被设置了,PTE的P标志被清0,这两种情况都会导致#PF。
###############################################################
# init_pae32_paging:
init_pae32_paging:
movl $PDPT_BASE, %esi
call clear_4k_page
movl $0x201000, %esi
call clear_4k_page
movl $0x202000, %esi
call clear_4k_page
# PDPTE[0]
movl $PDPT_BASE, %esi
movl $0x201001, (%esi)
movl $0x00, 4(%esi)
# PDE[0] 0x00 ~ 0x1fffff (2M)
# PDE[1] 0x200000 ~ 0x3fffff (2M)
# PDE[2] 0x400000 ~ 0x400fff (4K)
movl $0x201000, %esi
movl $0x00, %eax
movl $0x00000087, (%esi, %eax, 8)
movl $0x00, 4(%esi, %eax, 8)
inc %eax
movl $0x00200087, (%esi, %eax, 8)
movl $0x00, 4(%esi, %eax, 8)
inc %eax
movl $0x00202007, (%esi, %eax, 8)
movl $0x70000000, 4(%esi, %eax, 8)
# PTE[0] 0x400000 ~ 0x400fff (4K)
movl $0x202000, %esi
movl $0x00400000, (%esi)
/*
movl xd_bit, %eax
movl %eax, 4(%esi)
*/
ret
然后就是编写#PF处理代码了,这里能够修改线性地址转换过程中由于数据结构中保留位被设置以及P标志为0的异常。
###############################################################
# PF_handler():
PF_handler:
jmp do_PF_handler
pf_msg1: .asciz "----> Now, enter #PF handler, occur at: 0x"
pf_msg2: .asciz "----> Error Code: 0x"
pf_msg3: .asciz "----> fixup <----"
do_PF_handler:
popl %esi
pushl %ecx
pushl %edx
pushl %ebx
movl %esi, %ebx
# puts error address
movl $pf_msg1, %esi
call puts
movl %cr2, %ecx
movl %ecx, %esi
call print_int_value
call println
# puts error code
movl $pf_msg2, %esi
call puts
movl %ebx, %esi
call print_int_value
call println
call get_maxphyadd
pushl %ecx
leal -64(%eax), %ecx
negl %ecx
movl $-1, %edi
shll %cl, %edi
shrl %cl, %edi
popl %ecx
# fix error
get_pdpte:
movl %ecx, %eax
shrl $30, %eax
andl $0x03, %eax
movl $PDPT_BASE, %ebx
# PDPTE
pushl %edi
movl 4(%ebx, %eax, 8), %edx
notl %edi
andl %edx, %edi
jz get_pdpte_low
popl %edi
andl %edi, %edx
movl %edx, 4(%ebx, %eax, 8)
jmp do_PF_handler_done
get_pdpte_low:
popl %edi
movl (%ebx, %eax, 8), %edx
btsl $0, %edx
movl %edx, (%ebx, %eax, 8)
jnc do_PF_handler_done
get_pde:
#PDE
movl %edx, %ebx
andl $0xfffff000, %ebx
movl %ecx, %eax
shrl $21, %eax
andl $0x01ff, %eax
pushl %edi
movl 4(%ebx, %eax, 8), %edx
notl %edi
andl $0x7fffffff, %edi
andl %edx, %edi
jz get_pde_low
popl %edi
orl $0x80000000, %edi
andl %edi, %edx
movl %edx, 4(%ebx, %eax, 8)
jmp do_PF_handler_done
get_pde_low:
popl %edi
movl (%ebx, %eax, 8), %edx
btsl $0, %edx
movl %edx, (%ebx, %eax, 8)
jnc do_PF_handler_done
bt $7, %edx
movl %edx, (%ebx, %eax, 8)
jc do_PF_handler_done
get_pte:
#PTE
movl %edx, %ebx
andl $0xfffff000, %ebx
movl %ecx, %eax
shrl $12, %eax
andl $0x01ff, %eax
pushl %edi
movl 4(%ebx, %eax, 8), %edx
notl %edi
andl $0x7fffffff, %edi
andl %edx, %edi
jz get_pte_low
popl %edi
orl $0x80000000, %edi
andl %edi, %edx
movl %edx, 4(%ebx, %eax, 8)
jmp do_PF_handler_done
get_pte_low:
popl %edi
movl (%ebx, %eax, 8), %edx
btsl $0, %edx
movl %edx, (%ebx, %eax, 8)
jc do_PF_handler_done
do_PF_handler_done:
movl $pf_msg3, %esi
call puts
call println
call println
popl %ebx
popl %edx
popl %ecx
iret
最后在开启分页之后,首先对线性地址0x400000的转换过程进行打印,然后逃转到0x400000地址去执行其中的指令,由于0x400000地址中的指令也是打印对于线性地址0x400000的转换过程,所以执行结构应该看到两次对于0x400000线性地址的转换过程的打印结构。
movl $msg3, %esi
call puts
call println
movl $0x400000, %esi
call dump_pae_page
call println
ljmp $KERNEL_CODE32_SELECTOR, $0x400000
最后的执行结构:
从执行结果可以看出,从第一次打印的结构来看,对线性地址0x400000的转换中使用的PDE的保留位被设置了,PTE的P标志被清0,所以没有对线性地址0x400000的转换。接下来的两次#PF正是由于跳转到0x400000地址去执行指令导致的。
第一次#PF是由PDE的保留位被设置导致的,从错误代码中可以看到,0x19表示P==1,RSVD==1,I/D==1,这说明异常是由于违反了页级别的保护引起的,具体是由于一些分页结构的保留位被设置引起的,同时是在取指令时引起的。接下来#PF处理过程修复了这个错误。
第一次#PF异常处理结束后,会重现执行跳转代码,但是这是线性地址0x400000仍然没有转换,所以导致了第二次#PF异常。
第二次#PF是由PTE的P标记被清0导致的,从错误代码中可以看出,0x10表示P==0,I/D==1,这说明异常是由于页不存在导致的,同时是在取指令时引起的。接下来#PF处理过程也修改了这个错误。
两种错误都被修正之后,代码可以正确的跳转到0x400000地址去执行了,执行的结构就是打印出线性地址0x400000被转换的过程,从结果上看之前的错误都被#PF处理过程修复了。
参考
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》
《x86/x64体系探索及编程》