一次堆栈溢出的分析

原文地址:http://blog.csdn.net/henzox/article/details/32712891


        在项目开发过程中,我会经常查一些引起程序崩溃的问题。就在前段时间,测试组反馈了一个现象,当对某一个功能进行拷机的过程中,大约进行了半个小时之后,程序就会引起崩溃,经过数次的重复测试,均出现了以上现象。经过对崩溃日志的初步分析,我发现,虽然每次崩溃的地方一致,但通过阅读源码,我发现那些地方一般是不会出现问题的,难道读代码不够仔细,我仔仔细细地分析了一遍发生崩溃的上下文,都是非常正常的调用情况。难道又出现了其它模块的干扰,造成主线程内存错乱了,经常查找崩溃的人都知道,如果出现了这种情况,那查找起来无异于登天,虽然有丰富的定位崩溃的经验,但每次遇到这样的问题,你的经验往往只能起到一丁点的辅助作用。

        就在我一筹莫展的时候,测试人员边进行拷机,边随口说了一声,“这个窗口怎么我刚移动了位置,现在又回到了原来位置?”,每当山穷水复疑无路的时候,任何一丝的细节都可能成为突破的关键,我说,“可能新弹的窗口又把原来的窗口置位了吧。”,测试人员便应声到,“这样交互好像不是很友好吧!”,而此时我关心的并不是交互的问题,而是这个崩溃何解,便随意答应了一下,说“那我去看一下代码。”

        打开源码,简单几句代码出现在了眼前:

[cpp] view plain copy
  1. bool XXX::YYY( WPARAM wParam, LPARAM lParam, bool& bHandled )  
  2. {  
  3.     HWND hWnd = CConfCtrlLogic::Instance()->GetApplyChimeRspWnd();  
  4.     if ( ::IsWindow( hWnd ) && ::IsWindowVisible( hWnd ) )  
  5.     {  
  6.         SendMessage( hWnd, WM_CLOSE, 0, 0 );      
  7.     }  
  8.   
  9.     CStdString strInfo = wParam ? STRING_JOIN_DISCUSS_SUCC : STRING_JOIN_DISCUSS_FAIL;  
  10.     CMessageBoxDlg dlg( IDR_XML_MSG_NOTIFY_DLG, 255, false );  
  11.     dlg.EnbaleAutoClose( FALSE );  
  12.     dlg.SetInfo( strInfo, STRING_TIP, g_pMainLogic->GetMainHwnd(), ID_OK );  
  13.     dlg.Create( g_pMainLogic->GetMainHwnd(), STRING_TIP, UI_WNDSTYLE_BOX, WS_EX_TOOLWINDOW );  
  14.     CConfCtrlLogic::Instance()->SetApplyChimeRspWnd( dlg.GetHWND() );  
  15.     dlg.CenterWindow();  
  16.     dlg.ShowModal();  
  17.     bHandled = true;  
  18.     return true;  
  19. }  

        这个函数便是弹出框起始的地方,拷机会定时有消息产生,也就是这个函数会被定时调用,该函数的大致意思是:

                1:如果有弹出框并且弹出框是显示的,就发个 WM_CLOSE 消息,关掉它。

                2:新建一个弹出框,并调用ShowModal 显示。

        看似简单并且无差错的逻辑,却引起了我的警觉,我下意识地点开了 ShowModal 的源码,因为 ShowModal 在这个版本产生了无数个问题,纠其原因,就是 DUI 的 ShowModal 也接管了消息循环,并且做了简单的逻辑处理,代码如下:

[cpp] view plain copy
  1. UINT CWindowWnd::ShowModal()  
  2. {  
  3.     ASSERT( ::IsWindow(m_hWnd) );  
  4.     UINT nRet = 0;  
  5.     HWND hWndParent = GetWindowOwner( m_hWnd );  
  6.     ::ShowWindow( m_hWnd, SW_SHOWNORMAL );  
  7.     ::EnableWindow( hWndParent, FALSE );  
  8.     MSG msg = { 0 };  
  9.     while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) )   
  10.     {  
  11.         if( WM_CLOSE == msg.message && msg.hwnd == m_hWnd )   
  12.         {  
  13.             nRet = msg.wParam;  
  14.             ::EnableWindow( hWndParent, TRUE );  
  15.             ::SetFocus( hWndParent );  
  16.         }  
  17.   
  18.         if( !CPaintManagerUI::TranslateMessage(&msg) )   
  19.         {  
  20.             ::TranslateMessage( &msg );  
  21.             ::DispatchMessage( &msg );  
  22.         }  
  23.   
  24.         if( WM_QUIT == msg.message )   
  25.         {  
  26.             break;  
  27.         }  
  28.     }  
  29.     ::EnableWindow( hWndParent, TRUE );  
  30.     ::SetFocus( hWndParent );  
  31.     if( WM_QUIT == msg.message )   
  32.     {  
  33.         ::PostQuitMessage( msg.wParam );  
  34.     }  
  35.   
  36.     return nRet;  
  37. }  

        通过代码可以看到,消息循环会判断是不是窗口,并且取出一个消息,然后处理掉,当接收到 WM_CLOSE 消息后,在 WM_CLOSE 消息中我们的通常处理都会是关掉窗口,然后导致 IsWindow 判定失败,退出消息循环。也算是非常正常的逻辑。

        但结合上段代码的调用就会发现,这其中是有问题的。

        有几点要明确:

                1.  XXX::YYY 被调用的时候,肯定是在一个消息循环里;

                2.  当 SendMessage 传入 WM_CLOSE 的时候,会直接去调用窗口过程,并处理WM_CLOSE分支的业务,一般是销毁窗体,此时也还在XXX::YYY 被调用时的消息循环里;

                3.  但 SendMessage 返回,接着调用 ShowModal 时,依然是在前两步相同的消息循环里,一直没有出消息循环。

        问题就这样产生了,ShowModal 会创建一个消息循环,且 ShowModal 不返回,堆栈继续向下涨,当 XXX::YYY 相关的消息再产生的时候,就会在上一个 ShowModal 产生的消息循环里处理 XXX::YYY ,然后继续 SendMessage and ShowModal 再创建,周而复始,却一直没有退出过一个消息循环,当然也就没有返回过函数,我们知道,函数只调用不返回,当然堆栈会一直向下涨,最后造成堆栈溢出。

        回忆一下,测试描述的问题是,隔一定时间,程序必崩,这也符合了崩溃场景,因为拷机是以固定时间来产生这个消息,而调用环境相同,那么栈增长速度必然相同,栈大小固定,那么隔固定时间后,也必然会引起栈溢出,至此,这个崩溃被定位,那解决方法就很简单了,我们采用了一位同事的非常好的建议,像这种不需要用户确定的通知型消息,根本不用模态框来完成,所以直接使用了 Pop 框通知一下,最后也由那位同事(weilaitao)行了代码的修改。

        堆栈溢出的问题有时是非常头疼的,而像这种由特殊业务造成的间接性的栈溢出也是少见,且查且珍惜。


你可能感兴趣的:(一次堆栈溢出的分析)