2-9-9-12分页
在10-10-12分页方式下,物理地址最多可达4GB。但随着硬件发展,4GB的物理地址范围已经无法满足要求,Intel在1996年就已经意识到这个问题了,所以设计了新的分页方式。也就是2-9-9-12分页,又称为PAE(物理地址扩展)分页.
在了解2-9-9-12分页之前,应该先明确10-10-12分页到底是怎么来的,这样更有助于理解2-9-9-12分页。
为什么是10-10-12分页
intel实际上是先确定了页的大小,为4KB。那么物理页要求的索引应该是4096,也就是2的12次方,这个12次方实际上就是最后的12。
当初的物理内存比较小,所以4个字节的PTE就够了,又因为页的尺寸是4K,所以一个页能存储1024个 PTE ,也就是2的10次方 第二个10也就确定了。
第一个10同理,四字节的PDE,而页是4096,所以能储存1024个PDE,也就是2的10次方。
为什么是2-9-9-12分页
页的大小是4K,这点是不能动的。那么最后12位依旧是12位。
如果想增大能访问的物理页,需要增大PTE,而考虑到对齐效率高的问题,直接扩展到8个字节,也就是64位。
那么一个PTE有8个字节64位,一个PTT表还是4K,所以这里2的9次方 512即可索引到每一项。第三个9就是这个2的9次方。
同理是第二个9,一个PDE有8个字节64位,一个PDT表还是4K,也是2的9次方可以索引到每一项。
第一个2则是多出来的,做成了新的一级。
2-9-9-12分页结构(PAE,物理地址扩展)
2位2进制数最多就有四个索引,称为PDPTE(Page-Directory-Point-Table Entry):页目录指针表项。每项占8个字节。
在2-9-9-12分页模式下,通过线性地址找到物理地址
在boot.ini文件中修改execute为noexecute。
执行这样一段代码:
#include "stdafx.h"
int main(int argc, char* argv[])
{
char x = 'a';
printf("x的地址:%x\n",&x);
getchar();
return 0;
}
获取到x的线性地址。并进行拆分:
可以看到进程的cr3已经是0x20这样增长,10-10-12的cr3是0x1000增长。
只是多了一层偏移,与10-10-12分页寻址大体差不多。
kd> !dq 09e40300 + 0
kd> !dq 1a781000 + 0
kd> !dq 1a8a4000 + 978
kd> !dq 1aaa9000 + F7C
PDPTE(Page-Directory-Point-Table Entry)
36-63位是保留位,保留位并不意味着填0就行了,而是cpu要用的,但是我们不能用,并且cpu用了还不会告诉我们怎么用的。
12-35位是PDT的基址,把低12位补0,那么0-35位,共36位为PDT基址。
9-11位是拿给操作系统用的,反正cpu不用。
0-11位都是属性。
PDE结构
在2-9-9-12分页模式下,PDE的结构有两种,一种是PS位为1的情况,一种是PS位为0的情况。
PS=1
当PS=1时是大页,35-21位是大页的物理地址,这样36位的物理地址的低21位为0,这就意味着页的大小为2MB,且都是2MB对齐。
2MB哪里来的呢?2-9-9-12,后面的9和12合并成了一个大页,所以是21位,也就是2的21次方,所以是2MB。
PAT位为Page Attribute Table,页属性表,可以看到下面PS为0的时候就有没这一项,原因就是这个位是针对页的,目录当然没有。
PS=0
当PS=0时,35-12位是页表基址,低12位补0,共36位。
PTE结构
PTE中35-12是物理页基址,24位,低12位补0
物理页基址+后12位的页内偏移指向具体数据
XD标志位
AMD中称为NX位,即No Excetion。
在上面说到保留位是cpu自己在用的,并且不会跟我们说怎么用的。在保留位的最高位,也就是63位,实际上是一个标明是否可执行的位。
细心的同学已经发现了,在PDE和PTE结构中,无论是10-10-12分页还是2-9-9-12分页都只有R/W位,也就是可读可写,但是并没有可执行的概念,之前在段的学习中,是有可读可写可执行的概念的,这里显然是不科学的。所以在2-9-9-12分页模式中的最高位标明该物理页的可执行属性。
Intel就做了硬件保护,做了一个不可执行位,XD=1时,表明该物理页是不可执行的,实际上是为了保护数据段,数据就是数据,是不能被当作代码执行的。
这里做一个呼应,在上面的2-9-9-12实验中可以看到,当找到PTE时,他的最高位已经变成了8。
这表明我们存储的’a‘就是a,而不能被执行。有一定的程度预防缓冲区溢出漏洞。
在PAE分页模式下,PDE与PTE的最高位为XD/NX位。
线性地址0xc0600000
在学习10-10-12分页的时候我们知道:PTE的基址是0xc0000000,PDE的基址是0xc0300000。在这两个线性地址能直接找到PTE和PDE,无需依靠Cr3。
但是在2-9-9-12分页模式下,没有指向Cr3的线性地址。但是仍然有一个特殊的线性地址:0xc0600000。
这个线性地址可以通过反编译ntkrnlpa.exe
中的MmIsAddressValid
函数找到。这个函数的作用就是判断线性地址可不可用。
他这里是sub,其实等同于add 0xC0600000
虽然没有线性地址指向cr3,那么有什么对应关系呢?
获取PDPTE指针。
kd> !dq 0ac40340
# ac40340 00000000`18733001 00000000`18674001
# ac40350 00000000`18675001 00000000`185b2001
获取0xc0600000线性地址的PDE
kd> !dq 0ac40340 + 3*8
kd> !dq 185b2000 + 3*8
kd> !dq 185b2000
#185b2000 00000000`18733063 00000000`18674063
#185b2010 00000000`18675063 00000000`185b2063
可以发现c0600000对应的PDE重新指回了PDPTT四个指针。
第三个PDPTE指向了一个PDT表,此表的前四项 指向了PDPTE的每一个元素。
TLB
假设我们有如下代码:
mov eax,dword ptr ds:[12345678]
这行代码会通过线性地址12345678和当前进程的cr3一起去找到对应的物理地址,而整个过程,实际上要访问PDE和PTE甚至是PDPTE,显然不只是四个字节。在2-9-9-12会读24个字节,如果跨页可能更多。
再看下面一段代码:
mov eax,dword ptr ds:[12345ffe]
在0x12345ffe位置下读取4字节的数据,那么实际上会读取到下一个页的地址,那么这里又会牵扯到PDE和PTE的访问和读取数据,这显然是非常损耗效率的。
为了提高效率,只能用一块缓存来做记录。
于是CPU内部做了一个表,来记录这些东西,这个表格是CPU内部的,和寄存器一样快,这个表格叫:TLB(Translation Lookaside Buffer)。
TLB结构
LA存储的是线性地址,PA是对应的物理地址,ATTR是属性,如果是10-10-12分页,那么是PDE和PTE的属性AND起来。
如果是2-9-9-12分页,属性是PDPTE PDE PTE三个属性AND起来的。
实际上有些属性是or起来的,比如XD位,G位。
LRU是用来统计这个线性地址的读写情况的,这是因为TLB这个表在CPU内部,那么他就不会很大,当线性地址存储满了的时候,他就会看LRU的统计情况,把读写的次数比较少的项删除,然后再把新的线性地址项添上。
不同的CPU 这个表的大小不一样。
只要Cr3变了,TLB立马刷新,一核一套TLB。这个也比较好理解,Cr3切换代表进程的切换,进程变了自然线性地址与物理地址的对应关系也失去了意义。
但有这样一个需求:在4GB虚拟空间内,低2G是用户自己的数据,高2G是系统的数据,高2G的内容对于每个进程来说,几乎都是相同的,那么当进程切换的时候,也意味着Cr3变化了,我们不希望TLB中所有的数据都被删除,重建的话耗费很多时间并影响效率,希望有一些高2G空间的线性地址所指引的物理地址项被保留,如何做到这一点呢?
这实际上与PDE和PTE属性中G位相关,G位表明该物理页为全局的,当Cr3切换时,TLB不会清空ATTR(属性)中G位为1的项,这也是为什么很多高线性地址属性的G位都为1的原因。
TLB种类
TLB在X86体系的CPU里的实际应用最早是从Intel的486CPU开始的,在X86体系的CPU里边,一般都设有如下4组TLB(也就是上面说的一套TLB):
第一组:缓存一般页表(4K字节页面)的指令页表缓存(Instruction-TLB)。
第二组:缓存一般页表(4K字节页面)的数据页表缓存(Data-TLB)。
第三组:缓存大尺寸页表(2M/4M字节页面)的指令页表缓存(Instruction-TLB)。
第四组:缓存大尺寸页表(2M/4M字节页面)的数据页表缓存(Data-TLB)。
TLB有多大这个没说,不同cpuTLB的大小不同。
TLB是确实存在的
TLB看不见摸不着,我们怎么知道他是否真实存在呢?
看如下一段代码:
#include "stdafx.h"
#include <Windows.h>
unsigned int g_value=0;
__declspec(naked) void test()
{
__asm
{
push 0x30;
pop fs;
pushad;
pushfd;
//0地址的PDE一般是存在的,这里就直接挂PTE
mov eax,0x600000;
mov ebx,0xc0000000;
//在PAE模式下获取pte的算法,可以逆向MmIsAddressValid找到。
shr eax,0x9;
and eax,0x7ffff8;
//find pte
mov edx,eax;
add edx,ebx;
mov edx,dword ptr ds:[edx];
mov dword ptr ds:[ebx],edx;
mov dword ptr ds:[0],0x12345678;
mov eax,0x700000;
mov ebx,0xc0000000;
shr eax,0x9;
and eax,0x7ffff8;
//find pte
mov edx,eax;
add edx,ebx;
mov edx,dword ptr ds:[edx];
mov dword ptr ds:[ebx],edx;
mov eax,dword ptr ds:[0];
mov g_value,eax;
popfd;
popad;
retf;
}
}
int main(int agrc,char * agrv[])
{
char buf[]={0,0,0,0,0x48,0};
void * p1 = VirtualAlloc((void*)0x600000,0x1000,MEM_COMMIT | MEM_RESERVE ,PAGE_EXECUTE_READWRITE);
void * p2 = VirtualAlloc((void*)0x700000,0x1000,MEM_COMMIT | MEM_RESERVE ,PAGE_EXECUTE_READWRITE);
if(p1 == NULL)
{
if(p2 != NULL) VirtualFree(p2,0x1000,MEM_COMMIT | MEM_RESERVE);
printf("virtual failed p1\n");
return 0;
}
if(p2 == NULL)
{
VirtualFree(p1,0x1000,MEM_COMMIT | MEM_RESERVE);
printf("virtual failed p2\n");
return 0;
}
//这里必须要赋值,不然会0xC0000005,VirtualAlloc分配好了空间,但是当你使用的时候才挂物理页。
*((unsigned int *)p1)=0x100;
*((unsigned int *)p2)=0x200;
printf("%X\n",test);
//eq 8003f048 0040ec00`00081005
__asm
{
call fword ptr buf;
push 0x3b;
pop fs;
};
printf("%X\n",g_value);
VirtualFree(p1,0x1000,MEM_COMMIT | MEM_RESERVE);
VirtualFree(p2,0x1000,MEM_COMMIT | MEM_RESERVE);
return 0;
}
我们将0x600000线性地址对应的物理页挂在0地址上,并且将值改为0x12345678(mov dword ptr ds:[0],0x12345678),然后再将0地址挂上0x700000的物理页,这时候由于0x700000位置上的值是200(((unsigned int )p2)=0x200),那么如果没有TLB,下次cpu寻址的时候0位置上的值应该为200。但运行结果却为:
说明确实有缓存帮我们存储了地址,当第一次寻址的时候已经将linear和对应的物理地址储存到TLB中。
那么如何取到最新的值呢,也就是我们想取到0地址挂上0x700000对应物理页后的值,这里最好的办法就是刷新Cr3,前面我们也提到了Cr3的切换会将TLB清空(G位为1除外)。
mov eax,cr3;
mov cr3,eax;
TLB刷新,这样可以成功读取到最新的数据,而不是老的缓存。
全局页体验
那如果我们此时将PTE属性设置为全局,更新Cr3还能刷新我们这行缓存吗?
or edx,0x100;
即便刷新了缓存,但是由于已经变成全局物理页,所以TLB不会删除地址0这一项。
INVLPG指令
这个指令可以直接删除TLB的某一项缓存,所以我们现在可以通过该指令强行删除全局页缓存。
invlpg dword ptr ds:[0];
中断与异常
中断
中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知,CPU“有事情需要处理”,因此又叫中断请求(Interrupt Request)。
中断请求的目的是希望CPU暂时停止执行当前正在执行的程序,转去执行中断请求所对应的中断处理例程(中断处理程序在哪有IDT表决定)
80×86有两条中断请求线:
- 非屏蔽中断线,称为NMI(NonMaskable Interrupt)
- 可屏蔽中断线,称为INTR(Interrupt Require)
非可屏蔽中断
当非可屏蔽中断产生时,CPU在执行完当前指令后会里面进入中断处理程序。
非可屏蔽中断不受EFLAG寄存器中IF位的影响,一旦发生,CPU必须处理。
非可屏蔽中断处理程序位于IDT表中的2号位置。
找一下2号中断执行代码,这里忘了就回去看TSS,任务门。
kd> uf 8053f3fc
nt!KiTrap02:
8053f3fc fa cli
8053f3fd ff3540f0dfff push dword ptr ds:[0FFDFF040h]
8053f403 a13cf0dfff mov eax,dword ptr ds:[FFDFF03Ch]
8053f408 8a685f mov ch,byte ptr [eax+5Fh]
8053f40b 8a485c mov cl,byte ptr [eax+5Ch]
8053f40e c1e110 shl ecx,10h
8053f411 668b485a mov cx,word ptr [eax+5Ah]
8053f415 890d40f0dfff mov dword ptr ds:[0FFDFF040h],ecx
8053f41b 9c pushfd
8053f41c 812424ffbfffff and dword ptr [esp],0FFFFBFFFh
8053f423 9d popfd
8053f424 8b0d3cf0dfff mov ecx,dword ptr ds:[0FFDFF03Ch]
8053f42a 8d4158 lea eax,[ecx+58h]
8053f42d c6400589 mov byte ptr [eax+5],89h
8053f431 8b0424 mov eax,dword ptr [esp]
8053f434 6a00 push 0
8053f436 6a00 push 0
8053f438 6a00 push 0
8053f43a 6a00 push 0
8053f43c ff7050 push dword ptr [eax+50h]
8053f43f ff7038 push dword ptr [eax+38h]
8053f442 ff7024 push dword ptr [eax+24h]
8053f445 ff704c push dword ptr [eax+4Ch]
8053f448 ff7020 push dword ptr [eax+20h]
8053f44b 6a00 push 0
8053f44d ff703c push dword ptr [eax+3Ch]
8053f450 ff7034 push dword ptr [eax+34h]
8053f453 ff7040 push dword ptr [eax+40h]
8053f456 ff7044 push dword ptr [eax+44h]
8053f459 ff7058 push dword ptr [eax+58h]
8053f45c ff3500f0dfff push dword ptr ds:[0FFDFF000h]
8053f462 6aff push 0FFFFFFFFh
8053f464 ff7028 push dword ptr [eax+28h]
8053f467 ff702c push dword ptr [eax+2Ch]
8053f46a ff7030 push dword ptr [eax+30h]
8053f46d ff7054 push dword ptr [eax+54h]
8053f470 ff7048 push dword ptr [eax+48h]
8053f473 ff705c push dword ptr [eax+5Ch]
8053f476 6a00 push 0
8053f478 6a00 push 0
8053f47a 6a00 push 0
8053f47c 6a00 push 0
8053f47e 6a00 push 0
8053f480 6a00 push 0
8053f482 6a00 push 0
8053f484 6a00 push 0
8053f486 6a00 push 0
8053f488 6a00 push 0
8053f48a ff7020 push dword ptr [eax+20h]
8053f48d ff703c push dword ptr [eax+3Ch]
8053f490 8bec mov ebp,esp
8053f492 833ddcaf548000 cmp dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)],0
8053f499 7502 jne nt!KiTrap02+0xa1 (8053f49d)
nt!KiTrap02+0x9f:
8053f49b eb24 jmp nt!KiTrap02+0xc5 (8053f4c1)
nt!KiTrap02+0xa1:
8053f49d 833ddcaf548008 cmp dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)],8
8053f4a4 721b jb nt!KiTrap02+0xc5 (8053f4c1)
nt!KiTrap02+0xaa:
8053f4a6 7517 jne nt!KiTrap02+0xc3 (8053f4bf)
nt!KiTrap02+0xac:
8053f4a8 803dc0d4548000 cmp byte ptr [nt!KdDebuggerNotPresent (8054d4c0)],0
8053f4af 750e jne nt!KiTrap02+0xc3 (8053f4bf)
nt!KiTrap02+0xb5:
8053f4b1 803dc1d4548000 cmp byte ptr [nt!KdDebuggerEnabled (8054d4c1)],0
8053f4b8 7405 je nt!KiTrap02+0xc3 (8053f4bf)
nt!KiTrap02+0xbe:
8053f4ba e82999fbff call nt!KeEnterKernelDebugger (804f8de8)
nt!KiTrap02+0xc3:
8053f4bf ebfe jmp nt!KiTrap02+0xc3 (8053f4bf)
nt!KiTrap02+0xc5:
8053f4c1 ff05dcaf5480 inc dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)]
8053f4c7 6a00 push 0
8053f4c9 ff1580864d80 call dword ptr [nt!_imp__HalHandleNMI (804d8680)]
8053f4cf ff0ddcaf5480 dec dword ptr [nt!KiAbiosPresent+0x8 (8054afdc)]
8053f4d5 7533 jne nt!KiTrap02+0x10e (8053f50a)
nt!KiTrap02+0xdb:
8053f4d7 a140f0dfff mov eax,dword ptr ds:[FFDFF040h]
8053f4dc 66833858 cmp word ptr [eax],58h
8053f4e0 7428 je nt!KiTrap02+0x10e (8053f50a)
nt!KiTrap02+0xe6:
8053f4e2 81c48c000000 add esp,8Ch
8053f4e8 8f0540f0dfff pop dword ptr ds:[0FFDFF040h]
8053f4ee 8b0d3cf0dfff mov ecx,dword ptr ds:[0FFDFF03Ch]
8053f4f4 8d4128 lea eax,[ecx+28h]
8053f4f7 c640058b mov byte ptr [eax+5],8Bh
8053f4fb 9c pushfd
8053f4fc 810c2400400000 or dword ptr [esp],4000h
8053f503 9d popfd
8053f504 cf iretd
nt!KiTrap02+0x10e:
8053f50a b802000000 mov eax,2
8053f50f e9dc280000 jmp nt!KiSystemFatalException (80541df0)
nt!KiSystemFatalException:
80541df0 55 push ebp
80541df1 6a00 push 0
80541df3 6a00 push 0
80541df5 6a00 push 0
80541df7 50 push eax
80541df8 6a7f push 7Fh
80541dfa e81774fbff call nt!KeBugCheck2 (804f9216)
80541dff c3 ret
可屏蔽中断
在硬件级,可屏蔽中断是由一块专门的芯片来管理的,通常称为中断控制器。它负责分配中断资源和管理各个中断源发出的中断请求.为了便于标识各个中断请求。中断管理器通常用IRQ(Interrupt Request)后面加上数字来表示不同的中断。比如:在Windows中 时钟中断的IRQ编号为0 也就是:IRQ0(大多数操作系统时钟中断在10-100MS之间,Windows系列为10-20MS)。
1、如果自己的程序执行时不希望CPU去处理这些中断,可以用CLI指令清空EFLAG寄存器中的IF位,用STI指令设置EFLAG寄存器中的IF位。
2、硬件中断与IDT表中的对应关系并非固定不变的,参见:APIC(高级可编程中断控制器),作用就是中断号和IDT表的映射关系。
异常
异常通常是CPU在执行指令时检测到的某些错误,比如除0、访问无效页面等。
中断与异常的区别:
1、中断来自于外部设备,是中断源(比如键盘)发起的,CPU是被动的。
2、异常来自于CPU本身,是CPU主动产生的。
3、INT N虽然被称为“软件中断”,但其本质是异常。EFLAG的IF位对INT N无效。
异常处理
无论是由硬件设备触发的中断请求还是由CPU产生的异常,处理程序都在IDT表。
常见的异常处理程序:
比如缺页异常,CPU会执行IDT表中的0xE号中断处理程序,由操作系统来接管。
缺页异常的产生:
1、当PDE/PTE的P=0时。
2、当PDE/PTE的属性为只读但程序试图写入的时。
3、当物理内存空间不足时,线性地址对应的物理页将被存储到文件中。
控制寄存器
控制寄存器用于控制和确定CPU的操作模式。
Cr0 Cr1 Cr2 Cr3 Cr4 。
Cr1 保留。
Cr3 页目录表基址。
Cr0 保留。
Cr0
1、PE位:CR0的位0是启用保护(Protection Enable)标志。
PE=1为保护模式 PE=0则为实地址模式,这个标志仅开启段级保护,而并没有启用分页机制。
若要启用分页机制,那么PE和PG标志都要置位。
2、PG位:当设置该位时即开启了分页机制。在开启这个标志之前必须已经或者同时开启PE标志。
PG=0且PE=0 处理器工作在实地址模式下。
PG=0且PE=1 处理器工作在没有开启分页机制的保护模式下。
PG=1且PE=0 在PE没有开启的情况下 无法开启PG。
PG=1且PE=1 处理器工作在开启了分页机制的保护模式下 。
3、WP位:对于Intel 80486或以上的CPU,CR0的位16是写保护(Write Proctect)标志
当设置该位为1时,处理器会禁止超级用户程序(例如特权级0的程序)向用户级只读页面执行写操作。保护三环用户数据。
当CPL<3的时候:
如果 WP=0 可以读写任意用户级物理页,只要线性地址有效。
如果 WP=1 可以读取任意用户级物理页,但对于只读的物理页,则不能写。
Cr2
当CPU访问某个无效页面时,会产生缺页异常,此时,CPU会将引起异常的线性地址存放在CR2中。
Cr4
PAE/PSE说明:
PAE=1 是2-9-9-12分页 PAE=0 是10-10-12分页,这个值实际上就是从boot.ini文件中读出来的。
PSE:相当于时PS位的总开关,只有当PSE为1时的,PS位才起作用,否则无论PS位为何值,都没有大页的存在。
后记
保护模式的细节还有很多,具体可参考intel白皮书第三卷:
发表评论
您还未登录,请先登录。
登录