天天看点

堆栈溢出分析

        在项目开发过程,我会经常检查的一些问题,导致程序崩溃。只是前一段时间,测试组的反馈现象,当处理用于寻呼机的特定功能,经过约半小时进行,该计划将导致崩溃,反复试验几次后,均高于现象。对崩溃日志的初步分析。我发现。尽管每次崩溃的地方一致。但通过阅读源代码,我发现那些地方通常是不会出现故障的,难道读代码不够细致,我仔细致细地分析了一遍发生崩溃的上下文,都是很正常的调用情况。难道又出现了其他模块的干扰,造成主线程内存错乱了,常常查找崩溃的人都知道。假设出现了这种情况,那查找起来无异于登天,尽管有丰富的定位崩溃的经验,但每次遇到这种问题,你的经验往往仅仅能起到一丁点的辅助作用。

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

        打开源代码。简单几句代码出如今了眼前:

bool XXX::YYY( WPARAM wParam, LPARAM lParam, bool& bHandled )
{
  HWND hWnd = CConfCtrlLogic::Instance()->GetApplyChimeRspWnd();
  if ( ::IsWindow( hWnd ) && ::IsWindowVisible( hWnd ) )
  {
    SendMessage( hWnd, WM_CLOSE, 0, 0 );  
  }

  CStdString strInfo = wParam ? STRING_JOIN_DISCUSS_SUCC : STRING_JOIN_DISCUSS_FAIL;
  CMessageBoxDlg dlg( IDR_XML_MSG_NOTIFY_DLG, 255, false );
  dlg.EnbaleAutoClose( FALSE );
  dlg.SetInfo( strInfo, STRING_TIP, g_pMainLogic->GetMainHwnd(), ID_OK );
  dlg.Create( g_pMainLogic->GetMainHwnd(), STRING_TIP, UI_WNDSTYLE_BOX, WS_EX_TOOLWINDOW );
  CConfCtrlLogic::Instance()->SetApplyChimeRspWnd( dlg.GetHWND() );
  dlg.CenterWindow();
  dlg.ShowModal();
  bHandled = true;
  return true;
}      

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

                1:假设有弹出框而且弹出框是显示的。就发个 WM_CLOSE 消息,关掉它。

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

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

UINT CWindowWnd::ShowModal()
{
  ASSERT( ::IsWindow(m_hWnd) );
  UINT nRet = 0;
  HWND hWndParent = GetWindowOwner( m_hWnd );
  ::ShowWindow( m_hWnd, SW_SHOWNORMAL );
  ::EnableWindow( hWndParent, FALSE );
  MSG msg = { 0 };
  while( ::IsWindow(m_hWnd) && ::GetMessage(&msg, NULL, 0, 0) ) 
  {
    if( WM_CLOSE == msg.message && msg.hwnd == m_hWnd ) 
    {
      nRet = msg.wParam;
      ::EnableWindow( hWndParent, TRUE );
      ::SetFocus( hWndParent );
    }

    if( !CPaintManagerUI::TranslateMessage(&msg) ) 
    {
      ::TranslateMessage( &msg );
      ::DispatchMessage( &msg );
    }

    if( WM_QUIT == msg.message ) 
    {
      break;
    }
  }
  ::EnableWindow( hWndParent, TRUE );
  ::SetFocus( hWndParent );
  if( WM_QUIT == msg.message ) 
  {
    ::PostQuitMessage( msg.wParam );
  }

  return nRet;
}      

        通过代码能够看到,消息循环会推断是不是窗体,而且取出一个消息,然后处理掉,当接收到 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 再创建,周而复始。却一直没有退出过一个消息循环,当然也就没有返回过函数。我们知道,函数仅仅调用不返回,当然堆栈会一直向下涨,最后造成堆栈溢出。

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

        堆栈溢出的问题有时是很头疼的。而像这样的由特殊业堆栈溢出的服务间接原因是罕见,并检查珍惜。