stand on the shoulders of giants

虚拟内存到物理内存(32位)

部分出处(http://blog.sina.com.cn/wyw1976) 和 http://www.cnblogs.com/Winston/archive/2009/04/12/1434225.html

我们通过一个实际的例子,窥看操作系统是如何完成从逻辑地址到物理地址的转换的。

(1)运行计算器程序(calc.exe),随便输入任意数字,如下图所示:

image

(2)运行WinDbg, File-->Attach to a process, 选择calc.exe

(3)输入命令"x calc!g*", 列出calc中所有以g开头的符号,如图

image

其中的gpszNum存放的就是用户输入的数值

(4)输入命令 "db poi(calc!gpszNum)", 因为gpszNum是一个指针变量,因此用取值操作poi,结果如下:

 image
我们看到了我们在界面上输入的数字。

(5)另一种方法:既然gpszNum是一个指针,那就意味着gpszNum中存放的是一个地址,而该地址指向的内存中包含的才是我们要找的值,如下图(dd 命令用于读取指定内存地址中的值):

image

dd gpszNum 也可以直接 dd 01014db0
我们仍然找到了字符串“888652”,它所在的逻辑地址是000af360, 这显然是一个用户态地址,因为在Windows平台上,每个进程的地址空间是0~4G, 其中0~2G是用户态,而2G~4G是内核态。那它对应的物理地址是多少呢?这就涉及到逻辑地址到物理地址的转换。

Virtual Address(虚地址)

对于一个32bit的virtual address,其格式如下所示:

image

其中高10bit为Page Directory中的index项,中间10Bit为Page Table的index项,最低12bit为页内偏移地址。注意,它们记录的都是“偏移”量。

从virtual address向physical address转换的过程如下:

Step1: 通过CR3寄存器定位到页目录的起始位置(DirBase), 故CR3 Regesiter又称为页目录基地址寄存器; !Process 0 0可以看到每个进程的DirBase

Step2: 取virtual address的高10bit作为index,在PD中查找相应的PDE. 每个PDE的高20位代表该PDE所指向页表起始物理地址的高20位; 一般都是000000000,所以取!DirBase的第一项作为PDE;

Step3: 根据PDE中的页表基地址(即PDE的高20bit)定位到Page Table(页表); 一般step3和step4一起 !(PDE前20位 + 10-21bit索引*4),得到PTE

Step4:  取virtual address中第10-21bit作为索引,选取页表中的一个PTE(页表项),每个PTE的高20位代表的是4KB内存页的起始物理地址的的高20位

Step5: 取PTE中的内存页表基地址(高20bit)+ virtual address中的低12位offset,即可以得到实际的physical address 了; !PTE前20位+低12位offset

image

有关Paging的二级寻址,
总是为每个进程分配一个页目录page directory如果系统中有多个进程,内存中就会有多个页目录,每个page directory本身占用4K。
先找page directory,有1024项目,每一项对应一个page directory entry(通过线性地址的高十位)
再从page table中找PTE,每个PTE对应一个页面文件 4K(有4k,2M, 4M)。( 通过线性地址的中10位)每个page table本身占用4K
所以一张Page table对应的内存空间是 4K * 1024 = 4M , 因而Page directory对应大小是1024 * 4M = 4G ,虚拟内存大小4G, 其中0~2G是用户态,2G~4G是内核态

有关逻辑地址,线性地址和物理地址
程序中变量或函数的逻辑地址是在程序编译确定的,实际上就是段内的偏移。
逻辑地址加上段地址,就形成了线性地址。

在Windows平台上,逻辑地址就是线性地址。现在我们通过WinDbg来验证。

(1)打开WinDbg, 点击File----->Attach to a Process...,关联任何一个进程,WinDbg会显示段寄存器的值。

(2)执行“dg + 段选择子”, 可以查看段的详细信息,如下图所示:
image

其中base指定了段的基地址,可以看到代码段CS, 数据段DS的基地址是0,这也就是意味着逻辑地址+0=线性地址,因此逻辑地址==线性地址。也就是Windows在淡化段地址,真正其作用的是页地址。

另外,上图中还有一个fs==0038是什么意思呢?在Windows平台上,该寄存器存放的是当前线程的TEB(线程环境块),而它的基地址是非零的,验证如下:

(1)执行"dg 0038", 可见其base为7ffdc000

(2)执行"!teb",显示当前线程的TEB信息,可以看到其存放地址确实是7ffdc000,如下图所示:

image
32位线性地址分为3段:

31----22:高10位指定了页目录偏移,系统在加载每个进程的时候都有为该进程分配内存,而内存的管理是通过三级页表的方式,但并不是所有内存都一步分配到位的。首先,总是为每个进程分配一个页目录,它的大小是1024,高10位就是指定了1024项中的一项,每一项其实也是一个内存地址,该地址开始的1024个空间代表一个页表。只要进程是活着的,这个页目录就一直存在内存中,如果系统中有多个进程,内存中就会有多个页目录在每个进程的进程环境块(PEB)中会记录页目录的地址,在进程切换的时候,该地址会传递给CPU的CR3寄存器以便CPU进行地址转换。在WinDbg中,我们可以看到该信息:

(1)打开WinDbg, 点击File---->Kernel Debuging...,选择Local, 进入内核调试模式

(2)在WinDbg窗口中执行".symfix c:\111",以及“.reload”, 加载符号表

(3)运行"!process 0 0", 列出了当前系统中所有进程,其中的DirBase就是页目录地址。

 image

21----12:中10位指定了页表偏移通过高10位可以得到一个页表,该页表也有1024项,通过中10位在这1024项中选择1项,每一项包含一个地址,该地址开始的4096个字节就是一页。页表要占用内存,而且随着程序所需内存的变化而变化。

11----0:低12位指定了页内偏移通过中/高两项,我们可以定位一个页,每页大小为4096,是一个连续空间,低12位就是具体地址了4096中的一项,也就是具体的一个物理内存了。

一般的,地址转换的步骤是逻辑地址---->线性地址---->物理地址

 

从最后一步开始,执行du, 然后执行".format", 这条命令将显示该地址的二进制形式,然后我们按照线性地址的组成,分为10-10-12的形式, 如下如所示:

 image
页表项偏移: 00000000,  即第一项

页表偏移:  0010101111,  即为0xAF

页内偏移:  0011 01100000,即为0x360

(2) 再启动一个WinDbg, 点击File--->Kernel Debug..., 选择Local, 进入内核调调试模式

(3) 执行“!process 0 0”, 找到计算器进程,如下图所示,其DirBase=3f74a000, 这就是页目录所在的地址。

image

(4) 执行“!dd 3f74000”, 读取页目录,
image
因为线性地址的页表项为0, 就对应的第一项,即64db6867, (注意, 页表项的高20位即64db6000代表页表地址,低12位为属性位)

(5)页表地址64db6000代表一个页表的起始地址。从该地址开始的内存中的每一项(4个字节)对应一个PTE,因为指针占用是4个字节,所以要用偏移量乘以4。因此该起始地址加上页表偏移(0xAF),就可以找到PTE。执行“!dd 64db6000 + AF*4”,如下图所示:

image

(6)上图中的1589867的高20位即(0x15894000)就是物理页的其实地址,因为每页的大小是4K,所以页地址的低12位应该为0,之所以不为0,是充分利用这些位存放flag,例如标志是否可读,是否在内存中等等。页的其实地址加上页内偏移(360),就是物理地址,从下图可以看出,该地址存放的就是我们要找的字符串。

 image
至此,我们完成了从逻辑地址到物理地址的转换。

很清晰的过程,但是我没用试验成功,我的环境如下:
image

image

posted @ 2010-09-05 23:15  DylanWind  阅读(3286)  评论(0编辑  收藏  举报