文章收藏-FAQ 位置:电脑学习网

Windows 内存机制解析

前言
    写这篇文章之前相当长的一段时间里,对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]就不必惊讶,怎么这里使用逻辑地址而不是线性地址。在以后涉及异常处理的地方会对此再做说明。
   
二、从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
此时恢复stack框架,使esp与刚进入这个函数时相同,ret 8使esp再加8,使esp与没调用这个函数的时候一致。如果使用__cdecl调用规则,则由调用方以类似add  esp,8进行清场工作,使stack的大小与未进行函数调用时一致。Stack的使用就这样完全被编译器实现了,只要不溢出就和我们无关,也许也算一种内存的智能管理。最后要补充的两点是:首先stack不像heap会自动扩充,如果你用光了储备,他会准时溢出。其次是不要以为你使用了缺省参数进行链接,你就有1M的stack,看看启动代码你就知道在你拥有stack之前,C Run –Time 
    Library以用去了一小部分stack的空间。
三、浅谈一下Heap
(鉴于Matt Pietrek在它的《Windows 95 系统程式设计大奥秘》对9x系统的heap做了非常详细的讲解,此处涉及的内容将仅限于Win2000)
 
Heap与Stack正好相反,你需要手动来管理每一块内存的申请和释放(在没有垃圾收集机制的情况下),而对于C/C++程序员来说,操作Heap的方式实在是太多了点。下面是几乎所有可以操作堆内存的方法的列表:
malloc/free
new/delete
GlobalAlloc/GlobalFree
LocalAlloc/LocalFree
HeapAlloc/HeapFree
 
其中malloc/free由运行时库提供,new/delete为C++内置的操作符。他们都使用运行时库的自己的堆。运行时库的在2000和win9x下都有自己独立的堆。这也就意味着只要你一启动进程,你将至少有两个堆,一个作为进程缺省,一个给C/C++运行时库。
 
GlobalAlloc/GlobalFree和LocalAlloc/LocalFree现在已失去原有的含义,统统从进程缺省堆中分配内存。
 
HeapAlloc/HeapFree则从指定的堆中分配内存。
 
单就分配内存而言(new/delete还要管构造和析构),所有这些方式最终要归结到一点2000和98下都是是HeapAlloc。所以微软才会强调GlobalAlloc/GlobalFree和LocalAlloc/LocalFree会比较慢,推荐使用HeapAlloc,但由于Global**和Local**具有较简单的使用界面,因此即使在微软所提供的源代码中他们仍被大量使用。必须指出的是HeapAlloc并不在kernel32.dll中拥有自己的实现,而是把所有调用转发到ntdll.RtlAllocateHeap。下面这张从msdn中截取的图(图2),应该有助于我们理解同堆相关的API。
 
堆内部的运作同SGI STL的分配器有些类似,大体上是这样,OS为每个堆维护几个链表,每个链表上存放指定大小范围的区块。当你分配内存时,操作系统根据你所提供的尺寸,先确定从那个链表中进行分配,接下来从那个链表中找到合适的块,并把其线性地址返还给你。如果你所要求的尺寸,在现存的区块中找不到,那么就新分配一块较大的内存(使用VirtualAlloc),再对他进行切割,而后向你返还某一区块的线性地址。这只是一个大致的情形,操作系统总在不停的更新自己的堆算法,以提高堆操作的速度。
堆本身的信息(包括标志位和链表头等)被存放在Heap Header中,而堆句柄正是指向Heap Header的指针,Heap Header的结构没有公开,稍后我们将试着做些分析。非常有趣的是微软一再强调只对toolhelp API有效的HeapID其实就是堆句柄。
 
原来是准备分析一下堆内部的一些结构的,可后来一想这么做实用价值并不是很大,所需力气却不小。因此也就没具体进行操作。但这里把实现监测堆中各种变化的小程序的实现思路公开一下,希望对大家有所帮助。这个小程序非常的简单,主要完成的任务就是枚举进程内所有的堆的变化情况。由于涉及到比较两个链表的不同,这里使用了STL的vector容器和某些算法来减少编码。同时为了使STL的内存使用不对我们要监测的对象产生干扰,我们需要建立自己的分配器,以使用我们单独创建的堆。此外还需要特别注意的一点是由于toolhelp API Heap32Next在运行过程中不允许对任何堆进行扰动(否则他总返回TRUE),导致我们只能使用vector,并预先保留足够的空间。(访问堆内部某些信息的另一种方式是使用HeapWalk API,看个人喜好了)。
 
