C++编码中减少内存缺陷的方法和工具

上一篇 / 下一篇  2012-07-20 09:29:12 / 个人分类:C++

51Testing软件测试网GCrRmJ'R:`5t

  概要:基于C++的软件的缺陷和错误大部分都和内存相关,预防、发现、消除代码中和内存相关的缺陷,成为程 序员编写、调试、维护代码时的重要任务。该文基于“面向网络海量空间信息的大型GIS”课题的工程实践,提出和总结了如何使用C++语言机制、开发环境和 相关质量保证工具来预防、发现各种编译期、运行期内存缺陷的方法和工具。51Testing软件测试网%VKaX4u0v7z4[2B!\H S

51Testing软件测试网 ^}f%bf8Y)d6G

  C++语言是桌面系统,尤其是系统软件、大型应用软件的主流开 发语言。C++语言以其灵活性著称,同时也更复杂。利用C++编写健壮的代码,更具有挑战性。C++允许动态内存管理, 同时也容易导致更多和内存相关的问题。一般而言, 除了系统设计上的缺陷, 基于C++的软件的缺陷和错误大部分都和内存缺陷(主要包括内存访问错误和内存泄漏两类)相关。 所以,消除代码中的内存相关缺陷,成为程序员编写、调试、维护代码中的任务,也是保证软件质量的关键。51Testing软件测试网[~?'_"|/^L'Rc;B(G

Oz?;SR tE0  本文的工作基 于“863”计划项目“面向网络海量空间信息的大型GIS”课题。该系统是基于C++/MFC编写,开发环境是Visual Studio .net 2003。本文基于此项目的工程实践,总结了如何使用C++语言机制、开发环境和相关质量保证工具来预防、发现各种编译期、运行期和内存相关的缺陷的方法 和工具。51Testing软件测试网N,?6s7`#`/V

51Testing软件测试网(k K5Mom"]ms P

  1、遵循C++相关的编码规范和惯用法,预防缺陷

nn|LGkM;X0

;H5IU'w1i0  编码规范是语言相关的规则,是经过实践总结出来的经验。良好的编程标准将有效地帮助开发人员避免开发有潜在危险的代码。一般来说,为了减少内存缺陷,应该遵循下列编码规则:51Testing软件测试网A4s7^~K'SMw

51Testing软件测试网A-U:U.BL

  (1)基类或者带有虚函数的类应该将其析构函数声明为虚函数。51Testing软件测试网\:U(h G_

51Testing软件测试网T5HMzS E

  (2)在构造函数中防止内存泄漏,在析构函数中不要抛出异常。51Testing软件测试网8p t~2R*}

51Testing软件测试网pg0|#E|&T_

  (3)使用对应形式的new和delete。即:用delete来释放new申请的内存,delete[]释放new[]申请的内存。

2?$epC O2c D9X%D[0

*|+rv*y;l M.Q0  (4)指针在使用前必须初始化,指向动态内存的指针在释放后应立即置为空。

F,D[2a o0

9v'J hK _[}6x0  (5)如果类构造函数中分配了资源,那么需要显式提供拷贝构造函数和赋值操作符,并且在析构函数中释放资源。

"u ]Mo't5c1h8vt0

ejdZK0   值得重视的是C++中的惯用法RAII。RAII核心思想是利用对象来管理资源,在对象的构造函数中获取资源,在其析构函数中释放资源[2]。为了保证 动态申请的内存能在即使出现异常的情况下仍能释放,比较理想的方法是使用局部变量来管理动态内存的所有权(ownership),就是所谓的智能指针。 STL中的auto_ptr就是为解决资源所有权问题设计的,但是缺少对引用数和数组的支持并且不能用在STL容器中。Boost库[3]提供的智能指针 相对成熟,实用价值高。其中,shared_ptr线程安全并且可以用在STL容器中。具体示例参考文献[3]。

@aHmJbS'G0

VtFDM0  1.1 编码规范检查工具 CodeWizard

4RbAt5hF)^_051Testing软件测试网"T&E b,VF:v2q

   CodeWizard能够对源程序直接进行自动扫描、分析和检查。一旦发现违例,产生信息告知与哪条规则不符并作出解释。以CodeWizard 4.3 为例,其中内置了超过500条编码标准。CodeWizard可以选择对于当前的工程执行哪些编码标准。CodeWizard可以和VC++紧密集成,安 装完毕以后,VC++中有CodeWizard工具条。

9oP ad,hg)@0

6`0R$s$Qu C3Uh;X7X0  1.2 代码检查工具 PC-Lint51Testing软件测试网n T~3z7J\)p7v

51Testing软件测试网o*R.e v$_RY

   PC-Lint可检查编译器不易发现的错误。PC-Lint可对100多个C库函数进行检查,可以发现标准C/C++代码中的1 000多个常见错误。要把PC-lint和Visual Studio集成在一起,需要自己配置。Jon Zyzyck提供了一个报告生成器,可以帮助完成这个工作。可在http://www.ddj.com下载。文献[4]说明了如何在VC++环境中集成 PC-Lint。51Testing软件测试网5QKK ho

#hp6Uw6X!^ ?0  2、利用语言机制、开发环境和相关工具以预防和发现内存缺陷

$RG:Q7j` J sGl&s051Testing软件测试网Y Ed4}j0B

  发现问题是解决问题的前提。相对于修复内存缺陷,发现内存缺陷并准确定位导致缺陷的代码更为费时费力。及早准确地发现内存缺陷,对于提高开发效率非常重要。51Testing软件测试网W"LN4w J E!|

51Testing软件测试网&g9[8jo5IW&^2L

  2.1 利用断言及早暴露内存缺陷51Testing软件测试网C?.YwFUc0x4i

51Testing软件测试网8o9x'ii4o8z \3WHv

  断言是布尔调试语句,用来检测在程序运行的时候某一条件的值是否总为真。断言经常用来确认函数的输入、输出,检查对的当前状态是否合法等。 在以下的场景使用断言可以帮助发现和内存非法访问相关的错误:51Testing软件测试网UbR wD1K

51Testing软件测试网 m'@s]3r(X5S9_

   (1)验证指针是否可读/写。在函数的入口处,经常需要验证指针所指向的内容区域是否可读/写。 通常采用assert(p!= NULL)的检测形式。 但是,指针的值不为空并不代表指针指向了合法可读/写内存。Win32 API提供了函数IsBadReadPtr、IsBadWritePtr、IsBadStringPtr、IsBadCodePtr用来检测指针指向的内 存区域是否可读/写。C运行时库提供了_CrtIs ValidPointer、_CrtIsValidHeapPointer等函数,MFC库提供了AfxIsValidAddress、 AfxIsValidString函数来完成类似功能。51Testing软件测试网E-^;w!B\D [

z0k/gAXxS6Y0  (2)对基于MFC的程序,ASSERT_VALID宏通过调用重载的 AssertValid函数来确定指向CObject派生类对象的指针是否有效。ASSERT_VALID宏主要调用了 AfxIsValidAddress函数和CObject派生类对象的AssertValid函数(参考MFC源代码afx.h、 objcore.cpp)。51Testing软件测试网v)|beo?R|^-i

Tu%{;jNX/z!FPW0  2.2 利用C运行时刻库检查内存泄漏51Testing软件测试网"x1Koa]3x0wiykSI

51Testing软件测试网)}(W(x^?

  VC++的C运行库(CRT)提供了广泛的功能,帮助用户检测内存泄漏。CRT提供了_CrtMemCheckPoint、_CrtDump MemoryLeaks、_CrtSetDbgFlag等函数来帮助调试内存泄漏。

(AqJ{-WTb0

Y9o[.lGr0  对于非MFC的工程, 要开启有效的内存泄漏报告功能, 需要进行如下设置:51Testing软件测试网%C*rI*v*k Pb

6FRKoJ1X0  (1)在StdAfx.h的头部添加如下代码并开启编译器/Yu 选项:

Y#a4U)Tq2\Ksr0

3m@.{;i.kS051Testing软件测试网h@)j;[|lBRN

#define _CRTDBG_MAP_ALLOC
-e'slmH:nQS/sV5T g0#include <stdlib.h>
6Q([O1NV{X.D0#include <crtdbg.h>
nXwC&[!Kw0#define DEBUG_NEW new(_NORMAL_BLOCK, THIS_FILE, __LINE__)

~7N7X9xQV.U0  (2)确保在每个.cpp文件的头部包含以下内容:51Testing软件测试网$H y3o[:G~ e4i2C%b

)n%}r'[ qp9GV9q0

&^x m!c:QEYX0
#include "stdafx.h"51Testing软件测试网+F'gZ[d_&w
#ifdef _DEBUG51Testing软件测试网1Mt;m%Qj%m_'P
#define new DEBUG_NEW51Testing软件测试网t!ye gmY1Q`7tr
#undef THIS_FILE51Testing软件测试网)rVKW/S
static char THIS_FILE[] = __FILE__;51Testing软件测试网/r/QB!l-Yj/b;{9R
#endif
51Testing软件测试网8?6_~Avv

  (3)在程序的开始处开启报告内存泄漏的开关:51Testing软件测试网9\.B QQ?2Mrmf

51Testing软件测试网u)s"]/q"h2W

  _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF|_CRTDBG_LEAK_CHECK_DF);

:jH$b8B S cN+_0

4Y!j2V(vs;U0  对于MFC工程, MFC已经做了相关的工作, 只需要确认在每个.cpp文件的头部包含上述第(2)点的内容。51Testing软件测试网Hd.x4XevTn%Y(u

51Testing软件测试网C%QYa Fg fl*v

  在某些情况下,需要知道发生内存泄漏的内存块中的内容,但是标准的内存转储只是内存块头部的十六进制形式。为了得到更多的有用信息,需要以用户 块类型(_CLIENT_ BLOCK)申请内存,并利用_CrtSetDumpClient建立用户块型内存的转储函数。具体的说,对于不是从CObject继承的类,需要:

`6zf1s {6o_051Testing软件测试网0D~d6z!@K We

  (1)为每个类/结构指定一个用户块子类型(参考crtdbg.h)。

#~q^!_5[;z051Testing软件测试网6p }-V Z6LA

  (2)在申请内存时,采用重载的new形式:void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)(参考MFC源代码 afxmem.cpp),其中nType就是用户块的子类型。51Testing软件测试网V"vN{,z

51Testing软件测试网eWyZ8c/H

  (3)创建一个用户块内存转储函数,专门对每种需要转储的子类型进行处理(需要包含dbgint.h)。

p6?9c6V q*W{"NE]lG051Testing软件测试网"O6z_5m#[0I!q%h}

  (4)利用_CrtSetDumpClient对用户块内存转储函数进行注册(参考MFC源代码dumpinit.cpp)。51Testing软件测试网t6J`8Qvd#d

51Testing软件测试网 m yk`8A.{z

  对于从CObject继承来的类,MFC 已经按照上述方法做了基础工作(参考MFC源代码 afxmem.cpp、dumpinit.cpp)。要有效转储从CObject继承的对象,需要:(1)对每个从CObject继承的类重载虚函数 Dump。(2)在程序的初始化部分 加入代码 afxDump.SetDepth(1)来开启深度转储。

,G&Ts+k7y QJ s9XpT7a0

6] H q PJI*P;l0  2.3 利用Purify和Insure++查找运行时内存缺陷

!wnLyfc7@i0

7F;{!y0IhJ3g0  Rational Purify和Parasoft Insure++ 是用于运行时错误检查的工具。Purify主要检测:数组内存越界读/写,使用未初始化的内存,对已释放的内存进行读/写,内存泄漏等。Insure++ 利用其专利技术(源码插装和运行时指针跟踪)能够发现大量的内存操作错误,报告错误的源代码行和执行轨迹。根据笔者的测试(基于98个有各种内存错误的 C++程序,涵盖了典型情形),Insure++ 6.1都能准确检测。

!F4o `J `Z!r,T6Q0 3、利用VC++环境的调试和诊断功能,检查和发现常见内存缺陷51Testing软件测试网)RR6e~:Q)z e

  理解常见的内存缺陷问题以及在VC++环境下的症状,能辅助我们减少问题的发生和及时修改问题。

P]5q;uA0

)k'[Z,oC9W4Iv0  从错误的表现形式上看, 和堆栈有关的错误主要分为两大类:堆栈溢出和函数返回信息被破坏。

&qY!{]0e I}$}0

w'~8j bg0R}5U0  (1)堆栈溢出(overflow)51Testing软件测试网!] `ls2ZPf

?D)DSC d e0  此类错误主要有两种情形:51Testing软件测试网zO.s/XU;V'AO

51Testing软件测试网Q%?%L0U,x4e'o0cCbh

  1)过大的局部变量。缺省情况下Windows为每个线程保留1M堆栈空间。在菜单 Project->Properties->Configuration Properties -> Linker->System中可以看到Stack Reserve Size选项可以调整保留的堆栈空间大小。51Testing软件测试网8TZ%qU%{? o @

51Testing软件测试网,?m#iqF6h-]T

  2)递归调用层数过深。在调试过程中,调用堆栈(call stack)窗口中可以发现函数递归调用的模式。51Testing软件测试网.F7`,|]2te-`5}

#p o1uYS!N7gZg/Uz0  (2)函数返回信息被破坏

z&{4c(Nf \b"}sI8kk0

'y qfg\R#vT9P h\N0  此类错误主要有两种情形:51Testing软件测试网:gBEOG,d C

51Testing软件测试网[/J3e1g&Xr,J

  1)对局部变量的写操作超出了范围(上溢)。在调试过程中,函数堆栈被破坏掉的明显标志是无法显示调用堆栈,并且错误发生在被调用函数即将返回的位置。

.B mg qcf(q2k!U0K0

lx-~'a4M0  2)在调用函数和被调用函数之间如果出现了函数参数的不匹配或者调用规范的不一致。

eM9uj/re~9Q!h0

F2q:wMB`#Yvs0  为了检查此类错误,应该在代码编译时打开/GS、/RTCs开关(在菜单Project->Properties->Configuration Properties-> C/C++->Code Generation下设置)。51Testing软件测试网,lt({NJ.`9DVl8l

51Testing软件测试网K{/O4gEu

  另外一类错误是动态内存错误。典型的情况如下:

9t)l V&u!A_0

u @]0GS `$zc _0  (1)内存写越界。在调试版本中,如果是写上溢,就会收到“Damage:after block...”的跟踪消息,如果是写下溢出就会收到“Damage: before block...”的跟踪消息。51Testing软件测试网r]OH VB

51Testing软件测试网 L2@fZ*~Pn)A5g

  (2)删除不合法指针。在调试版本中,删除未初始化的指针或者非堆指针时,会收到_CrtIsValidHeapPointer断言错误。51Testing软件测试网:xj T N;\/E,E

51Testing软件测试网9FPgY |/p R9@

  (3)多次释放。在调试版本中,如果多次删除同一指针, 会收到_BLOCK_TYPE_IS_VALID断言错误。要防止此类错误,应在delete某个指向动态内存的指针后立即将其置为空。51Testing软件测试网 i!}YDL)E

|qU0A+gpsUx0  4、利用Windows结构化异常处理机制处理发布版本软件的内存崩溃

L(C5}c#w;J?0

l2t'KA3[m0a0  在程序的发布阶段,应尽量减少程序错误尤其是内存崩溃。如果崩溃了,应该“优雅”地退出,尽量收集程序崩溃时的运行信息以帮助程序供应商后续的 调试。要捕捉内存非法访问并获知非法访问的指令地址、寄存器内容等信息,需要用到Windows的结构化异常处理(Structured Exception Handling,SEH)机制[6]。MiniDumpWriteDump是dbghelp.dll提供的一个 API函数(参考MSDN),用于转储用户模式程序的一些信息(比如堆栈情况等)并存为一个文件(比如.dmp文件),此文件可以被微软的调试器 (VC++或者WinDBG)利用进行事后调试。使用此函数需要dbghelp.h、dbghelp.lib和dbghelp.dll(这些文件可以在 Windows Platform. SDK中找到)。51Testing软件测试网\eH}1FE8hE^H

51Testing软件测试网lw|)hc/Pz0P

  要事后根据.dmp文件调试代码,需要为发布版本软件产生debug symbols (pdb)文件(打开编译器/DEBUG选项)。在拿到.dmp文件以后,用VC++打开.dmp文件,然后调试执行(按F5键)。这样,崩溃现场就会重 现。文献[5]基于上述的方法实现了崩溃报告系统。51Testing软件测试网4w%QdVpPQdB

T~%_H(C)i;X0  5、结论

-NN}%b2@4R V F S-hu0

v~3b(p2]r/~\J K0  实践证明,在上述方法和工具支持下的减少软件内存缺陷的方法和工具,可以有效防止和查找代码中的内存错误和内存泄漏,并且能和开发人员日常编码 无缝结合,执行起来非常高效。上述方法配合单元测试、代码评审、每日构建、Bug追踪等措施,形成了一个高效的质量保证流程,在我们的大型平台软件开发过 程中起到了重要作用。51Testing软件测试网\"P` xx&Fp


TAG:

 

评分:0

我来说两句

Open Toolbar