009-09-16 19:02 Free Heap block xxxxxxxx modified at xxxxxxxx after it was freed

 方法一:如果你是C++程序員,如果你寫過一個很複雜的程序,如果你經常碰到莫名其妙的崩潰問題。那麼你就有可能遭遇了野指針。如果你比較細心,注意了Debug output輸出窗口的話,那麼你就有可能注意到這樣一行提示:

HEAP:   Free   Heap   block   xxxxxxxx modified   at   xxxxxxxx after   it   was   freed

網絡上關於這個問題提問的人不少,但是真正給的出答案的卻少之又少。在網上搜索了幾天,終於發現了這個問題的解決之道。下面我來介紹一下。

 

GFlags是windows debug tools 工具包下的一個工具,在Windows 2000的Resource Kit中也可以找得到。用來設置一些調試屬性,總體上分爲3個級別System, Kernel, 和 Image File。

我們設置好Path環境變量,將其指向Debug tools工具的目錄下。

輸入如下命令行:

gflags /p App.exe /full

或者 pageheap app.exe /full

該命令行會在註冊表裏設置一些調試參數,使內存在使用的時候加入了保護機制,所以一旦內存寫越界,或者發生野指針的問題,都會導致一箇中斷。由此,你就可以確定問題到底出在哪裏了。

方法二:

多線程與內存(heap)

關於多線程中的內存問題,必需足夠地重視,否則,程序總是會出現莫名其妙的錯誤。如果幻想某些情況“應該”不會出現,或者是認爲某些步驟是按照正常的順序下來的,那就錯誤大大地,因爲有一個簡單的事實,就是你沒有遵守事實,想和CPU對着幹!

xx SDK的報錯退出問題,從去年直到現在都沒有得到有效的解決,近段時間,被頻繁地爆露出來,是到了不得不直面的時候了,所以,前兩週,去了xx N次,直到現在,還不能確定問題是否真地解決了

SDK的現象:
在自己的測試demo上沒有問題,而在平臺上就有問題
後來的測試證明,不是demo沒有問題,而是原來的demo只是在跑自己測試的設備,其數量少,環境不真實,實際的情況無法得到有效的測試,所以,demo不會有任何問題

直接運行程序,就是出現調試的那種錯誤,在平臺的vc debug模式下,出現的是觸發了用戶斷點的錯誤。在SDK的vc debug模式下,爆露了錯誤的本質:
HEAP[pingtai.exe]: HEAP: Free Heap block 2be3000 modified at 2be3200 after it was freed
這種錯誤過去從沒有遇到過,經查,表明是因爲堆被破壞而產生錯誤。種種跡象表明,是因爲某個內存被刪除之後,其內容又被修改了,這樣就是破壞了堆,就出現了這種錯誤,模擬程序如下:

void fun1()
{
char *p = new char[128];
delete[] p;

strcpy( p, "abcdefghijklmnopqrstuvwxyz" );
}

在我模擬的時候,其情況如下:
1. 如果我strcpy()的字節少於13個字節,則不會報任何錯誤。此情況並不絕對
2. 當fun1()返回時,並不出錯,當程序退出時,報錯
3. 在fun1()返回後,再調用其它任何函數,即會報錯

爲方便後面的敘述,把錯誤的描述如下:
HEAP[<exe>]: HEAP: Free Heap block <addr1> modified at <addr2> after it was freed

對模擬程序的報錯進行分析,得出如下結論:
對內存<addr1>進行了刪除,然後,又修改了內存<addr2>處的內容,其中,<addr2>是被包含在<addr1>中的,形式如下:
|------------------------------|
| <addr1> | <addr2>           |
|------------------------------|
在模擬程序中,<addr1> = p,<addr2> >= p 且 <addr2> < p + 128
在真實的內存中,<addr1>並不是等於p,而是會比p小,其不表示程序代碼中的任何一個內存,而是在new時,用於保存p的內存而產生的內存指針,該指針由windows保存和維護(注:個人認爲,通過計算,應該是可以得到p的地址),實際的模式如下:
|------------------------------|
| <addr1> | p                 |
|------------------------------|
<addr1>是windows佔用的內存,而p是程序佔用的內存

既然內存的操作已經明確,而且也知道了如何產生了這種錯誤,那麼就要根據<addr2>來找是程序中哪個地方出現了這種情況,採用倒推 的方式,即首先根據<addr2>找到是哪個內存被刪除後又重新進行了附值(或者是其中的某處被重新附值),然後再根據情況來使刪除和附值不 要衝突

程序的順序應該沒有問題,因爲都是刪除後就不再進行附值操作,或者是其它任何使用的可能了,所以,從需要分配內存(主要是char*類型的內存分 配)變量開始找起。把所有new出來的char*指針地址及其長度打印出來,從上萬行的打印信息中一個一個查找,卻發現沒有<addr2>
這就奇怪了,難道是程序的執行順序出現了問題?不可能吧
把所有new出來的類地址及其內存範圍進行打印,再一行一行地仔細找,汗......
竟然是CAccept類出現了這種情況!難道其刪除之後,還有被使用的情況發生?根據以上內存的分析,肯定是有這種使用情況存在,否則....,找!
因爲CAccept的類使用較少,找起來也比較容易,主要就在兩個地方使用,一個是完成端口的死循環中,一個是任務檢查的死循環中,肯定是這兩個地方的使用有衝突。
完成端口的工作模式:
while( true )
{
if ( !getcompleteio() )
break;
if ( isaccept() )
{
CAccept *pacc = getaccept(); // 從列表中獲取
// 使用pacc,其中有附值操作
... ...
// 刪除pacc
SAFE_DELETE(pacc)
}
else
{
// 數據收發及socket端口的其它消息處理
}
}