程序的运行过程是这样的,先对当前进程中存在的堆进行枚举,并把结果存入一个set类型的变量heapid1,接下来创建自己的堆给分配器使用,并对进程中存在的堆再次进行枚举并把结果存入另一个set类型的变量heapid2,这样就可以调用set_difference求出我们新建堆的ID,再以后列举队内部的信息时将排除这个ID所表示的堆。接下来就可以在两点之间分别把堆内部的信息存入相应的vector,比较这两个vector,就可以得到对应于分配内存操作,堆内部的变化情况了。
  
(图2 from msdn by Murali R. Krishnan)
 
下面是一些悬而未决的问题,那位感兴趣可以自己探索。
 
Heap Header结构是什么样的?
 
堆内部对内存块的组织方式?(是链表么)
 
每一个小块的描述信息在那里?(如果是链表,那就应该有指针使这些小块彼此相连。)

//myallocator.h

#ifndef _MYALLOCATOR_
#define _MYALLOCATOR_


#include 〈iostream〉
#include 〈windows.h〉


namespace MyLib {
   template 〈class T〉
   class MyAlloc {
     public:
    static HANDLE hHeap;
       // type definitions
       typedef T        value_type;
       typedef T*       pointer;
       typedef const T* const_pointer;
       typedef T&       reference;
       typedef const T& const_reference;
       typedef size_t    size_type;
       typedef ptrdiff_t difference_type;

       // rebind allocator to type U
       template 〈class U〉
       struct rebind {
           typedef MyAlloc〈U〉 other;
       };

       // return address of values
       pointer address (reference value) const {
           return &value;
       }
       const_pointer address (const_reference value) const {
           return &value;
       }

       /* constructors and destructor
        * - nothing to do because the allocator has no state
        */
       MyAlloc() throw() {
       }
       MyAlloc(const MyAlloc&) throw() {
       }
       ~MyAlloc() throw() {
       }

       // return maximum number of elements that can be allocated
       size_type max_size () const throw() {
     size_type N;
           N=(size_type)(-1)/ sizeof(T);

     return  (0 〈 N ? N : 1);
       }

       // allocate but don’t initialize num elements of type T
       pointer allocate (size_type num, const void* = 0) {
           // print message and allocate memory with global new
           /*std::cerr 〈〈 “allocate “ 〈〈 num 〈〈 “ element(s)“
                     〈〈 “ of size “ 〈〈 sizeof(T) 〈〈 std::endl;
   */
           pointer ret = (pointer)(HeapAlloc(hHeap,0,num*sizeof(T)));
          // std::cerr 〈〈 “ allocated at: “ 〈〈 (void*)ret 〈〈 std::endl;
           return ret;
       }
  char *_Charalloc(size_type N)//vc 所附带的stl的特色
  {
   return (char*)HeapAlloc(hHeap,0,N*sizeof(T)); 
  }
       // initialize elements of allocated storage p with value value
       void construct (pointer p, const T& value) {
           // initialize memory with placement new
           new((void*)p)T(value);
       }

       // destroy elements of initialized storage p
       void destroy (pointer p) {
           // destroy objects by calling their destructor
           p-〉~T();
       }

       // deallocate storage p of deleted elements
    //原本应该为pointer
       void deallocate (void* p, size_type num) {
           // print message and deallocate memory with global delete
           /*
     std::cerr 〈〈 “deallocate “ 〈〈 num 〈〈 “ element(s)“
                     〈〈 “ of size “ 〈〈 sizeof(T)
                     〈〈 “ at: “ 〈〈 (void*)p 〈〈 std::endl;
   */
           HeapFree(hHeap,0,(void*)p);
       }
   };

   // return that all specializations of this allocator are interchangeable
   template 〈class T1, class T2〉
   bool operator== (const MyAlloc〈T1〉&,
                    const MyAlloc〈T2〉&) throw() {
       return true;
   }
   template 〈class T1, class T2〉
   bool operator!= (const MyAlloc〈T1〉&,
                    const MyAlloc〈T2〉&) throw() {
       return false;
   }
}//end namespace MyLib
#endif

 

