前言
写这篇文章之前相当长的一段时间里,对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]就不必惊讶,怎么这里使用逻辑地址而不是线性地址。在以后涉及异常处理的地方会对此再做说明。
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 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
//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)
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结构是什么样的?
堆内部对内存块的组织方式?(是链表么)
每一个小块的描述信息在那里?(如果是链表,那就应该有指针使这些小块彼此相连。)