C++编码中减少内存缺陷的方法和工具
C++语言是桌面系统,尤其是系统软件、大型应用软件的主流开发语言。C++语言以其灵活性著称,同时也更复杂。利用C++编写健壮的代码,更具有挑战性。C++允许动态内存管理, 同时也容易导致更多和内存相关的问题。一般而言, 除了系统设计上的缺陷, 基于C++的软件的缺陷和错误大部分都和内存缺陷(主要包括内存访问错误和内存泄漏两类)相关。 所以,消除代码中的内存相关缺陷,成为程序员编写、调试、维护代码中的任务,也是保证软件质量的关键。 #define _CRTDBG_MAP_ALLOC #include <stdlib.h> #include <crtdbg.h> #define DEBUG_NEW new(_NORMAL_BLOCK, THIS_FILE, __LINE__) (2)确保在每个.cpp文件的头部包含以下内容: #include "stdafx.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif (3)在程序的开始处开启报告内存泄漏的开关: _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF); 对于MFC工程, MFC已经做了相关的工作, 只需要确认在每个.cpp文件的头部包含上述第(2)点的内容。 在某些情况下,需要知道发生内存泄漏的内存块中的内容,但是标准的内存转储只是内存块头部的十六进制形式。为了得到更多的有用信息,需要以用户块类型(_CLIENT_ BLOCK)申请内存,并利用_CrtSetDumpClient建立用户块型内存的转储函数。具体的说,对于不是从CObject继承的类,需要: (1)为每个类/结构指定一个用户块子类型(参考crtdbg.h)。 (2)在申请内存时,采用重载的new形式:void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)(参考MFC源代码 afxmem.cpp),其中nType就是用户块的子类型。 (3)创建一个用户块内存转储函数,专门对每种需要转储的子类型进行处理(需要包含dbgint.h)。 (4)利用_CrtSetDumpClient对用户块内存转储函数进行注册(参考MFC源代码dumpinit.cpp)。 对于从CObject继承来的类,MFC 已经按照上述方法做了基础工作(参考MFC源代码 afxmem.cpp、dumpinit.cpp)。要有效转储从CObject继承的对象,需要:(1)对每个从CObject继承的类重载虚函数Dump。(2)在程序的初始化部分 加入代码 afxDump.SetDepth(1)来开启深度转储。 2.3 利用Purify和Insure++查找运行时内存缺陷 Rational Purify和Parasoft Insure++ 是用于运行时错误检查的工具。Purify主要检测:数组内存越界读/写,使用未初始化的内存,对已释放的内存进行读/写,内存泄漏等。Insure++利用其专利技术(源码插装和运行时指针跟踪)能够发现大量的内存操作错误,报告错误的源代码行和执行轨迹。根据笔者的测试(基于98个有各种内存错误的C++程序,涵盖了典型情形),Insure++ 6.1都能准确检测。 3 利用VC++环境的调试和诊断功能,检查和发现常见内存缺陷 理解常见的内存缺陷问题以及在VC++环境下的症状,能辅助我们减少问题的发生和及时修改问题。 从错误的表现形式上看, 和堆栈有关的错误主要分为两大类:堆栈溢出和函数返回信息被破坏。 (1)堆栈溢出(overflow) 此类错误主要有两种情形: 1)过大的局部变量。缺省情况下Windows为每个线程保留1M堆栈空间。在菜单Project->Properties->Configuration Properties -> Linker->System中可以看到Stack Reserve Size选项可以调整保留的堆栈空间大小。 2)递归调用层数过深。在调试过程中,调用堆栈(call stack)窗口中可以发现函数递归调用的模式。 (2)函数返回信息被破坏 此类错误主要有两种情形: 1)对局部变量的写操作超出了范围(上溢)。在调试过程中,函数堆栈被破坏掉的明显标志是无法显示调用堆栈,并且错误发生在被调用函数即将返回的位置。 2)在调用函数和被调用函数之间如果出现了函数参数的不匹配或者调用规范的不一致。 为了检查此类错误,应该在代码编译时打开/GS、/RTCs开关(在菜单Project->Properties->Configuration Properties-> C/C++->Code Generation下设置)。 另外一类错误是动态内存错误。典型的情况如下: (1)内存写越界。在调试版本中,如果是写上溢,就会收到“Damage:after block...”的跟踪消息,如果是写下溢出就会收到“Damage: before block...”的跟踪消息。 (2)删除不合法指针。在调试版本中,删除未初始化的指针或者非堆指针时,会收到_CrtIsValidHeapPointer断言错误。 (3)多次释放。在调试版本中,如果多次删除同一指针, 会收到_BLOCK_TYPE_IS_VALID断言错误。要防止此类错误,应在delete某个指向动态内存的指针后立即将其置为空。 4 利用Windows结构化异常处理机制处理发布版本软件的内存崩溃 在程序的发布阶段,应尽量减少程序错误尤其是内存崩溃。如果崩溃了,应该“优雅”地退出,尽量收集程序崩溃时的运行信息以帮助程序供应商后续的调试。要捕捉内存非法访问并获知非法访问的指令地址、寄存器内容等信息,需要用到Windows的结构化异常处理(Structured Exception Handling,SEH)机制[6]。MiniDumpWriteDump是dbghelp.dll提供的一个 API函数(参考MSDN),用于转储用户模式程序的一些信息(比如堆栈情况等)并存为一个文件(比如.dmp文件),此文件可以被微软的调试器(VC++或者WinDBG)利用进行事后调试。使用此函数需要dbghelp.h、dbghelp.lib和dbghelp.dll(这些文件可以在Windows Platform SDK中找到)。 要事后根据.dmp文件调试代码,需要为发布版本软件产生debug symbols (pdb)文件(打开编译器/DEBUG选项)。在拿到.dmp文件以后,用VC++打开.dmp文件,然后调试执行(按F5键)。这样,崩溃现场就会重现。文献[5]基于上述的方法实现了崩溃报告系统。 5 结论 实践证明,在上述方法和工具支持下的减少软件内存缺陷的方法和工具,可以有效防止和查找代码中的内存错误和内存泄漏,并且能和开发人员日常编码无缝结合,执行起来非常高效。上述方法配合单元测试、代码评审、每日构建、Bug追踪等措施,形成了一个高效的质量保证流程,在我们的大型平台软件开发过程中起到了重要作用。 参考文献 1 Sutter H, Alexandrescu A. C++ Coding Standards: 101 Rules, Guidelines, and Best Practices[M]. Addison-Wesley Professional, 2004-10. 2 Stroustrup B. The Design and Evolution of C++[M]. Addison-Wesley Professional, 1994-03. 3 Karlsson B. Beyond the C++ Standard Library: An Introduction to Boost[M]. Addison-Wesley Professional, 2005-08. 4 Zyzyck J. A Report Generator for PC-Lint[J]. Dr. Dobb's Journal, 2003, 28(2): 52. 5 Dietrich H. XCrash Report: Exception Handling and Crash Reporting[Z]. 2003-10. http://www./debug/ XCrash ReportPt4.asp. 6 Richter J M. Programming Applications for Microsoft Windows[M]. Microsoft Press, 1999-09. |
|