from http://blog.sina.com.cn/s/blog_51396f890100qjum.html
-
http://blog.csdn.net/wang_junjie/archive/2008/07/03/2608997.aspx
本文所有代码均在VC2008下编译、调试。如果您使用的编译器不同,结果可能会有差别,但本文讲述的原理对于大部分编译器应该是相似的。对于本文的标题,实在不知道用什么表示更恰当,因为本文不仅淡了内存泄露检测机制,也谈到了指针越界的检测机制。到底应该说是MFC的机制,还是C++的机制?Anyway,相信你看了一定会有所收获。并欢迎常来本博客http://lionel.bokee.com留言讨论。在我们开发MFC应用程序的时候,不知大家是否注意到Debug版本输出窗口经常会有下面这样的信息:
Detected memory leaks! Dumping objects -> c:\my.data\my.codes\memleak\memleak\memleak.cpp(34) : {126} normal block at 0x00A321A0, 4 bytes long. Data: < > 01 00 00 00 Object dump complete.
编译器是怎么知道我们写的代码有内存泄露并能精确到文件、行号的呢?事实上也并不是所有情况都能精确到文件、行号,看看下面这种情况:
Detected memory leaks! Dumping objects -> First-chance exception at 0x75c739e5 (kernel32.dll) in MemLeak.exe: 0xC0000005: Access violation reading location 0x711af9f4. #File Error#(62) : {137} normal block at 0x00A721A0, 4 bytes long. Data: < > CD CD CD CD Object dump complete.
虽然检测出了内存泄露,但我们只能知道内存地址、行号,文件名是#File Error#,而且还伴随着内存非法访问的异常。这个异常看似是MFC在检测内存泄露的时候产生的。 下面我们从C++内存分配与回收的两个操作符 new, delete一步步分析C++内存管理以及MFC内存泄露检测机制。所有这些都是针对Debug版本的,最后我们再看看Release版本的情况。
一、内存分配操作符new 新建一个MFC应用程序,无论是Win32 Console Application + MFC Support,还是MFC Application或者是MFC DLL。编译器为我们生成的代码最前面,在#include下面都会有下面这三行代码:
#ifdef _DEBUG #define new DEBUG_NEW #endif
这三句话的意思是,如果是Debug版本,那么将new操作符定义为DEBUG_NEW。在afx.h中有对DEBUG_NEW的定义:
// Memory tracking allocation void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine); #define DEBUG_NEW new(THIS_FILE, __LINE__)
看来MFC是重新定义了一个new操作符,并把文件名、行号调试信息传给了new。下面是这个new操作符调用的其它函数。可见是按照MFC -> C++ -> C -> Win32 API的流程分配的内存:
DEBUG_NEW -> void* __cdecl operator new(size_t nSize, LPCSTR lpszFileName, int nLine) afxmem.cpp -> void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine) afxmem.cpp -> extern "C" _CRTIMP void* __cdecl _malloc_dbg(…) dbgheap.c -> extern "C" void* __cdecl _nh_malloc_dbg(…) dbgheap.c -> extern "C" static void * __cdecl _nh_malloc_dbg_impl(…) dbgheap.c -> extern "C" static void * __cdecl _heap_alloc_dbg_impl(…) dbgheap.c -> __forceinline void * __cdecl _heap_alloc (size_t size) malloc.c -> LPVOID WINAPI HeapAlloc(…); winbase.h
二、内存回收操作符delete MFC并没有重新定义delete操作符,因为所有调试信息已经传给了new操作符。delete操作符只要依然按照MFC -> C++ -> C -> Win32 API的流程将之前分配的内存释放掉就可以了:
operator delete -> class CCRTAllocator::static void Free(void* p) throw() atlalloc.h -> extern "C" _CRTIMP void __cdecl void * pUserData, int nBlockUse) dbgheap.c -> extern "C" void __cdecl _free_dbg_nolock(void * pUserData, int nBlockUse) dbgheap.c -> void __cdecl _free_base (void * pBlock) free.c -> BOOL WINAPI HeapFree(…); winbase.h
三、C++内存链 内存链是MFC检测内存泄露的基础,当我们每new一块内存,_heap_alloc_dbg_impl就会把这块内存加入内存链,当我们delete 一块内存,_free_dbg_nolock就会把这块内存从内存链中删除。VC的实现是使用了一个双向链表。每一个节点的结构定义如下:
typedef struct _CrtMemBlockHeader { struct _CrtMemBlockHeader * pBlockHeaderNext; // 下一个节点指针 struct _CrtMemBlockHeader * pBlockHeaderPrev; // 前一个节点指针 char * szFileName; // 调用new的文件名 int nLine; // 调用new的行号 size_t nDataSize; // 调用new分配内存大小 int nBlockUse; // 本块内存使用目的 long lRequest; // 请求编号 unsigned char gap[nNoMansLandSize]; // 内存前面的空白 } _CrtMemBlockHeader;
结构体中有几个成员可能需要解释一下。nBlockUse表示本块内存的用途,一般取值为_NORMAL_BLOCK。lRequest表示请求内存的编号,初始值为1,每请求一次,该值加1。我们在输出窗口看到的normal block就表示nBlockUse=_NORMAL_BLOCK, {137} 就是lRequest的值。data是真正返回给我们的指针,编译器在data前后用gap, anotherGap将数据保护起来并赋予特殊的值,以检测我们对指针操作是否越界。这些空白区域内存大小为#define nNoMansLandSize 4。data同样被赋予特殊的值,特殊值总共有四种:
static unsigned char _bNoMansLandFill = 0xFD; static unsigned char _bAlignLandFill = 0xED; static unsigned char _bDeadLandFill = 0xDD; static unsigned char _bCleanLandFill = 0xCD;
比如说我们new了一个int对象,int* p = new int;那么上面这个结构体内容如下:
+------------------------------------------------------------------------------+ | pBlockHeaderNext | …… | gap: FDFDFDFD | p: CDCDCDCD | anotherGap: FDFDFDFD | +------------------------------------------------------------------------------+
比如说我们内存访问越界了:*(p+1) = 0,那么在delete这个指针的时候,_free_dbg_nolock会对gap, anotherGap的值进行检查,发现不等于_bNoMansLandFill,就报错。如果我们写*(p+1) = 0xFDFDFDFD,那么就把编译器骗了,编译器认为内存访问并没有越界。当我们delete一块内存的时候,这块内存会被用_bDeadLandFill填充。如果我们new了多个对象,那么这些对象就链接再了一起,例如:
int* pB = new int; int* pA = new int;
内存布局如下:
+--------------------------------------------------------------------------+ | +--------------------------+ +--------------------------+ | +-> | pHead = pBlockHeaderNext | -----------> | pBlockHeaderNext = NULL | | |--------------------------| |--------------------------| | | pBlockHeaderPrev = NULL | | pBlockHeaderPrev ->-|-+ |--------------------------| |--------------------------| | ...... | | ...... | |--------------------------| |--------------------------| |gap: FDFDFDFD | |gap: FDFDFDFD | |--------------------------| |--------------------------| |pA: CDCDCDCD | |pB: CDCDCDCD | |--------------------------| |--------------------------| |anotherGap: FDFDFDFD | |anotherGap: FDFDFDFD | +--------------------------+ +--------------------------+
知道了内存块的布局,我们甚至可以通过一个指针,打印出当前new过的所有对象内存地址及大小。为了验证上述内容的正确性,我们不妨写一个简单的验证程序:
int* pB = new int(2); int* pA = new int(1);
cout << "*pA = " << *pA << ", *pB = " << *pB << endl; // *pA = 1, *pB = 2
*((int*)(*(pA - 8)) + 8) = 1; *((int*)(*(pB - 7)) + 8) = 2;
cout << "*pA = " << *pA << ", *pB = " << *pB << endl; // *pA = 2, *pB = 1
delete pA; delete pB;
四、内存泄露检测机制 MFC正是因为有了内存链,才可以检测出哪些内存还没有被释放。在程序退出的时候,dbgheap.c中的extern "C" _CRTIMP int __cdecl _CrtDumpMemoryLeaks(void) 函数会被调用,然后遍历当前的内存链,看看还有哪些内存没有被释放,然后打印出内存泄露的信息。原理很简单,这里不再赘述。那么为什么有的情况下我们无法通过输出的信息定位到具体泄露的文件呢?为什么有的时候会显示#File Error#?看看上面提到的结构体中文件名的保存char * szFileName,仅仅保存了一个指向文件名的指针而已。这个文件名是作为一个字符串,保存在.exe或.dll的.rdata中的。如果在.exe 文件退出的时候,我们显式加载的.dll文件已经被我们卸载了,并且在该.dll文件内存存在内存泄露的话,虽然_CrtDumpMemoryLeaks 会尝试读取并显示文件名,但szFileName指针指向的内存空间已经是无效的了。_CrtDumpMemoryLeaks在读取文件名之前会先调用 API函数IsBadReadPtr判断该指针是否有效。如果已经无效则显示#File Error#。本文最开始所提到的异常,正是由IsBadReadPtr导致的。
五、Release版本 对于Release版本,就没有上面提到的内存链了。对于new和delete的调用将会被直接转到malloc.c和free.c。因为没有内存链,没有多余的保护数据填充,没有内存越界检测机制,所以有些时候Debug版本会崩溃,但是Release版本却没有。这并不代表代码没有问题,而是内存非法访问更难发现了,当Release版本崩溃的时候,问题也更难定位了。
上述内存泄露检测、内存越界访问检测的原理很简单,但并不能查出所有内存非法访问。所以永远不要乱用指针,然后把所有对指针的判断都用try{}catch{}规避。因为并不是所有指针非法访问都能catch到,即使catch到了,内存也可能已经被写坏了。
-
2008-12-11
使用PageHeap.EXE或GFlags.EXE检查内存越界错误(转载) - [C/C++]
我推荐使用PageHeap.Exe和Gflags.Exe,主要的原因还是因为当有人问内存越界的错误如何查出来的时候,国外的朋友经常会推荐这两个工具(highly recommend)。我用过之后,也觉得有些时候用用还是有好处的。
PageHeap.Exe将针对某个指定的应用程序启用Page Heap标志,从而自动监视所有的malloc、new和heapAlloc的内存分配,找出内存错误。
PageHeap.Exe的下载地点:
http://download.microsoft.com/download/vc60pro/utility/6.0/win98/en-us/pageheap1.exe
下面我们简单地给出PageHeap使用步骤:
第一步:
在命令行中运行PageHeap.Exe。如果你以前设置过启用Global Page Heap标志,那么你将看到一个列表,给出所有已经启用了的应用程序的名字,不含路径。
如下所示:
C:\>pageheap
pgh.exe enabled
testSplit.exe enabled
第二步:编译一个小程序,其中有如下代码:
void main()
{
int m_len = 5;
char *m_p = (char *)HeapAlloc (GetProcessHeap (), HEAP_ZERO_MEMORY, m_len);
m_p[m_len] = 0;
HeapFree (GetProcessHeap (),0, m_p);
}Build出一个Debug版本。运行之,你看不到有任何异常的报告。
但其实m_p[m_len]=0这句话就是越界写了,因为只分配到了m_p[m_len-1]!这种情况就叫Dynamic memory overrun。用BoundsChecker是可以查到的。
这时,表面上看不出任何问题,但是一颗定时炸弹已经埋下了。
第三步:在命令行中运行PageHeap /enable YourApplicationName.exe 0x01。
再运行一次不带参数的PageHeap,察看上面的命令是否生效。你的应用程序应该在启用的列表中。
注意:千万不要在YourApplication.Exe前面加上路径!!
0x01的含义在后面说明。
第四步:再次运行你的程序。
你将会注意到在Output窗口的加载各种DLL之前,多了几句话:
Loaded exports for 'C:\WINNT\System32\ntdll.dll'
Page heap: process 0x57C created heap @ 00130000 (00230000, flags 0x1)
Loaded 'C:\WINNT\system32\MFC42D.DLL', no matching symbolic information found.
..
Loaded 'C:\WINNT\system32\MSVCP60D.DLL', no matching symbolic information found.
Page heap: process 0x57C created heap @ 00470000 (00570000, flags 0x1)
Loaded exports for 'C:\WINNT\system32\imm32.dll'这就是Page Heap的监视机制在发挥作用!他告诉你你的堆00470000被创建出来了。
然后程序退出后,Output窗口有这么几句话表明一定有什么错误发生了:
Page heap: block @ 0015AFF8 is corrupted (reason 10)
Page heap: reason: corrupted suffix pattern
Page heap: process 0x57C destroyed heap @ 00471000 (00570000)
The thread 0x8A8 has exited with code 0 (0x0).这说明在销毁堆00470000时遇到了麻烦,就是数据块0015AFF8被误用了,原因是误用了下标语法。看,说得多么清楚!也节省了许多翻来覆去查代码的工作!
PageHeap的使用中有几点值得注意:1:启用PageHeap不能够影响正在运行中的应用程序。如果你需要启用一些正在运行且不能重启的程序的PageHeap,那请运行PageHeap启用后,重新启动机器。
2:要想查看PageHeap把信息放到哪里了,请打开你的注册表,来到HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
你将会看到你的应用程序也在这个项下面。你的应用程序的GlobalFlag被设置为了0x02000000,PageHeapFlags被设置为了0x01。
3:PageHeap 的原理是这样,它在已分配的内存的后面放上几个守护字节(Guard Bytes),再跟上一个标记为PAGE_NOACCESS的内存页。这样,已分配内存的后面如果被重写了,那么守护字节就会被改变,于是当内存被释放时,PageHeap就会引发一个AV(Access Violation)。大体上就是这样。所以只有最后释放这块问题内存时,才会有PageHeap的报告!这就是PageHeap的局限性吧。
参数0x01的含义:
FLAGS hex value (0x...) has the following structure:
B7-B0 Bit flags 1 - enable page heap
01 - enable page heap. If zero normal heap is used. In 99% of the cases you will want this to be set.
02 - collect stack traces (default on checked builds)
04 - minimize memory impact
08 - minimize randomly(1)/based on size range(0)
10 - catch backward overruns看到了吗?你还可以设置参数为0x10,从而可以检查内存向前的越界写!
Gflags.Exe 是微软的Debugging Tools里面的工具。在Windows 2000的Resource Kit中也可以找得到。我们也可以用它来完成和PageHeap相同的任务。当然,Gflags.EXE还能做许许多多其他的事情。这里我们就不介绍了,总之物超所值。
具体的使用办法是:
1) 运行Gflags.Exe;
2) 你将看到一个对话框。在”Image File”的编辑框中写下你的应用程序的名字,如YourApp.Exe。注意不要路径!
3) 选择”Image File Options”的单选钮;
4) 这时,你会看到对话框的内容突然一变。选中“Place heap
allocations at ends of pages”前的复选框。5) 点击Apply按钮。
这样,就达到了PageHeap的效果。现在运行你的程序,overwrite你的堆,就应该生成一个AV了!
(请结合查看微软KB:SAMPLE: PageHeap1.exe Finds Heap Corruption and Memory Errors (Q264471))