//teststlmem.cpp

/*
 written by leezy_2000
 03-9-5 15:12
*/
#include “stdafx.h“

#pragma warning(disable:4786)

//#define _STLP_USE_MALLOC
#include “myallocator.h“
#include 〈iostream〉
#include 〈set〉
#include 〈vector〉
#include 〈algorithm〉
#include 〈windows.h〉
#include 〈Tlhelp32.h〉

typedef unsigned long ULONG_PTR, *PULONG_PTR;

using namespace std;

/*
  本程序需要注意的几点:

  1、在实现自己的分配器,这样可以使stl容器的变化不影响我们要监测的堆

  2、容器只能用vector否则任何堆的任何变化将导致Heap32Next始终返回TRUE
  这应该是微软的bug

  3、分配内存失败的时候应该抛出std::bad_alloc内存,此处考虑不会出现低
  内存的情况,没抛出此异常。即认定自编写分配器分配内存时不会失败。
*/

//用于比较堆内存块的仿函数
//以块大小来判定两个HEAPENTRY32的大小
class HeapInfoCompare
{
public:
 bool operator() (const HEAPENTRY32& he1,const HEAPENTRY32& he2) const
 {
  return (he1.dwBlockSize 〈 he2.dwBlockSize);
 }
};

typedef vector 〈 HEAPENTRY32, MyLib::MyAlloc〈HEAPENTRY32〉 〉 HEAPENTRYSET;

void heapinfo(HEAPENTRYSET& hset,ULONG_PTR heapid);

void getheapid(set〈ULONG_PTR〉& heapid)
{
 HANDLE hSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST,GetCurrentProcessId());
 HEAPLIST32  heaplist32;

 heaplist32.dwSize=sizeof(HEAPLIST32);

 BOOL bRet=Heap32ListFirst(hSnapShot,&heaplist32);

 while(bRet)
 {
  heapid.insert(heaplist32.th32HeapID);

  cout〈〈heaplist32.th32HeapID〈〈endl;

  bRet=Heap32ListNext(hSnapShot,&heaplist32);
 }
 CloseHandle(hSnapShot);

 cout〈〈“the end“〈〈endl;
}

HANDLE MyLib::MyAlloc〈HEAPENTRY32〉::hHeap=NULL;

HANDLE hHeap;

