Windows内存机制解析

上传人:野鹰 文档编号:3199310 上传时间:2017-07-31 格式:DOC 页数:18 大小:106.50KB
返回 下载 相关 举报
Windows内存机制解析_第1页
第1页 / 共18页
Windows内存机制解析_第2页
第2页 / 共18页
Windows内存机制解析_第3页
第3页 / 共18页
Windows内存机制解析_第4页
第4页 / 共18页
Windows内存机制解析_第5页
第5页 / 共18页
点击查看更多>>
资源描述

《Windows内存机制解析》由会员分享,可在线阅读,更多相关《Windows内存机制解析(18页珍藏版)》请在金锄头文库上搜索。

1、Windows 内存机制解析By leezy_2000 03-9-3 9:38前言写这篇文章之前相当长的一段时间里,对 windows 内存机制是有着相当的困惑的。各个进程的内存空间是如何隔离和共享的?GDT(全局描述表)尚在,可分段机制去了那里?既然我们有虚拟的 4G 空间和结构化异常为何分配内存仍可能失败?在什么时候 stack 会溢出?当我把这些问题都弄清楚后,我写了这篇文章为自己做了个总结,希望对大家也有帮助。同时由于写 Windows 内存这块的文章比较多,我将尽力做到与别人的内容不重合。动笔后不久,我发现 imquestion 对于 Windows 内存写了几篇非常不错的文章,总题

2、目叫JIURL 玩玩 Win2k 内存篇 ,推荐阅读。一、总论Windows 内存管理机制,底层最核心的东西是分页机制。分页机制使每个进程有自己的4G 虚拟空间,使我们可以用虚拟线性地址来跑程序。每个进程有自己的工作集,工作集中的数据可以指明虚拟线性地址对应到怎样的物理地址。进程切换的过程也就是工作集切换的过程,如 Matt Pietrek 所说如果只给出虚拟地址而不给出工作集,那这个地址是无意义的。 (见图一)在分页机制所形成的线性地址空间里,我们对内存进行进一步划分涉及的概念有堆、栈、自由存储等。对堆进行操作的 API 有 HeapCreate、HeapAlloc 等。操纵自由存储的 AP

3、I 有VirtualAlloc 等。此外内存映射文件使用的也应该算是自由存储的空间。栈则用来存放函数参数和局部变量,随着 stack frame 的建立和销毁其自动进行增长和缩减。说到这里,也许有人会提出疑问:对 x86 CPU 分段机制是必须的,分页机制是可选的。为什么这里只提到了分页机制。那么我告诉你分段机制仍然存在,一是为了兼容以前的 16 位程序,二是 Windows 毕竟要区分 ring 0 和 ring 3 两个特权级。用 SoftIce 看一下 GDT(全局描述表) 你基本上会看到如下内容:GDTbase=80036000 Limit=03FF0008 Code32 Base=0

4、0000000 Lim=FFFFFFFF DPL=0 P RE /内核态 driver 代码段0010 Data32 Base=00000000 Lim=FFFFFFFF DPL=0 P RW /内核态 driver 的数据段001B Code32 Base=00000000 Lim=FFFFFFFF DPL=3 P RE /应用程序的代码段0023 Data32 Base=00000000 Lim=FFFFFFFF DPL=3 P RW /应用程序的数据段这意味着什么呢?我们再看一下线性地址的生成过程(见图一) 。从中我们应该可以得出结论,如果 segmeng base address 为

5、0 的话,那么这个段可以看作不存在,因为偏移地址就是最终的线性地址。此外还有两个段存在用于 Kernel Processor Control Region 和 user thread environment block。所以如果你在反汇编时看到 MOV ECX,FS:2C就不必惊讶,怎么这里使用逻辑地址而不是线性地址。在以后涉及异常处理的地方会对此再做说明。二、从 Stack 说开去从我个人的经验看,谈到内存时说堆的文章最多,说 stack 的最少。我这里反其道而行的原因是 stack 其实要比堆更重要,可以有不使用堆的程序,但你不可能不使用 stack,虽然由于对 stack 的管理是由编译

6、器确定了的,进而他较少出错。通过链接开关/STACK:reserve,commit可以指定进程主线程的 stack 大小,当你建立其他线程时如果不指定 dwStackSize 参数,则也将使用/STACK 所指定的值。微软说,如果指定较大的 commit 值将有利于提升程序的速度,我没验证过,但理应如此。通常并不需要对 STACK 进行什么设定,缺省的情况下将保留 1M 空间,并提交两个页(8K for x86) 。而 1M 空间对于大多数程序而言是足够的,但为防止 stack overflow 有三点需要指出一是当需要非常大的空间时最好用全局数组或用 VirtualAlloc 进行分配,二是

7、引用传递或用指针传递尺寸较大的函数参数(这点恐怕地球人都知道) ,三是进行深度递归时一定要考虑会不会产生 stack 溢出,如果有可能,可以采用我在递归与 goto一文中提到的办法来仿真递归,这时候可以使用堆或自由存储来代替 stack。同时结构化异常被用来控制是否为stack 提交新的页面。 (这部分写的比较简略因为很多人都写过,推荐阅读 Jeffery RitcherWindows 核心编程第 16 章)下面我们来看一下 stack 的使用。假设我们有这样一个简单之极的函数:int _stdcall add_s(int x,int y)int sum;sum=x+y;return sum;

8、这样在调用函数前,通常我们会看到这样的指令。mov eax,dword ptr ebp-8push eaxmov ecx,dword ptr ebp-4push ecx此时把函数参数压入堆栈,而 stack 指针 ESP 递减,stack 空间减小。在进入函数后,你将会看到如下指令:push ebpmov ebp,espsub esp,44h这三句建立 stack 框架,并减小 esp 为局部变量预留空间。建立 stack 框架后,ebp+* 指向函数参数,ebp-*指向局部变量。另外在很多情况下你会看到如下三条指令push ebxpush esipush edi这三句把三个通用寄存器压入堆栈

