51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

手机号码,快捷登录

查看: 5061|回复: 4
打印 上一主题 下一主题

C++程序的内存故障定位和预防

[复制链接]

该用户从未签到

跳转到指定楼层
1#
发表于 2007-8-23 15:50:54 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
由于我们的程序是使用C/C++开发的,而其最大特点就是内存的使用由我们自己管理,并且程序可以直接访问到程序的内存区而没有任何保护,所以内存问题是我们经常遇到的一类问题,其中包括:内存访问越界,内存泄漏等。本文就这个问题进行分析,以便能够对大家在问题出现时能够较快的定位,并且在平时的编码过程中提供一些方法来避免这些问题的出现。
下面就主要根据程序对内存的使用、内存故障的定位和防止办法来进行阐述。
一,内存空间的分配使用
  总体上来说,程序(加载到内存后称为进程)在被操作系统加载到内存(虚拟内存)时,程序的各个部分按段(Section)或者节(Segment)指定到进程空间的各个内存地址,其中主要包括数据区、代码区,和进程依赖的操作系统内核的库的代码区等;而进程在运行时需要其他的内存区,主要有栈空间区、堆空间区等。
  
其中“数据区”、“栈”和“堆”就是我们程序数据所在地,对他们的访问和使用不当就会导致内存故障。下面就按这三种数据根据实际情况来对内存故障进行分析。
二,各种情况下的导致的内存异常和定位方法
A.数据区内存故障
数据区存放的是我们程序中的全局变量,静态变量;类的实例如果为全局对象 则其类变量(包括虚函数表)也是存放在数据区的(相当于一个全局结构变量)。
这类故障引起的问题如果不能调试的话一般比较难以定位,如果是在调试方式下可以通过设置“内存改变断点”进行定位。下面就用我们遇到的一个例子加以说明:
IMF进程在某些情况下会Core在AttrIdToParaId函数中,而这个函数在库utility中,已经很长时间没有改动,并且它的代码比较简单,就是对全局变量g_aAttrParaId进行遍历,也不可能越界,除非这段数据区被破坏。而对这段数据进行分析我们可以看到,这个数据与CString的初始值的内存位置是紧挨着的,很可能就是CString使用不当导致g_aAttrParaId数据区破坏。
(dbx) print &_omcDataNil
&_omcDataNil = 0xff0a25a0
(dbx) x 0xff0a25a0 /20
0xff0a25a0: _omcDataNil       :  0xff0a25a8 0x00000000 0xffffffff 0x00000000
0xff0a25b0: _omcInitData+0x0008:       0x00000000 0x00000000 0x00010000 0x00610000
0xff0a25c0: g_aAttrParaId      :        0x00000064 0x544f5000 0x00000000 0x00000000
0xff0a25d0: g_aAttrParaId+0x0010:      0x00000000 0x00000000 0x00000000 0x00000000
0xff0a25e0: g_aAttrParaId+0x0020:      0x00000000 0x00000000 0x00000000 0x00000433

而在IMF日志文件中,在出现Core时,没有任何异常情况,而在15分钟前,有个性能入库的失败打印,而一般正常流程没有这个打印的,所以我们就在这个打印的代码附近进行排查,果然有个错误使用CString的地方:
  CString csExpErrorInfo[3], csDelErrorInfo[3];
sprintf((CHAR*)(LPCSTR)csExpErrorInfo[CS_HOUR],               
      (LPCSTR)omcLoadString(RES_COMM_EXP_CS_HOUR_FAIL),
        (LPCSTR)csFileName, (LPCSTR)csStartTime ,(LPCSTR)csStopTime);

   这段代码就是直接向初始的CString对象中,输入字符串,而这个初始的     CString对象(LPCSTR)操作返回的地址就是_omcInitData后面的地址。所以我们可    以肯定地说IMF的Core就是这段代码引起的。

   当然,如果是在调试状态,我们可以通过在内存g_aAttrParaId处设置内存改    变断点来进行扑捉向这个地址写内存的代码,这样定位的效率要快的多。
B.栈的内存故障
栈存在的是我们程序中使用的局部变量(自动变量),使用alloca申请的内存; 类的实例如果为局部对象则其类变量(包括虚函数表)也是存放在栈内(相当 于一个临时结构变量)。
这类故障的定位比较容易,因为它引起的故障所涉及的范围一般来说总是在当前函数内。比如CMIP中有个打印码流地代码:
            CHAR dump_buffer[40000];
            dump_buffer[0] = '\0';
            sprintf (dump_buffer + strlen(dump_buffer), (LPSTR)"CMIPM : ASend to Fsm " );
            PrintFsmFid( m_cIndFsmTable.tIndFsm, dump_buffer + strlen(dump_buffer));
            sprintf (dump_buffer + strlen(dump_buffer), (LPSTR)" SUCCESS, Msg %d, Len %d, Data is : \n", tMessage, (INT)nArgLength );
            PrintHexData( pbyOperation, nArgLength, dump_buffer + strlen(dump_buffer));
         
    当最后一个函数向buffer中填写的字符超过40000个时,便会引起内存故障,  一般都是Core的问题,当然有些情况会有些特殊,比如函数地址不对,这个也可   能是对象的虚函数表被破坏导致的。
    由于栈是有单个线程使用,所以如果这个线程有递规函数或者自动变量的数据  比较大,容易引起栈空间超出当前线程指定的栈空间,导致程序崩溃。
