程式調試手記—解決Stack Overflow問題

來源:互聯網
上載者:User

前言

程式員最痛苦的事莫過於深陷於BUG的泥潭,我也沒少在這上面摔跤。這裡,我把自己的一些經驗教訓總結出來,涉及的內容包括死迴圈、死結、記憶體流失以及記憶體訪問錯誤等,如果能對朋友們有所協助,那就再好不過了。不過,我不打算按照循序漸進的方式來撰寫這些文章 ,而是想到哪寫到哪,也許到最後才會形成一個完整的系列。

本節將以一個真執行個體子講述如何在VC6環境下調試“Stack Overflow”錯誤。

問題浮現

我負責維護前任同事的開發的一個DLL,這是一個用於網路通訊的中介軟體,今天在應用程式斷開與伺服器的串連時突然報錯,而且屢試不爽。調試發現出錯總是在一個類的解構函式中出現,其類似代碼如下:

class Bar
{
public:
    ~Bar()
    {
        stringstream ss;
        ss << "~Bar" << 123;
        cout << ss.str(); // 這裡 出錯
    }
};

出錯時的函數調用棧為:

memcpy(unsigned char * 0x1da822a9, unsigned char * 0x00000000, unsigned long 1) line 331
std::char_traits<char>::copy(char * 0x1da822a9, const char * 0x00000000, unsigned int 1) line 194 + 20 bytes
std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign(const char * 0x00000000, unsigned int 1) line 134 + 20 bytes
std::basic_string<char,std::char_traits<char>,std::allocator<char> >::basic_string<char,std::char_traits<char>,std::allocator<char> >(const char * 0x00000000, unsigned int 1, const std::allocator<char> & {...}) line 48 + 43 bytes
std::basic_stringbuf<char,std::char_traits<char>,std::allocator<char> >::str() line 36 + 73 bytes
std::basic_stringstream<char,std::char_traits<char>,std::allocator<char> >::str() line 262 + 31 bytes

可以看出,basic_stringbuf在執行str()時,將NULL指標傳遞給了basic_string的建構函式,最終導致後來的memcpy出錯,而理論上這裡的指標是不該為NULL的。儘管VC6使用的STL庫存在不少問題,但這樣的代碼無論如何也不該出錯啊!難道是記憶體被改寫?但從其它成員變數的值看這種可能性很小。

疑點重重,為了弄清真相,我寫了前面那段代碼對STL進行調試和分析。地球人都知道P.J. Plauger這傢伙寫的STL那叫一個亂,搞不懂他都是怎樣記那些變數名的,代碼排版也是一個團糟。花了半個上午,總算弄明白了,結論是STL沒有問題,為此我還動用了google以確認有沒有人存在同樣的問題。

這下讓我更加鬱悶,如果是記憶體覆蓋那就非常麻煩了,因為根據以往的經驗,要在這樣一個龐大的多線程程式中找出是哪個地方改寫了記憶體那簡直是比登天還難。(後來證明我在此犯了一個小錯誤,那就是忽略了函數調用棧的深度)

出現轉機

事情到後來出現了轉機,同樣的程式在另外一個同事的機器上報錯時給出了一條重要訊息:“Unhandled exception xxx.exe: 0xC00000FD: Stack Overflow.”。

這相當於指明了問題所在,不過我還是有一點小小的疑問,為什麼我的機器上沒有這樣的錯誤提示呢? 沒有提示,或許是系統內容的問題,我有辦法讓問題出現,於是F5運行程式,選擇菜單“Debug/Exceptions”,在列表框中找到“Stack Overflow”,Action改為“Stop always”,如下:

在執行同樣的操作後,果然出現了“Stack Overflow”異常。

接下來,我便把注意力集中到函數調用棧的分析上。按快速鍵Alt+F7調出Call Stack視窗(或者通過菜單“View/Debug Windows/Call Stack”進行選擇),看得出調用棧確實很深,由於視窗顯示的內容有限,都找不到第一個函數了。

發現問題

剛開始以為是存在死迴圈,但仔細查看後並沒有發現問題,程式的的執行很正常。再一次對調用棧和相關代碼進行深入分析後,我終於發現了端倪。原程式使用了boost裡的智能指標來構建訊息佇列,當訊息佇列過長時,最後析構的時候就會造成調用層次過深,為此,我又編寫了如下一段代碼作測試:

#include <iostream>
using namespace std;

#include <boost/shared_ptr.hpp> // 使用boost庫的智能指標
using namespace boost;

struct Message
{
    Message(int index = 0)
    {
        m_index = index;
    }
    ~Message() // 解構函式
    {
        cout << "~Message: " << m_index << endl;
    }
    
    shared_ptr<Message> m_pNext; // 指向訊息佇列中的下一個訊息
    int m_index;
};

int main(int argc, char* argv[])
{
    shared_ptr<Message> pHead = shared_ptr<Message>( new Message(0) );
    shared_ptr<Message> pCur = pHead;
    for(int i=1; i<2000; ++i) // 這裡構建一個長度為2000的訊息隊 列
    {
        shared_ptr<Message> pNext = shared_ptr<Message>(&n bsp;new Message(i) );
        pCur->m_pNext = pNext;
        pCur = pNext;
    }
    return 0;
}