int main(int argc, char* argv[])
{
 //枚举此时所有堆并在建立新堆后再次枚举这样从中剔除新建堆
 set〈ULONG_PTR〉 heapid1,heapid2,heapid3;
 
 getheapid(heapid1);

 hHeap=HeapCreate(0,0,0);

 getheapid(heapid2);

 insert_iterator〈set〈ULONG_PTR〉 〉 iter(heapid3,heapid3.begin());

 set_difference(heapid2.begin(),heapid2.end(),heapid1.begin(),heapid1.end(),
  iter);

 set〈ULONG_PTR〉::iterator pos;
 ULONG_PTR newheapid;

 for( pos=heapid3.begin(); pos !=heapid3.end(); ++pos)
 {
  cout〈〈“The new heap id is\t“〈〈(*pos)〈〈endl;
  newheapid=*pos;
 }


 MyLib::MyAlloc〈HEAPENTRY32〉::hHeap=hHeap;

 //vector〈int, MyLib::MyAlloc〈int〉 〉 v1;
 HEAPENTRYSET heapset1,heapset2,heapset3;
 
 heapset1.reserve(400);//保证vector不自动增长
 heapset2.reserve(400);
 heapset3.reserve(400);

 int size;

 heapinfo(heapset1,newheapid);
 
 sort(heapset1.begin(),heapset1.end(),HeapInfoCompare());

 size=heapset1.size();

 HANDLE hCurHeap=GetProcessHeap();

// HeapAlloc(hCurHeap,HEAP_ZERO_MEMORY,4*1024);

 char* p=new char[4*1024];

// GlobalAlloc(GHND,4*1024);

 char* q=(char*)malloc(4*1024);

 cout〈〈 “the p is“〈〈(int)p〈〈endl;

 heapinfo(heapset2,newheapid);

 sort(heapset2.begin(),heapset2.end(),HeapInfoCompare());
 size=heapset2.size();

 insert_iterator〈HEAPENTRYSET〉 miter(heapset3,heapset3.begin());

 set_difference(heapset2.begin(),heapset2.end(),heapset1.begin(),heapset1.end(),
  miter,HeapInfoCompare());

 size=heapset3.size();

 HEAPENTRYSET::iterator mpos;
 for( mpos=heapset3.begin(); mpos !=heapset3.end(); ++mpos)
 {
  cout〈〈“The size of the different block is\t“〈〈(*mpos).dwBlockSize〈〈“\tand the addresss is\t“〈〈(*mpos).dwAddress〈〈“\tdwFlags is\t“〈〈(*mpos).dwFlags 〈〈endl;
  cout〈〈“The heapid is:\t“〈〈(*mpos).th32HeapID 〈〈endl;
 }

 return 0;
}
void heapinfo(HEAPENTRYSET& hset,ULONG_PTR hid)
{
 HANDLE hSnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPHEAPLIST,GetCurrentProcessId());
 HEAPLIST32  heaplist32;

 heaplist32.dwSize=sizeof(HEAPLIST32);

 BOOL bRet=Heap32ListFirst(hSnapShot,&heaplist32);
 
 static int i=0;

 while(bRet)
 {
  HEAPENTRY32  he32;
  DWORD totalsize=0,freesize=0;

  if(heaplist32.th32HeapID==hid) 
  {
   bRet=Heap32ListNext(hSnapShot,&heaplist32);
   continue;
  }

  DWORD number=10;
  HANDLE ProcessHeap[10];

  DWORD numget=GetProcessHeaps(number,ProcessHeap);

  HANDLE hHeap=GetProcessHeap();

  he32.dwSize=sizeof(HEAPENTRY32);

  Heap32First(&he32,heaplist32.th32ProcessID,heaplist32.th32HeapID);

  if(he32.dwFlags & LF32_FREE)
   freesize +=he32.dwBlockSize;
  
  totalsize +=he32.dwBlockSize;

  cout〈〈 “the heapid is :“〈〈he32.th32HeapID〈〈endl;
  cout〈〈“the information of first block: “〈〈 “Blocksize: “〈〈he32.dwBlockSize〈〈“\t Address: “〈〈(LONG)he32.dwAddress〈〈endl;
  
  if((he32.dwFlags & LF32_FIXED) || (he32.dwFlags & LF32_MOVEABLE))
  hset.push_back(he32);

  while(Heap32Next(&he32))
   {
    cout〈〈 “the information of block: “ 〈〈 “Blocksize: “〈〈he32.dwBlockSize〈〈“\t Address: “〈〈(LONG)he32.dwAddress〈〈endl;

    totalsize +=he32.dwBlockSize;

    if(he32.dwFlags & LF32_FREE)
    freesize +=he32.dwBlockSize;
    
    //cout〈〈 ++i 〈〈endl;    
    if((he32.dwFlags & LF32_FIXED) || (he32.dwFlags & LF32_MOVEABLE))
    hset.push_back(he32);
    //char*p =(char*)malloc(300);

   }

  cout〈〈“the total size of heap is: “〈〈totalsize〈〈endl;
  cout〈〈“the free  size of heap is: “〈〈freesize 〈〈endl;
  cout〈〈“the commited  size of heap is: “〈〈(totalsize-freesize)〈〈endl;

  bRet=Heap32ListNext(hSnapShot,&heaplist32);
 }

 CloseHandle(hSnapShot);

 cout〈〈“the end“〈〈endl;
}

    By leezy_2000 03-9-3 9:38

     [文章来源:“十万个为什么”电脑学习网]
     [网络地址:http://why100000.com]
     [版权声明:除本站部分特别声明禁止转载的专稿外,其他的文章可以自由转载,但请务必注明出处和原始作者。本站文章版权归文章原作者所有。如果本站转载的文章有版权问题请联系本站,我们会尽快予以更正。]
 

【字体:[大] [中] [小] 【加入收藏】 【发表评论】 【关闭本窗口】

Copyright © “十万个为什么”电脑学习网 2000-2007 陕ICP备06007929号
站务联系:MSN & Email:zhangking2008@gmail.com  QQ:9365822