荔园在线

荔园之美,在春之萌芽,在夏之绽放,在秋之收获,在冬之沉淀

[回到开始] [上一篇][下一篇]


发信人: oopilix ([0;1;32;40m), 信区: Visual
标  题: [zz]Windows内存机制解析
发信站: 荔园晨风BBS站 (Mon Oct 13 23:02:34 2003), 站内信件

By leezy_2000 03-9-3 9:38

前言



写这篇文章之前相当长的一段时间里,对windows内存机制是有着相当的困惑的。
各个进程的内存空间是如何隔离和共享的?GDT(全局描述表)尚在,可分段机制
去了那里?既然我们有虚拟的4G空间和结构化异常为何分配内存仍可能失败?在什
么时候stack会溢出?―――

当我把这些问题都弄清楚后,我写了这篇文章为自己做了个总结,希望对大家也有
帮助。同时由于写Windows内存这块的文章比较多,我将尽力做到与别人的内容不
重合。

动笔后不久,我发现imquestion对于Windows内存写了几篇非常不错的文章,总题
目叫《JIURL玩玩Win2k内存篇》,推荐阅读。



一、总论



       Windows内存管理机制,底层最核心的东西是分页机制。分页机制使每个进
程有自己的4G虚拟空间,使我们可以用虚拟线性地址来跑程序。每个进程有自己的
工作集,工作集中的数据可以指明虚拟线性地址对应到怎样的物理地址。进程切换
的过程也就是工作集切换的过程,如Matt Pietrek所说如果只给出虚拟地址而不给
出工作集,那这个地址是无意义的。(见图一)



       在分页机制所形成的线性地址空间里,我们对内存进行进一步划分涉及的
概念有堆、栈、自由存储等。对堆进行操作的API有HeapCreate、HeapAlloc等。操
纵自由存储的API有VirtualAlloc等。此外内存映射文件使用的也应该算是自由存
储的空间。栈则用来存放函数参数和局部变量,随着stack frame的建立和销毁其
自动进行增长和缩减。



       说到这里,也许有人会提出疑问:对x86 CPU分段机制是必须的,分页机制
是可选的。为什么这里只提到了分页机制。那么我告诉你分段机制仍然存在,一是
为了兼容以前的16位程序,二是Windows毕竟要区分ring 0和ring 3两个特权级。
用SoftIce看一下GDT(全局描述表)你基本上会看到如下内容:



GDTbase=80036000 Limit=03FF



0008 Code32 Base=00000000 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为0的话,那么这个段可以看作不存在,因为偏移地址就是
最终的线性地址。



此外还有两个段存在用于Kernel Processor Control Region和user thread
environment block。所以如果你在反汇编时看到MOV ECX,FS:[2C]就不必惊讶,怎
么这里使用逻辑地址而不是线性地址。在以后涉及异常处理的地方会对此再做说明




http://www.csdn.
net/Develop/ArticleImages/20/20854/CSDN_Dev_Image_2003-9-31653250.png


二、从Stack说开去



从我个人的经验看,谈到内存时说堆的文章最多,说stack的最少。我这里反其道
而行的原因是stack其实要比堆更重要,可以有不使用堆的程序,但你不可能不使
用stack,虽然由于对stack的管理是由编译器确定了的,进而他较少出错。



通过链接开关/STACK:reserve[,commit]可以指定进程主线程的stack大小,当你建
立其他线程时如果不指定dwStackSize参数,则也将使用/STACK所指定的值。微软
说,如果指定较大的commit值将有利于提升程序的速度,我没验证过,但理应如此
。通常并不需要对STACK进行什么设定,缺省的情况下将保留1M空间,并提交两个
页(8K for x86)。而1M空间对于大多数程序而言是足够的,但为防止stack
overflow有三点需要指出一是当需要非常大的空间时最好用全局数组或用
VirtualAlloc进行分配,二是引用传递或用指针传递尺寸较大的函数参数(这点恐
怕地球人都知道),三是进行深度递归时一定要考虑会不会产生stack溢出,如果
有可能,可以采用我在《递归与goto》一文中提到的办法来仿真递归,这时候可以
使用堆或自由存储来代替stack。同时结构化异常被用来控制是否为stack提交新的
页面。(这部分写的比较简略因为很多人都写过,推荐阅读Jeffery Ritcher《
Windows核心编程》第16章)



下面我们来看一下stack的使用。

假设我们有这样一个简单之极的函数:





int __stdcall add_s(int x,int y)

{

       int sum;



       sum=x+y;



       return sum;

}



这样在调用函数前,通常我们会看到这样的指令。

mov         eax,dword ptr [ebp-8]

push        eax

mov         ecx,dword ptr [ebp-4]

push        ecx

此时把函数参数压入堆栈,而stack指针ESP递减,stack空间减小。



在进入函数后,你将会看到如下指令:

push        ebp

mov         ebp,esp

sub         esp,44h

这三句建立stack框架,并减小esp为局部变量预留空间。建立stack框架后,
[ebp+*]指向函数参数,[ebp-*]指向局部变量。



另外在很多情况下你会看到如下三条指令

push        ebx

push        esi

push        edi

这三句把三个通用寄存器压入堆栈,这样这三个寄存器就可以用来存放一些变量,
进而提升运行速度。

很奇怪,我这个函数根本用不到这三个寄存器,可编译器也生成了上述三条指令。




对stack中内容的读取,是靠基址指针ebp进行的。所以对应于sum=x+y;一句你会看


mov         eax,dword ptr [ebp+8]

add         eax,dword ptr [ebp+0Ch]

mov         dword ptr [ebp-4],eax

其中[ebp+8]是x,[ebp+0Ch]是y,记住压栈方向为从右向左,所以y要在x上边。



我们再看一下函数退出时的情况:

pop         edi

pop         esi

pop         ebx

mov         esp,ebp

pop         ebp

ret         8


[回到开始] [上一篇][下一篇]

荔园在线首页 友情链接:深圳大学 深大招生 荔园晨风BBS S-Term软件 网络书店