C.堆的内存故障
堆内存即使用new或者malloc类函数申请得到的内存空间。
这类故障最多的就是内存泄漏,和delete同一个指针两次。而一般delete同一个指针两次的定位比较容易,它会立即引起Core。对于内存泄漏我们现在最好的方法是使用purify工具进行检查。
D.多线程对内存使用的影响
由于我们系统使用了多线程的环境,所以对于多线程的影响我们也必须考虑,而实际上多线程影响最大的就是全局变量,而new申请的内存在运行时,对整个进程而言也是全局可见和可访问的,也可以当作全局变量考虑。我们系统在西安出现的IMF的Core问题实际上就是多线程环境下对全局数据没有保护所至,具体分析如下:
在安全管理接口中有如下代码:
CString& CSmfObjectList::getObjAt(SWORD32 index)
{
Static CString result;
SWORD32 firstBegin = 0;
SWORD32 nextBegin = 0;
result = "";
if (index<0)
  return result;
for (WORD32 i=0; i<(WORD32)(index+1); i++)
{
  firstBegin = nextBegin;
  nextBegin = m_objectListStr.Find(SPACEMARK_OBJ_OBJ, firstBegin+1);
  if (nextBegin<0) nextBegin = m_objectListStr.GetLength();
}
result = m_objectListStr.Left(nextBegin);
return result;
}
这个返回的result实际上是一个全局对象,虽然它在函数内部声明,但由于是static变量,在整个进程中只有一份(这一点请牢记)。所以当有多个客户端并行发起命令时,这个变量就会被多个线程同时操作,从而引起CString的成员变量不一致,导致内存故障。
同时++, --操作在多线程环境下也不是原子操作,原来的CString的实现就依赖于这个假定而进行引用计数的统计,导致在现场操作频繁和数据量大的情况下,还是会出现Core的问题。
E.编译器对内存使用的影响
C++编译器中pack的使用也会对我们的程序产生影响,因为不同方式的pack会导致数据结构的编排不一致,从而引起访问错误,特别是在进程间通讯时。
三,防止内存故障的方法。
a. 防止数组越界
   在有些情况下,我们一般会声明一个最大可能的数组,作为临时变量,但当条  件变化时,这个数组就可能不够使用,从而引起越界。所以对于数组我们必须特别  小心。同时可以使用 alloca函数在栈上申请内存,其大小是可以随需要而定的,   而且不需要释放,这样就比较容易适应变化。
b. 防止delete同一个指针两次
  养成一个好的编码习惯
       delete var;
   var = NULL;
     可以很大地减少这种故障的出现几率。
c. C++对象和C的指针的转换和使用
   一般来说,将C++对象转换成C指针后,是不允许被修改的,一般的operater   char* 等操作都是按const定义的,而C代码可以强制转换成非const,所以我们建  议是除去这种语句。
d. 防止内存泄漏
 使用Guard模式可以对一个scope内使用的堆内存进行保护。
 可以使用简单的引用计数模式对一般的在函数中传递的堆内存进行保护。
 在接口中遵循“谁申请谁释放”的原则,比如原来的Cmis接口,对于any类型的值,在设置时就是应用申请,而有Cmis负责释放,这种设计很容易引起内存泄漏的问题,当然效率是另外的问题。
 在接口中使用对象,不要返回被调函数申请的对象指针。
  例如安全管理的接口函数中有:
CSmfObject* CSmfObjectList::findObject(CString& string)
{
CSmfObject* ptSmfObject = NULL;
if(!string.IsEmpty())
{
  ptSmfObject = new CSmfObject();
  ptSmfObject->setObject(string);
}
return ptSmfObject;
}
这种接口也很容易引起内存泄漏,在C++的实现中,完全可以使用如下接口
BOOL CSmfObjectList::findObject(const CString& string, CSmfObject& obj);
e. 多线程的编程
   多线程的编程比想象的要困难的多,所以有个说法:多线程编程就是艺术。在  我们的系统中,应用一般不要使用全局变量,如果需要,一定需要考虑到锁保护,  正如安全管理中鉴权和授权的两个状态机,由于它们共用了一些全局变量,而没有  锁保护,导致Core的问题。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

x
分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏
回复

使用道具 举报

该用户从未签到

2#
发表于 2007-8-23 16:45:34 | 只看该作者
不错

谢谢分享
回复 支持 反对

使用道具 举报

该用户从未签到

3#
发表于 2007-9-5 10:10:00 | 只看该作者
谢谢分享
回复 支持 反对

使用道具 举报

该用户从未签到

4#
发表于 2007-11-23 16:43:01 | 只看该作者
谢谢分享
回复 支持 反对

使用道具 举报

该用户从未签到

5#
发表于 2012-9-10 22:57:27 | 只看该作者
不错

谢谢分享
回复 支持 反对

使用道具 举报

本版积分规则

关闭

站长推荐上一条 /1 下一条

小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

GMT+8, 2024-11-23 05:30 , Processed in 0.089126 second(s), 28 queries .

Powered by Discuz! X3.2

© 2001-2024 Comsenz Inc.

快速回复 返回顶部 返回列表