9、,这样这三个寄存器就可以用来存放一些变量,进而提升运行速度。很奇怪,我这个函数根本用不到这三个寄存器,可编译器也生成了上述三条指令。对 stack 中内容的读取,是靠基址指针 ebp 进行的。所以对应于 sum=x+y;一句你会看到mov eax,dword ptr ebp+8add eax,dword ptr ebp+0Chmov dword ptr ebp-4,eax其中ebp+8是 x,ebp+0Ch 是 y,记住压栈方向为从右向左,所以 y 要在 x 上边。我们再看一下函数退出时的情况:pop edipop esipop ebxmov esp,ebppop ebpret 8此时恢复 s

10、tack 框架,使 esp 与刚进入这个函数时相同,ret 8 使 esp 再加 8,使 esp 与没调用这个函数的时候一致。如果使用_cdecl 调用规则,则由调用方以类似 add esp,8 进行清场工作,使 stack 的大小与未进行函数调用时一致。Stack 的使用就这样完全被编译器实现了,只要不溢出就和我们无关,也许也算一种内存的智能管理。最后要补充的两点是:首先 stack 不像 heap 会自动扩充,如果你用光了储备,他会准时溢出。其次是不要以为你使用了缺省参数进行链接,你就有 1M 的 stack,看看启动代码你就知道在你拥有 stack 之前,C Run Time Libra

11、ry 以用去了一小部分 stack 的空间。标题 Windows 内存机制解析( 二) leezy_2000(原作)关键字 Windows 内存机制by leezy_2000 2003-10-8 17:01三、浅谈一下 Heap(鉴于 Matt Pietrek 在它的Windows 95 系统程式设计大奥秘对 9x 系统的 heap 做了非常详细的讲解,此处涉及的内容将仅限于 Win2000)Heap 与 Stack 正好相反,你需要手动来管理每一块内存的申请和释放(在没有垃圾收集机制的情况下) ,而对于 C/C+程序员来说,操作 Heap 的方式实在是太多了点。下面是几乎所有可以操作堆内存的

12、方法的列表:malloc/freenew/deleteGlobalAlloc/GlobalFreeLocalAlloc/LocalFreeHeapAlloc/HeapFree其中 malloc/free 由运行时库提供,new/delete 为 C+内置的操作符。他们都使用运行时库的自己的堆。运行时库的在 2000 和 win9x 下都有自己独立的堆。这也就意味着只要你一启动进程,你将至少有两个堆,一个作为进程缺省,一个给 C/C+运行时库。GlobalAlloc/GlobalFree 和 LocalAlloc/LocalFree 现在已失去原有的含义,统统从进程缺省堆中分配内存。HeapAl

13、loc/HeapFree 则从指定的堆中分配内存。单就分配内存而言(new/delete 还要管构造和析构) ,所有这些方式最终要归结到一点2000 和 98 下都是是 HeapAlloc。所以微软才会强调 GlobalAlloc/GlobalFree 和LocalAlloc/LocalFree 会比较慢,推荐使用 HeapAlloc,但由于 Global*和 Local*具有较简单的使用界面,因此即使在微软所提供的源代码中他们仍被大量使用。必须指出的是HeapAlloc 并不在 kernel32.dll 中拥有自己的实现,而是把所有调用转发到ntdll.RtlAllocateHeap。下面这

14、张从 msdn 中截取的图(图 2) ,应该有助于我们理解同堆相关的 API。堆内部的运作同 SGI STL 的分配器有些类似,大体上是这样,OS 为每个堆维护几个链表,每个链表上存放指定大小范围的区块。当你分配内存时,操作系统根据你所提供的尺寸,先确定从那个链表中进行分配,接下来从那个链表中找到合适的块,并把其线性地址返还给你。如果你所要求的尺寸,在现存的区块中找不到,那么就新分配一块较大的内存(使用VirtualAlloc) ,再对他进行切割,而后向你返还某一区块的线性地址。这只是一个大致的情形,操作系统总在不停的更新自己的堆算法,以提高堆操作的速度。堆本身的信息(包括标志位和链表头等)被

15、存放在 Heap Header 中,而堆句柄正是指向Heap Header 的指针,Heap Header 的结构没有公开,稍后我们将试着做些分析。非常有趣的是微软一再强调只对 toolhelp API 有效的 HeapID 其实就是堆句柄。原来是准备分析一下堆内部的一些结构的,可后来一想这么做实用价值并不是很大,所需力气却不小。因此也就没具体进行操作。但这里把实现监测堆中各种变化的小程序的实现思路公开一下,希望对大家有所帮助。这个小程序非常的简单,主要完成的任务就是枚举进程内所有的堆的变化情况。由于涉及到比较两个链表的不同,这里使用了 STL 的 vector 容器和某些算法来减少编码。同时为了使 STL 的内存使用不对我们要监测的对象产生干扰,我们需要建立自己的分配器,以使用我们单独创建的堆。此外还需要特别注意的一点是由于toolhelp API Heap32Next 在运行过程中不允许对任何堆进行扰动(否则他总返回 TRUE),导致我们只能使用 vector,并预先保留足够的空间。 (访问堆内部某些信息的另一种方式是使用HeapWalk API,看个人喜好了) 。程序的运行过程是这样的,先对当前进程中存在的堆进行枚举,并把结果存入一个 set 类型的变量 heapid1,接下来创建自己的堆给分配器使用,并对进程

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 行业资料 > 其它行业文档

电脑版 |金锄头文库版权所有
经营许可证:蜀ICP备13022795号 | 川公网安备 51140202000112号