先从理论上讲下。。。
内存管理器
1. 将一个进程的虚拟地址翻译转化为物理地址,这样当进程中的代码引用到虚拟地址时候就可以正确访问物理地址。(虚拟地址空间上被提交的物理地址集合成为工作集)
2. 当物理内存被过度提交时候,即物理内存已经不够时候,将会进行swap处理。
3. 内存映射文件(内存区对象,section object)
4. copy-on-write
5. AWE , 地址窗口扩展
内存管理器由如下几个部件组成
1. 工作集管理器
当空闲内存的数量降低到某个特定阀值下时候;或者每1s钟掉一次,执行工作集修剪,页面变老等。
2. 进程/栈交换器
执行进程栈和内核线程栈的换入换出操作。
3. 已修改页面写出器
将修改列表上的dirty page写回到合适的页面文件。
4. 映射页面写出器
将映射文件中的dirty page写到磁盘。
下面讲一些我的总结。。
一个进程的地址空间中的页面,分为free,reserved,commit。
这个可以通过观察process explorer / vmmap ,前者可以看到commit mem/phy mem.
后者可以看到总共保留的空间,提交的空间,workingset,这样能够比较好的直观查看.
另外commit的页面分为share,private,或者mapped(该内存区可能被其他进程映射了,或者没有).
内存管理器中用以实现共享内存的是内存区对象也成为file mapping object。
共享内存的原理就是,多个进程可以共享同一块物理内存,而它们在各自进程内的地址依然是私有的。
据个例子来说,多个进程同时加载一个dll,那么实际上此dll的代码页面将被自动共享,即加载到物理内存后,被各个不同的进程映射访问。
内存区对象并不==共享内存,当内存区对象连接到一个磁盘文件时候成为映射文件;连接到内存时候便可提供共享内存。
从实现来说,CreateFileMapping 如果传入INVALID_HANDLE_VALUE 则表示将创建一个连接到内存的内存区对象,而其他进程就可以通过OpenFileMapping 来打开此内存区对象,同时将其map到本身的地址空间中。
至于这个mapviewoffilemapping的过程,大致可以推断:
1) 保留一个连续的地址空间
2) 将进程的页表修改或者添加,使得刚保留的虚拟地址解释指向此特定的物理地址
下面做一个例子,查看内存映射文件
下面是一个例子
表示是基于内存的。
另外ieframe.dll 分为有image和data 映射。
Name Mapping Description Version Base Size Image Base WS Shareable
Data 0x22E7000 0x79000 0x0 K
Data 0x2360000 0x2000 0x0 K
ieframe.dll Image Internet Explorer 8.00.6001.18876 0x3ECA0000 0xA93000 0x3ECA0000 72 K
ieframe.dll Data Internet Explorer 8.00.6001.18876 0x7010000 0xE000 0x0 12 K
另外image并不仅仅映射内存而已,也往往作为handle被打开
比如下例。。
File C:\WINDOWS\system32\stdole2.tlb 0x00120089 0x893C2598 R-- 0x898
File C:\WINDOWS\system32\Macromed\Flash\Flash10c.ocx 0x00120089 0x88E3D258 R-- 0x91C
File C:\WINDOWS\system32\ieframe.dll 0x00120089 0x88E99690 R-- 0xE70
File C:\WINDOWS\Microsoft.NET\Framework 0x00100001 0x893532E8 RW- 0xE6C
上面这些信息其实可以通过procexplorer的dll、handle来观察。
系统页目录,页表。
1. windows存在一份描述系统空间的页表PTEs,这份是所有进程共享的
2. windows中每个进程都有一个页目录表,这份页目录表PDE,PDE中部分PTE是本进程私有的,而部分则指向系统的页表。
3. windows中也目录始终被map到0xC0300000,PAE开启的话则map到0xC0600000
虚拟地址描述符VAD
一颗自平衡二叉树。
即一段最大的地址范围,作为一个节点,描述有地址范围,访问权限,是否集成。
然后较这段地址小的作为它的左节点;否则右节点;构成了二叉树。
kd> !vad 827db860
VAD level start end commit
8279fa78 (10) 40 7f 26 Private READWRITE
82aa3570 ( 9) 80 82 0 Mapped READONLY
829ff7e8 (10) 270 275 0 Mapped READONLY
82a06bc8 ( 9) 280 2c0 0 Mapped READONLY
82996ac8 ( 6) 400 408 2 Mapped Exe EXECUTE_WRITECOPY
....
Total VADs: 261 average level: 9 maximum depth: 16
内存区对象 section object
内存区对象可以被映射到页面文件(mem),或者磁盘中的另外一个文件.
一个内存区对象有如下属性:
最大尺寸
页面保护
页面文件/映射文件
基内存/非基内存 (基内存指sharemem在所有进程内虚拟地址一致,否则不一定一致 )
观察映射文件的情况。
!memusage
!ca controladdr
Control Valid Standby Dirty Shared Locked PageTables name
828fa7b8 244 5388 0 0 0 0 mapped_file( mscorwks.dll )
82a15e38 0 100 0 0 0 0 mapped_file( helpsvc.exe )
82acf430 304 176 0 60 0 0 mapped_file( msvcr80.dll )
827a1f60 0 32 0 0 0 0 mapped_file( LINK.EXE-18E29C64.pf )
82bc5ca8 6508 1184 0 0 0 0 mapped_file( $Mft )
829cdd98 388 216 0 140 0 0 mapped_file( wininet.dll )
8280da58 0 96 0 0 0 0 mapped_file( A8FABA189DB7D25FBA7CAC806625FD30 )
....................
内存"优化"
1. 通过大量申请内存,造成可用内存瞬间大量减少
2. 其他进程的工作集被裁剪,进入swapfile
3. 申请进程瞬间释放内存
4. 短时间内出现了大量的可用内存
不过这个代码是压缩其他进程的工作集以及系统可用内存为代价的。
将可能大大降低其它进程的性能。
任务管理器的 commit 表示已经提交的虚拟内存总量,而物理内存的可用,大致可以推断出pagefile占了多少。
最后提一个问题,请分析内存泄露和物理内存的使用以及虚拟内存的使用量的关系?
A>> 将直接造成虚拟内存使用量上升,但是物理内存不一定。
交换分区大小=0可行吗?
A>> 不可行,尽管windows允许你设置不设置交换分区。但是一般来说在windows运行过程中,windows将有可能提示你,“您系统的虚拟内存过低,windows将自动帮您扩展".
根据我的理解,windows在收到一个内存commit之后,会根据某种策略或者算法,来确定是否需要对已有的进程的工作集workingset进行裁剪,所谓裁剪就是指回收那些进程的workingset将其交换入交换文件,然后释放物理内存,将这部分+可用的物理内存来满足本次内存commit。
因此即时你设置了交换分区=0,windows依然在内部以某种我现在还不知道的方式在使用交换分区。
x86内存地址空间
特别说明一点,ntdll.dll 对于每个进程而言地址都市一致的。
今天观察vmmap和win 7的任务管理器的内存列表,发现了几个不解的地方。
先说一下可以理解的:
1. total workingset 表示本进程占用的物理内存,包括私有的和share。
2. peek workingset
3. shared/sharable workingset.
不解的地方:
1. vmmap 的size、commit到底表示啥含义,看了help还是有点不太理解。
2. win7的任务管理器中有一个 “已提交内存” 帮助中显示这是内存 - 提交大小
为某进程使用而保留的虚拟内存的数量。 从vmmap中检查后发现,这个貌似既不是reserved,也不是所有已经提交的内存,因为这个值往往还比较小。
然后再说一下自己的理解,其实我想一个进程的内存是用,我们最关注的不外乎:
1. 它使用了多少物理内存,这其中又分为它私有的多少,共享的多少。
2. 它究竟总共使用了多少虚拟内存,这个我的认知认为应该是它到底提交了多少内存。
第一点其实已经可以知道;
第二点,从表面看起来应该是vmmap的commit字段。
而vmmap的size-commited 貌似也等于reserved的大小(注意这一点得到了证实,确实是).
那么这儿的疑问是commit-total ws 的大小表示啥? 从我反复查看vmmap,我得到的一个可能的结论是
两者的差值(这个其实看detail view,看某个行,然后这个行commit和total ws不一样就可以),是已经提交的,但是没有分配给物理存储器的地址空间。 但是这个貌似又不太像书上说的。
我仔细查看vmmap后,再次推论, 比如某一行,commit显示是1000k,total ws显示 100k,而这个区域的属性是read,那么剩下的900k到哪儿去了呢?
这儿有两个推断:
1. 900k是disk
2. 900k实际上还是reserved ,不过vmmap可能是整体保留了一块区域,然后只是提交了其中的一部分。
不过这个可以继续做测试验证。。。
验证的时候,准备做最多两个例子:
1. 关闭windows的swap支持,然后看commited 和total WS 是否一致,如果一致那么就证实了commited - total ws 就是 disk file. 否则继续。。
2. 写一个程序,预分配一段地址空间,然后提交其中的一段,通过vmmap 来观察地址的属性。
今天检查后,发现1这种理解不正确,即os hide了disk swap和物理内存的差异,你看到的ws就是包含两者。 因此将os修改为不使用swap实际上不会对结果造成影响,commited和total ws之间的差异还是没有解。
继续进行第二个例子,发现这个操作完全正常,即如果我分配一个region,然后提交,然后回收,这些操作都能够正常的在vmmap看到对应的结果,但是依然无法解释这个差异。
最后我自己写了一个简单的程序:
PVOID pMem = ::VirtualAlloc(NULL , 1024 * 100 , MEM_RESERVE , PAGE_NOACCESS);
while(true)
{
int i = ::getchar();
if(i == L'c')
{
::VirtualAlloc((LPBYTE)pMem + 1024 * 50 , 10 * 1024 , MEM_COMMIT, PAGE_READWRITE);
*((LPBYTE)pMem +1024 * 50) = 1;
//*((LPBYTE)pMem +1024 * 70) = 1;
}
else if(i == L'f')
{
::VirtualFree((LPBYTE)pMem + 1024 * 50 , 10*1024 , MEM_DECOMMIT);
}
else if(i == L'r')
{
::VirtualFree(pMem , 0 , MEM_RELEASE);
}
}
当 ::VirtualAlloc((LPBYTE)pMem + 1024 * 50 , 10 * 1024 , MEM_COMMIT, PAGE_READWRITE);
执行后,我发现COMMITED 确实变为 12k,4k一个page.
但是total ws = 0, 然后我使用
*((LPBYTE)pMem +1024 * 50) = 1;
再观察,发现total ws = 4kb
哦,这样结果出来了,原来是虚拟内存的缺页机制在发生作用。
1. 每个虚拟内存page,有一个属性 reserve , free , commited 。
2. 每个虚拟内存页,有一个present标记,表示是否在物理内存中;
因此如果我知识commit一块内存,并不代表一定有这么多物理内存需要马上提交。
如果我始终不使用这块内存,则不会实际分配。
关于这个推论,可以继续通过lkd,来查看页表的变化来确认是否真是如此。。。
开始实验:
1. 准备win7下的local kerneldebug,可以看我另外一片文章,在win7下为了方便观察,需要关闭pae,开启local debug.
1) host 机为win7
2) target机为 xpmode
3) 双方采用虚拟串口连接
2. 启动test程序 如同上面的source code。 在每次对vm操作的时候,观察页表的变化.
当virtualalloca reserve完后,我们来看看现象。
通过第一个步骤,结合vmmap的help,大致就可以看到我之前的推断是ok的,即size 表示所有的分配大小,不管是reserve还是commit。
这个时候到了用windbg来查看这个地址的时候了。
我们看到了pmem=0xd90000=00000000 11011001 00000000 00000000
那么也就是说它的高10位地址=0x3,表示它在页目录表中的offset=3;
中间10bit=01 1001 0000 = 0x190,这也是页表中的offset;
最后是低10位也是内页便宜 = 0
那么我们在kernel模式中看看这个test.exe的页目录地址是啥?
这儿就看到了dirbase=29d6f000 , 注意这个是物理地址.
那么我们就知道了待访问的目标0xd90000,所在的页表地址是
29d6f000 + 3 * 4
也就是图中的0x2a414067
这个地址的高20位表示页的实际地址,滴12bit=0x67表示页的属性。
这个属性这儿就不具体分析了。
然后就是看实际的页地址了 2a414000+ 0x190 * 4
这儿我们看到地址处的页地址=0,即还不存在。
然后再来看windbg的表现
我们注意到,页的属性页内容从0--->0x80;
这儿付PDE的格式:
║ PAGE TABLE ADDRESS 31..12 │ AVAIL │G | PS│0 │A│PCD| PWT│U/S│R/W│P║
P - PRESENT
PS - 0 for 4KB;1 for 4MB
A - accessed or not
R/W - READ/WRITE
U/S - USER/SUPERVISOR
D - DIRTY
PTE格式(4KB)
║ PAGE FRAME ADDRESS 31..12 │ AVAIL │G |PAT│D│A│PCD|PWT│U/S│R/W│P║
P - PRESENT
R/W - READ/WRITE
U/S - USER/SUPERVISOR
D - DIRTY
AVAIL - AVAILABLE FOR SYSTEMS PROGRAMMER USE
那么这儿的0x80 是上可以看到PAT=1 , 其他=0
P=0 表示这个页不存在,至于PAT的含义我目前也没有搞清楚,不过这个不影响我们继续分析。
搞到这儿大致可以看到,windows在我们reserve一段地址的时候实际上从本例来看是保留了3个4KB page,
而这个也符合在vmmpa的图像
这儿我们注意到windows将内存的访问属性修改为了NOACCESS。
下面我们真正写入一个byte
从vmmap的结果看,windows在我们访问某一个byte的时候,真正提交了4kb的物理内存。并且如同我们的code那样,将访问属性修改为RW。
这时候我们看看windbg的report
上图表现了:
1. PTE的属性发生了 变化从0x80 -->0x2b367067, 这儿低12bit表示属性,显然P=1,表示已经在内存中了。
2. 高20bit 是页的真正地址,+ 页内offset=0,我们看到了我们写入的那个值=1了,呵呵。
这样从workingset,到vmmap的不理解。。。。