天天看點

堆棧溢出分析

        在項目開發過程,我會經常檢查的一些問題,導緻程式崩潰。隻是前一段時間,測試組的回報現象,當處理用于尋呼機的特定功能,經過約半小時進行,該計劃将導緻崩潰,反複試驗幾次後,均高于現象。對崩潰日志的初步分析。我發現。盡管每次崩潰的地方一緻。但通過閱讀源代碼,我發現那些地方通常是不會出現故障的,難道讀代碼不夠細緻,我仔細緻細地分析了一遍發生崩潰的上下文,都是很正常的調用情況。難道又出現了其他子產品的幹擾,造成主線程記憶體錯亂了,常常查找崩潰的人都知道。假設出現了這種情況,那查找起來無異于登天,盡管有豐富的定位崩潰的經驗,但每次遇到這種問題,你的經驗往往僅僅能起到一丁點的輔助作用。

        就在我一籌莫展的時候。測試人員邊進行拷機,邊随口說了一聲,“這個窗體怎麼我剛移動了位置,如今又回到了原來位置?”。每當山窮水複疑無路的時候。不論什麼一絲的細節都可能成為突破的關鍵,我說。“可能新彈的窗體又把原來的窗體置位了吧。”。測試人員便應聲到,“這樣互動好像不是非常友好吧!”,而此時我關心的并非互動的問題,而是這個崩潰何解。便任意答應了一下,說“那我去看一下代碼。”

        打開源代碼。簡單幾句代碼出如今了眼前:

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)行了代碼的改動。

        堆棧溢出的問題有時是很頭疼的。而像這樣的由特殊業堆棧溢出的服務間接原因是罕見,并檢查珍惜。