編譯後F5運行,“Stack Overflow”果然出現,這時我的心便一下輕鬆下來。

如果不親自試一試,你很難察覺上面的代碼有什麼問題。 事實上,這確實是一段很正常的C++程式,從邏輯上分析絕對沒有問題,那錯誤是如何發生的呢?

這得從C++的解構函式說起,先回顧一下以下知識:

1. 解構函式會在對象生命期結束時被自動調用。
2. 含有成員變數的類在自身解構函式調用結束後,將按照成員變數聲明順序的逆序依次調用其解構函式。
3. 具有繼承體系的類在自身解構函式調用結束後,將按照基類聲明順序的逆序依次調用其解構函式。

再來分析前面的代碼,當main函數執行到return 0時,智能指標pCur的解構函式會被調用,這沒有什麼問題,接著pHead的解構函式被調用,由於這時pHead所指向的Message(1)的引用計數降為0,所以它會被釋放,因而其解構函式被調用,根據規則2,接下來 Message(1).m_pNext會析構,這樣便開始了訊息佇列的遍曆,下面是一個周期的函數調用棧:

Message::~Message() line 53 + 8 bytes
Message::`scalar deleting destructor'(unsigned int 0x00000001) + 37 bytes
boost::checked_delete(Message * 0x0044e920) line 34 + 28 bytes
boost::checked_deleter<Message>::operator()(Message * 0x0044e920) line 52 + 9 bytes
boost::detail::sp_counted_base_impl<Message *,boost::checked_deleter<Message> >::dispose() line 265
boost::detail::sp_counted_base::release() line 147 + 13 bytes
boost::detail::shared_count::~shared_count() line 382
boost::shared_ptr<Message>::~shared_ptr<Message>() + 40 bytes

這一切都是那麼合情合理,但是我們不能忽視函數棧空間的大小。學過編譯原理就知道,函數棧空間是用於存放局部變數、函數返回地址以及函數參數等資料的記憶體地區,其大小是有限的(VC6預設是1MB)。當局部變數佔用空間太大,或者函數調用層次太深就會出現“Stack Overflow”的情況。最經常出現的錯誤有以下兩種:

1. 局部陣列變數空間太大,如下:

int main(int argc, char* argv[])
{
    char stack_overflow[1024*1024*5];
    stack_overflow[0] = 1;
    return 0;
}

解決這類問題的辦法有兩個,一是增大棧空間(後文中有詳細描述),二是改用動態分配,使用堆(heap)而不是棧(stack)。

2. 函數出現無限遞迴調用,如下:

void infinite_loop()
{
    infinite_loop();
}

int main(int argc, char* argv[])
{
    infinite_loop();
    return 0;
}

實際應用中,誰也不會直接編寫如此愚蠢的代碼,但往往由於粗心大意或者函數間的相互調用而造成了這樣的結果。解決的辦法當然是消除BUG。

解決辦法

回過頭來,再來看我們的問題,其原因主要還是由於忽略了boost智能指標在析構時的影響而造成的(這確實容易容忍忽視)。明白了問題所在,解決問題的方法便很多,如下:

1. 增大棧空間

調出“Project/Settings/Link”選項卡,選擇Output,其中的Stack allocations的reserve值便是棧空間所用大小(見),VC6中預設為1MB,根據實際情況將其加大然後重新編譯即可,具體說明可參見MSDN中的/stack選項。這個方案對於一些問題來說簡單可行,但不能滿足我這裡的需求。

這裡再多提一點,增大棧空間還有一個更簡單的方法,那就是使用VC附帶的EDITBIN工具,它可以直接增大可執行程式的棧空間,而不用重新編譯器,其使用方法如下:

EDITBIN /STACK:reserve[,commit] [files]

2. 限制隊列長度

這個方法不能滿足我的應用程式需求,因此不可行。

3. 改用其它方式實現訊息佇列

即不使用boost::shared_ptr,改用原始指標或者std::list來構建訊息佇列。但我的程式的模型比前面給出的測試代碼要複雜得多,牽扯到其它方面的因素,因此該方法也不太可行。

4. 斷開訊息佇列

這是我最後採用的方案,析構時的迭代之所以會發生,是源於boost::shared_ptr的引用計數原理,只要將訊息鏈斷開就不會有這樣的問題 。針對前面的測試例子,只要在return 0前面加上如下代碼即可避免“Stack Overflow”:

    pCur = pHead;
    for(i=1; i<2000; ++i) // 依次斷 開訊息鏈
    {
        pHead = pCur->m_pNext;
        pCur->m_pNext.reset();
        pCur = pHead;
    }

總結

本節內容雖然是根據一個實際例子提出的,但是其解決方案還是具有一定一般性,比如通過Link選項增大棧空間以及使用EDITBIN增大可執行檔棧空間等。

另外一個收穫就是要特別注意對智能指標構建的隊列的析構處理。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.