分类:
2008-10-13 16:31:07
除虫记之一(MFC的配对函数:4小时/2人)
网络猪新版上线之前,频频出现debug版本下Assert断言错误,进而引发崩溃。现在并不是每次都能重现,出现时的调用堆栈也不尽相同。下午2点开始和张义臣一同追查这个bug。
首先查找重现规律,在此之前,我已经初步得到一个重现规律,就是让前进后退的工具栏按钮重绘一次就能出现。
根据这个规律,基本上80%能出现。
重现操作n次,查看堆栈,凡是出现这个错误的时候,最后都是到了某个CDC的析构里面,在CDC::RemoveHandle()的时候出现了Assert错误,有的是CPaintDC,有的是CWindowDC,有的是内存兼容DC。
把堆栈往前翻,发现界面的最后一次调用也不尽相同,有CTitleBar的OnPaint(),有CToolBarEx的OnPaint(),有BCG的工具栏的OnNCPaint()。
没办法,挨个的审核相关代码,发现这些调用中没有什么可疑的代码,再继续重现操作。
做了n次的重现操作,在一次操作中,崩溃发生了变化,出现在了IExplorerEx的OnPaint的CString的Release()中,
相关代码如下:
CString str;
str.Format("% d", (int)(m_iCurProgressPos*100));
str = (!m_bProgressOver) ? str+"%" : "";
根据我以往的经验,CString对象使用不对,很容易出现内存问题,(我一直劝告尽量不要使用CString),很自然而然的想到会不会又是CString出了问题。
这段代码写的也够有个性的了,和相关开发人员沟通后,提出了修改的建议(后来没有执行,ft)。
让程序在这段代码之前就return,重试!kao,现象还在,看来不是这个问题。虽然不是这个CString的原因造成的错误,但既然有一次崩溃出现在了这个位置,就说明这个位置使用CString还是有隐患的,切记,慎用CString。
继续重现操作n次,其中一次出现在读取配置文件之后,工程中读取配置文件采用的是内存方式,是用stl实现的,用到了basic_string类型,于是怀疑是不是这个可变类型在读写的时候出了异常导致缓冲区溢出。可是查看stl的堆栈实在是费劲,一个个的模板看的眼花缭乱,于是,让另外一个同事把这部分功能隐掉做测试看是否能重现操作。我们这边继续重现审核代码。
重现操作n次,仍然没有头绪。开始静下心来思考讨论。
DC的RemoveHandle操作崩溃,什么原因能造成这种现象?内存泄漏?内存被冲?GDI泄漏?GDI释放出错?
挨个的思考之后,认为GDI资源释放可能会出问题,因为在原来写ATL控件的时候曾经碰到过释放系统内定资源后造成系统错误,事后的教训是msdn上说释放系统内定资源系统将不予理会这个观点是错误的,系统还是会理会的,所以不能释放系统内定gdi资源。
现在是不是也有人释放了系统内定资源呢?
查看所有可疑代码,没有发现这种情况,倒是发现了工程中有很多GDI资源没有释放的bug,真是有心栽花花不开,无心栽柳柳成荫,记录下相关bug,mail给开发人员去完善。
再次停下来讨论,既然是DC错误,会不会是DC的释放或者删除出错了呢?
搜索工程中ReleaseDC()调用,总共发现27出调用,挨个的审核,只发现一处用CreateCompitableDC创建的内存DC在DeleteDC()前ReleaseDC()了一把,理论上GetDC()得到的DC需要ReleaseDC(),创建的DC是不需要ReleaseDC的,这行代码多余,但不应该出错,为安全起见,删除!还发现一些MFC的CDC派生类无需要做ReleaseDC的也做了这个调用,也删除,重编译,错误仍然存在!tnnd,真顽固啊。
停下来,再讨论,想起来,在3天之前没有这个现象,是不是刚添加的代码或文件造成的这个现象?
可是,工程中几百个文件,几十人在维护。。。。。
在重现操作的过程中,重现规律有点变成了:好像是刷新上面的搜索框才会出错。
把这两天添加的文件EditWithTip从vss上取一个最初的版本,结果。。。。。编译通不过!
审核里面的代码,这个类是从CEdit派生的,里面的代码很少,只处理了WM_PAINT,WM_NCPAINT,WM_SETFOCUS,WM_KILLFOCUS,WM_SETTEXT消息,发现里面也用到了CString,出于对CString的恐惧,换成了char数组,现象还是有!
难道不是这个类的问题??
不甘心,重取这个类的最初代码,想办法编译成功,。。。。。TMD,错误现象没有了!!!,想起来陈总的一句话,这幸福来的太晚了点。
哈,鉴于这个类是张义臣亲自写的,最初的审核很粗略,这次可要好好的、认真的、仔仔细细的审核里面的代码喽!
里面没有审核的代码就是下面的,你看看有什么错误
case WM_ERASEBKGND:
{
CString str;
GetWindowText(str);
if (str.IsEmpty() && !m_bFocus )
{
CDC *pDC = CDC::FromHandle((HDC)wParam);
CFont *pOldFont = pDC->SelectObject(CPigFont::GetFont());
pDC->SetTextColor(m_dwColor);
pDC->TextOut(0, 0, m_strTip);
pDC->SelectObject(pOldFont);
pDC->Detach();
}
else
{
RECT rc;
GetClientRect(&rc);
CDC *pDC = CDC::FromHandle((HDC)wParam);
pDC->FillSolidRect( 0, 0, rc.right, rc.bottom, RGB(255,255,255));
pDC->Detach();
}
}
哈,当时看到这段代码的时候,就随口问了一句,你这个pDC为什么要Detach掉呀?没有Attach是不用Detach的啊。
说完这几句话,我们两个同时就下了结论,就是它惹的祸!注释,编译,运行,OK!
问题解决了!
原因是张义臣从其他地方把代码拷过来后没有认真审核就放上去了。
原因找到了,我们再来看看CDC的Detach到底做了些什么?
下面是CDC的Detach的实现代码
HDC CDC::Detach()
{
HDC hDC = m_hDC;
if (hDC != NULL)
{
CHandleMap* pMap = afxMapHDC(); // don't create if not exist
if (pMap != NULL)
pMap->RemoveHandle(m_hDC);
}
ReleaseAttribDC();
m_hDC = NULL;
return hDC;
}
看到了吧,这个里面把对应的HDC给删除掉了,这样就改变了afxMapHDC()这个全局的映射表了,下一个DC析构的时候当然会出问题啦。
教训:
1、崩溃发生在DC的相关处理中,首要考虑是不是全局的映射表是不是乱了
2、使用MFC的相关调用的时候,一定要注意有关的调用一定是配对的。比如这次的就是因为使用了Detach()在没有Attach()与之对应的情况下。