任務檢查的工作模式:
while ( true )
{
wait(5000); // 等待5秒鐘,開始任務檢查
lock();
while ( not_end_accept_list )
{
CAccept *pacc = nextaccept();
if ( pacc->tooLongTime )
SAFE_DELETE(pacc) // 刪除 pacc
}
unlock();

// 連接檢查
...
}

對accept的操作子函數(比如createaccept()、getaccept()、deleteaccept()等),都是被放入臨界區中 的,這裏仔細檢查發現,完成端口中對CAccept的使用沒有被放入臨界區中,雖然其getaccept()中有臨界區的操作,雖然任務檢查中對 accept列表的操作也是在臨界區中進行的,那麼,問題肯定就是出現在了這裏。爲了驗證,對任務檢查處的accept刪除和在完成端口中對accept 的使用進行信息打印,發現,當完成端口中還沒有對其獲取的CAccept使用完時,已經被任務檢查的循環刪除了!
這種情況在正常情況很難出現,爲 了使它容易出現,對臨界區的操作加了延時,即人爲使每個操作都等待較長的時間(原來是沒有任何等待的),比如100毫秒,或者50毫秒等,這樣,可以使很 多的操作都被掛起,在設備的連接達到一定多的時候,這種掛起的操作會越集越多,採用這種方法,可以使一些問題較容易地復現出來
找到問題了,解決起 來應該是比較容易的了(有些可是棘手的,比如完成端口中else分支裏的socket消息的處理),只需要在完成端口中把對CAccept的使用也放入相 應的臨界區中即可,修改完畢,測試,再也沒有CAccept的堆問題了,至此,該部分的問題已“完美”地被解決了,那個痛快啊---------,就別提 了... ...

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
還有兩個問題,一個是關於上層對SDK的調用問題,一個是SDK內部的問題。
這兩個問題,都和以上的問題的本質一樣,就是刪除了後還在使用!
1. 上層對SDK的調用
SDK 已經通知了上層設備斷開,但是,上層在得到該消息之後,竟然還在調用相應的接口。這本身不成問題,因爲上層調用SDK的時候,SDK會從相關的設備列表中 獲取相應的連接,然後拿這個連接對設備進行操作,如果是該設備已經斷開,那自然就會從相應的列表中刪除掉,那麼在上層調用的API中獲取連接時,肯定是獲 取失敗,SDK在判斷後,自然直接返回失敗。沒錯!你說得太對了,就是這樣。...... 停!你想,仔細地想一想,然後再往下看
================================
在 SDK中,不知道上層對SDK的調用和消息接收,是在一個線程中還是兩個線程中,那麼,SDK無從知道上層對SDK的消息處理和API調用的先後順序,有 一種情況,應該是比較容易理解的,當SDK向上層進行了很多的消息通知,上層可能是會把這個消息放入消息隊列中,然後一個一個地慢慢處理,其處理的某一個 消息,也許是10秒鐘之前的消息(有點誇張),如果是這樣,而且如果剛好這個消息就是連接斷開的消息,那麼在其處理該消息之前的3秒鐘,用戶要對該設備下 發命令,用戶看到的結果就是,該設備在線,即下發命令失敗,而且顯示的錯誤消息是設備不在線!這樣,程序也不會出錯,因爲7秒了,太久了,SDK應該是已 經把這個連接從連接列表中刪除了。
但不幸的是,並不總是這種情況,而是有一個糟糕的情況可能會出現。當API函數中獲取到相應的連接後,正在向設 備下發命令時,連接列表被任務檢查中的連接檢查循環刪除了(比如有1分鐘都沒有收到任何數據),這時,如果下發命令的函數中是讀取,而不進行任何的附值操 作,則出現的錯誤,應該是內存非法讀的異常,否則,出現的錯誤,應該是和上面一樣的堆錯誤。這種錯誤,也許解決起來比較容易,比如在使用時,也放入臨界 區,這樣當然可行,只是當有100個API時,你就必需得進行100次臨界區的操作,這不算重要,更重要的是,由於你在最外層增加了臨界區的操作,那麼在 SDK的內部,必需要小心地保證臨界區的操作不會產生衝突,否則,就會發生程序無反應的情況,需要使用任務管理器來結束的後果。如果其中有附值操作,這也 許是唯一的一種解決方法,但如果是沒有附值操作,則可能會比較簡單,比如,我只在那時出現了這個錯誤的API外層,增加了 try...catch()...,這樣,如果執行的中間其實例被刪除了,則該API直接返回失敗。具體的解決方案,可以根據實際情況的不同,進行不同的 處理,這裏,只是闡明我自己的想法而已
2. SDK內部的另外一個問題
和CAccept完全一樣,只是其複雜度相當大,由於其幾乎涉及到程序的所有地方,所以,解決起來相當棘手,而且,由於其和要上層通訊,則一不小心,就會使臨界區衝突(除了上層的代碼之外,SDK中的代碼都被放入臨界區):
[收到socket消息] --> [通知上層] --> [上層調用SDK API] --> [API調用SDK內部的函數]
這個過程中,因爲SDK通知上層後,需要等到上層返回,才進行下一步處理,上層需要調用API的返回後自己才返回,而API調用內部函數時候,可能需要先進入臨界區才能操作,如此便出現了臨界區的衝突。
所以,在處理收到的socket消息時,被放入臨界區是比較危險的做法,所以,這裏如果要找到一個好的解決方案,比較棘手

由於時間問題,暫只淺談之.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章