文档章节

<转载>浅谈MFC内存泄露检测及内存越界访问保护机制

s
 songchang
发布于 2012/09/28 10:16
字数 2106
阅读 214
收藏 1

本文所有代码均在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 _free_dbg(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];   // 内存前面的空白        

/* followed by:          *  unsigned char           data[nDataSize];    // 真正的内存          *  unsigned char           anotherGap[nNoMansLandSize]; // 内存后面的空白          */

         *  unsigned char           data[nDataSize];    // 真正的内存          *  unsigned char           anotherGap[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;   /* fill no-man's land with this */static unsigned char _bAlignLandFill  = 0xED;   /* fill no-man's land for aligned routines */static unsigned char _bDeadLandFill   = 0xDD;   /* fill free objects with this */static unsigned char _bCleanLandFill  = 0xCD;   /* fill new objects with this */

比如说我们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版本,就没有上面提到的内存链了。对于newdelete的调用将会被直接转到malloc.c和free.c。 因为没有内存链,没有多余的保护数据填充,没有内存越界检测机制,所以有些时候Debug版本会崩溃,但是Release版本却没有。这并不代表代码没有问题,而是内存非法访问更难发现了,当Release版本崩溃的时候,问题也更难定位了。

   上述内存泄露检测、内存越界访问检测的原理很简单,但并不能查出所有内存非法访问。所以永远不要乱用指针,然后把所有对指针的判断都用try{}catch{}规避。因为并不是所有指针非法访问都能catch到,即使catch到了,内存也可能已经被写坏了。

本文转载自:

s
粉丝 0
博文 4
码字总数 713
作品 0
东城
私信 提问
几个C++内存泄漏和越界检测工具简介

一、BoundsChecker 或许你还不知道大名顶顶的Nu-Mega,但一定听说过他们的产品SoftICE,BoundsChecker也是这家公司的产品。与Visual C++配合使用,据说威力强大。本人和没有实际用过,在此复...

江河海流
2014/05/14
4.5K
0
【代码质量】C++代码质量扫描主流工具深度比较

本文由腾讯WeTest团队提供,未经授权严禁转载!更多资讯可直接戳链接查看:http://wetest.qq.com/lab/ 微信号:TencentWeTest 文/张蓓 引言 静态代码分析是指无需运行被测代码,通过词法分析...

shzwork
04/08
74
0
C/C++的内存泄漏检测工具Valgrind memcheck的使用经历

Linux下的Valgrind真是利器啊(不知道Valgrind的请自觉查看参考文献(1)(2)),帮我找出了不少C++中的内存管理错误,前一阵子还在纠结为什么VS 2013下运行良好的程序到了Linux下用g++编译...

xumaojun
2018/04/22
0
0
Valgrind *不是* 泄漏检查工具

概要: 在我的社区中,Valgrind 是我已知的被误解最深的工具。Valgrind 不仅仅是一个内存泄露检查器。它只是包含了一个检查内存泄露的工具而已。但我想说的是这个工具恰恰是 Valgrind 中用处最...

oschina
2014/12/09
7.9K
14
Debug与Release版本的区别详解

Debug 和 Release 并没有本质的区别,他们只是VC预定义提供的两组编译选项的集合,编译器只是按照预定的选项行动。如果我们愿意,我们完全可以把Debug和Release的行为完全颠倒过来。当然也可...

长平狐
2012/10/08
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周日乱弹 —— 我,小小编辑,食人族酋长

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @宇辰OSC :分享娃娃的单曲《飘洋过海来看你》: #今日歌曲推荐# 《飘洋过海来看你》- 娃娃 手机党少年们想听歌,请使劲儿戳(这里) @宇辰OSC...

小小编辑
今天
672
10
MongoDB系列-- SpringBoot 中对 MongoDB 的 基本操作

SpringBoot 中对 MongoDB 的 基本操作 Database 库的创建 首先 在MongoDB 操作客户端 Robo 3T 中 创建数据库: 增加用户User: 创建 Collections 集合(类似mysql 中的 表): 后面我们大部分都...

TcWong
今天
38
0
spring cloud

一、从面试题入手 1.1、什么事微服务 1.2、微服务之间如何独立通讯的 1.3、springCloud和Dubbo有哪些区别 1.通信机制:DUbbo基于RPC远程过程调用;微服务cloud基于http restFUL API 1.4、spr...

榴莲黑芝麻糊
今天
25
0
Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
71
0
maven 环境隔离

解决问题 即 在 resource 文件夹下面 ,新增对应的资源配置文件夹,对应 开发,测试,生产的不同的配置内容 <resources> <resource> <directory>src/main/resources.${deplo......

之渊
昨天
69
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部