Windows内存机制解析

前言

 

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

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

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

 

一、总论

 

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

 

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

 

       说到这里,也许有人会提出疑问:对x86 CPU分段机制是必须的,分页机制是可选的。为什么这里只提到了分页机制。那么我告诉你分段机制仍然存在,一是为了兼容以前的16位程序,二是Windows毕竟要区分ring 0ring 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 address0的话,那么这个段可以看作不存在,因为偏移地址就是最终的线性地址。

 

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

 

ASPectratio="t">

 

二、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 RitcherWindows核心编程》第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会自动扩充,如果你用光了储备,他会准时溢出。其次是不要以为你使用了缺省参数进行链接,你就有1Mstack,看看启动代码你就知道在你拥有stack之前,C Run –Time

    Library以用去了一小部分stack的空间。

三、浅谈一下Heap

//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;
}

 

(鉴于Matt Pietrek在它的《Windows 95 系统程式设计大奥秘》对9x系统的heap做了非常详细的讲解,此处涉及的内容将仅限于Win2000)

 

HeapStack正好相反,你需要手动来管理每一块内存的申请和释放(在没有垃圾收集机制的情况下),而对于C/C++程序员来说,操作Heap的方式实在是太多了点。下面是几乎所有可以操作堆内存的方法的列表:

malloc/free

new/delete

GlobalAlloc/GlobalFree

LocalAlloc/LocalFree

HeapAlloc/HeapFree

 

其中malloc/free由运行时库提供,new/deleteC++内置的操作符。他们都使用运行时库的自己的堆。运行时库的在2000win9x下都有自己独立的堆。这也就意味着只要你一启动进程,你将至少有两个堆,一个作为进程缺省,一个给C/C++运行时库。

 

GlobalAlloc/GlobalFreeLocalAlloc/LocalFree现在已失去原有的含义,统统从进程缺省堆中分配内存。

 

HeapAlloc/HeapFree则从指定的堆中分配内存。

 

单就分配内存而言(new/delete还要管构造和析构),所有这些方式最终要归结到一点200098下都是是HeapAlloc。所以微软才会强调GlobalAlloc/GlobalFreeLocalAlloc/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其实就是堆句柄。

 

原来是准备分析一下堆内部的一些结构的,可后来一想这么做实用价值并不是很大,所需力气却不小。因此也就没具体进行操作。但这里把实现监测堆中各种变化的小程序的实现思路公开一下,希望对大家有所帮助。这个小程序非常的简单,主要完成的任务就是枚举进程内所有的堆的变化情况。由于涉及到比较两个链表的不同,这里使用了STLvector容器和某些算法来减少编码。同时为了使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结构是什么样的?

 

堆内部对内存块的组织方式?(是链表么)

 

每一个小块的描述信息在那里?(如果是链表,那就应该有指针使这些小块彼此相连。)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值