标 题: 【原创】Windows下的分页模式- 页目录和页表从物理内存到虚拟映射求值
作 者: hrpirip
时 间: 2012-12-06,12:45:36
链 接: http://bbs.pediy.com/showthread.php?t=159554
昨天在网上看到一段代码令大为不解,大家都知道一个虚拟地址到物理地址的转换伪公式为:
*(*(*PD[(VirtualAddress>>22)] & FFFFF000) [(VirtualAddress & 3FF000)] )&FFFFF000 + VirtualAddress & FFF;
PD为页目录基址,
*PD[(VirtualAddress>>22)] 主要算出页表的物理页号(即定位哪一张页表)后面是属性位,而后& FFFFF000获得该页表所在的实际物理地址。
而后以页表的实际物理地址牵引[(VirtualAddress & 3FF000)] 即定位该页表的哪一个页表项,得到虚拟地址所在页的物理页号和一些属性位。而后将其)&FFFFF000,即得到所在页的物理地址,最后再加上虚拟地址的低12bit(即 VirtualAddress & FFF),即为页内偏移,从而得到真实物理地址。
后来才发现原来伪公式是针对你可以直接访问物理地址而言的,并不能直接使用,在Windows保护模式下实际物理页表和物理页目录都被映射到进程的地址空间了,每个进程都有自己的页目录和页表,以此进行隔离进程。因此在内存空间中对页目录和页表的访问应当以虚拟地址的方式进行访问。网上摘录代码如下:
unsigned int PDE; unsigned int PTE; if (VirtualAddress >= 0x80000000 && VirtualAddress < 0xa0000000) { PhysicalAddress = VirtualAddress - 0x80000000; } else { PDE = * (unsigned int *) ( (VirtualAddress >> 22) * 4 + 0xC0300000); if (PDE & 0x00000001) { PTE = * (unsigned int *) ( (VirtualAddress >> 12) * 4 + 0xC0000000); if (PTE & 0x00000001) { PhysicalAddress = ( (PTE & 0xFFFFF000) + (VirtualAddress & 0x00000FFF) ); } } }
普及一下:
0xC0300000 2k以后页目录即被映射到这个地址上,页目录本身为一页,具有1024个页目录项,每一项为4字节,整个页目录的大小为4k。每一个页目录项都描述一张页表。(当页目录项的P位为1的时候)
0xC0000000 2k后所有的页表都将从该地址开始映射,每4kb为一个页表,每个页表1024个项,每项描述4k的内存页,因此每个页表应当描述: 1024个页表项*4kb = 4M ,4M大小的内存,共1024张页表,因此这些页表描述了整个4G内存地址空间。而每个页表项与页目录项一样,仅占用4个字节的地址空间,因此每一张页表的占内存的大小为: 1024个页表项*4字节 = 4KB。
接下来看一句:
if (VirtualAddress >= 0x80000000 && VirtualAddress < 0xa0000000) { PhysicalAddress = VirtualAddress - 0x80000000; }
首先判断虚拟地址是否为80000000 - 9FFFFFFF范围内的地址,如果是,则直接得其偏移作为物理地址。
这是因为2k下地址空间 80000000 - 9FFFFFFF 总是和物理内存 00000000 - 1FFFFFFF 一一映射,所以只要直接减去0x80000000得到的偏移即为物理地址(这一段内存叫做4M大内存页,每一页为4M,而不是4kb,描述这段内存不再使用页表,而是直接用页目录中的物理页号进行定位,一般供内核使用,以防止过多的占用Tlbs总存放的页表项。)
PDE = * (unsigned int *) ( (VirtualAddress >> 22) * 4 + 0xC0300000);
这一句也很好理解。即计算出PDE的地址(物理地址)。
而后代码中转,直接抛弃这个PDE了,当时大为不解,为哈计算个PDE后就丢了呢。 原来是这样的。
前面已经解释过了,物理地址是不能直接访问的。也就是这个所在页表的起始物理地址(PDE),是不能访问的,因此我们没有办法通过得到的PDE地址去访问一个页表。既然页表都访问不了,那么久不可能获得一个PTE。
但是可以看到,作者将其中的PTE获取方式写为:
PTE = * (unsigned int *) ( (VirtualAddress >> 12) * 4 + 0xC0000000);
并没有借助到PDE,那么这是怎么计算的呢?
这时候就应该开始变通了。看如下图:
我们可以看到:
当虚拟地址寻址页目录为第0项的时候,其页表地址其实是从第0张页表开始的,
当虚拟地址寻址页目录为第1项的时候,其页表地址其实是从第1张页表开始的。
当虚拟地址寻址页目录为第2项的时候,其页表地址其实是从第2张页表开始的。
接下来我们可以得知:
当虚拟地址寻址页目录为第0项的时候,其页表地址实际跨越了0个页表项,
当虚拟地址寻址页目录为第1项的时候,其页表地址实际跨越了1024个页表项。(跨过了第0张页表的1024个页表项)
当虚拟地址寻址页目录为第2项的时候,其页表地址实际跨越了2048个页表项。(跨过了第0张页表的1024个页表项和第1张页表的1024个页表项)
现在我们来试着计算一个地址:
假设地址: 0x00401000
该地址二进制形式为:10000000001000000000000
取其高10bit 0000000001 (页目录牵引第一项)
取其中10bit 0000000001 (页表牵引 第一项)
其低12bit为0,即页对齐,这里为了简单点说明,因此没有页内偏移。
我们按照上面得出的结论,以页表项为最小单位进行计算,0000000001为第一个页目录项,即其指向了第一张页表,针对所有页表的总基址而言,这张页表的所处位置已经跨越了1024个页表项(属于第0张页表的1024个页表项),所以我们可以写出:
0000000001 * 1024个页表项 * 4(每个页表项4字节) = 4096个字节。
即相对于所有页表的总基址0xC0000000的偏移。
现在我们得到了页表的地址,接下来再看中10bit 的0000000001,其含义为我们需要的是这张页表中的第1项,因为不是使用第0项,所以我们略过第个0页表项所占用的4个字节。取第一项即得到该虚拟地址真实所在的内存页物理地址。
总结为:
页表项相对页表总基址的偏移为 = 4096个字节 + 4个字节 = 4100个字节。
好,接下来我们用作者的方式来计算这个偏移:
0x00401000 右移 12位后 等于 0x401
0x401 * 4 = 0x1004 = 4100个字节。
结果是很明显的,是正确的。那么为什么我们上面计算那么多,而作者仅依靠0x00401000右移12位后再乘以4就可以得到所要使用的页表项相对于所有的页表总基址的偏移地址呢?
答案说难也不难,说简答也不简单。关键在于理解:
既然说0x00401000 右移 12位后 等于 0x401 ,再乘以4字节后即为所需要的页表项相对于所有页表总基址的偏移,那么可以说明这个0x401 即为所跨过的所有页表项的个数。
我们来看一下 这个0x401 的二进制形式: 10000000001
去掉最尾部的1 ,可以看到是一个10000000000转换为 10进制为 1024,刚好为所跨越的第0张页表的1024个页表项的个数。 后面的1即为页表内第一个页表项。
可以得知的是,(VirtualAddress>>12) 右移12位后将虚拟地址中的页目录项牵引号和页表项牵引号都算进去了,即:使用了哪一张页表和使用了哪一个页表项。
接下来的事情我没有办法用专业语言来描述,因为我是个17岁的中专生也是半路出家自学电脑的,没有什么高等数学基础,因此只能笼统的说一下结论,或者我补习后回来补充:
虚拟地址转换为二进制后:
页目录牵引号经过右移12位就等于乘以十进制的1024(比如牵引1位移12位就是10000000000?1024个页表项,牵引2位移12位后就是100000000000?2048个页表项),也就可以直接通过位移计算该页表总共跨越过多少个页表项,以此求出要找的那个页表偏移。
而后只要用这个页表偏移值加上虚拟地址中的页表项偏移值,即可得到我们需要的页表项相对所有页表总基址的偏移。
也就是说0x401就是位移的结果: 10000000001 = 10000000000b + 1b
和初中公式:x * y + z * y = (x+z) * y 的原理是一样的,这也是为什么最后这个0x401可以代入取址公式的原因 401*4 = (10000000000b + 1b) *4字节 = 4096字节。
最后再用这个值加上0xC0000000就可以获得PTE的虚拟地址,直接*PTE + page_offset(12bit) 即得到了虚拟地址所对应的最终物理地址。
也不得不佩服Unix和微软的人才,分页机制涉及了很多"巧合"与"契合",却也很合理