01-0006 C++記憶體訪問越界 [問題整理]

雯飾太一發表於2020-10-19

1. 相關問題

在嘗試解決上述問題的時候,搜尋的關鍵詞如下:

  • ntdll.dll crash
  • VS未載入ntdll.pdb可能的錯誤原因
  • ntdll.pdb斷點
  • C++ delete出錯
  • C++動態陣列delete出錯
  • VS 觸發斷點,記憶體釋放異常
  • C++".exe" 觸發了一個斷點
  • C++動態陣列空間的釋放
  • C++delete偶爾出錯[rRelese模式]

2. 出現記憶體訪問越界或記憶體洩露可能的原因

2.1 沒有delete或者free

通過new或者malloc分配的記憶體空間沒有及時的釋放,或者釋放不當造成的記憶體洩露。

2.1.1 直接不釋放

函式體內申請記憶體,執行結束後不釋放[也沒有返回,提供其他位置釋放的可能性]

void fun(){
	int* a=new int[10];
}

2.1.2 釋放錯誤

釋放不夠徹底,delete使用方法錯誤[僅僅釋放了0號元素或第一層指標]

void fun(){
	int* a=new int[10];
	delete a;//僅僅釋放了0號元素
}

void fun(){
	int** a=new int*[10];
	for(int i=0;i<10;i++){
		a[i]=new int[10];
	}
	delete [] a;//第二層的空間並沒有釋放,應該迴圈釋放
}

如果在delete位置出現中斷,是因為將要被delete的這個指標指向的空間發生了訪問越界的情況,導致了堆被破壞,從而使得delete失敗

2.1.3 釋放void*

釋放void* 導致解構函式不能被正常呼叫,這種情況有可能出現:

void fun(){
	void* pVoid = new int(2);
	delete pVoid;
}

備註:關於上述記憶體的釋放,直接引用原作者寫的一段文字說明,沒有實際驗證。其是否能夠正確釋放,與編譯器所處的模式也有一定的關係,具體看引用的回覆部分。

這時候記憶體會正確釋放嗎?我們知道指標是分型別,特定型別的指標定址的位元組數是不一樣的,比如字元指標加一的時候是移動一個位元組,int型別指 針加一的時候,移動兩個位元組。因為第一次申請的是int型的兩個位元組的記憶體,如果系統釋放記憶體的時候是按型別釋放的話,delete一個int型的指標能 正確釋放int型指標所指向的記憶體,因為系統知道,int型就佔兩個位元組,從指標的起始位置,釋放兩個位元組的指標不就行了?但是,這樣看來,第二段程式碼就 有問題:在第二段程式碼中,從堆中申請了一個int型的兩個位元組的記憶體賦給一個void型的指標,然後釋放這個void型別的指標所指向的記憶體,由於 void型別指標是無型別的,系統無法知道void型別的指標指向多大的記憶體空間,這樣,能正確釋放指標所指向的記憶體空間嗎?會不會造成記憶體洩露?為什麼 呢?
.
這個時候我們就需要對變數的記憶體分配有一定的瞭解。
.
比如,int *pNum = new int(2);所申請的記憶體就是2個位元組嗎?答案是否定的,每當我們從堆裡面申請一塊的記憶體時,系統所分配的記憶體比物件所佔用的記憶體要稍微大一點,就像 OSI協議中每層都要加一些特定的資訊用於辨認一樣,系統也會為記憶體加上一個標識頭,用於標識這塊記憶體的邊界,這種技術成為cookie,不要以為 cookie是http協議中特有的標識資訊,在c++中也有這種技術。其實cookie是一種很古老的技術,最初用來在進城間傳遞一些資訊,後來因為用 於http協議中用來標示使用者資訊而為世人所熟知。扯遠了,回到前面所說。所以當把一個int型別的指標賦給一個void型別的指標,然後用void型別 的指標釋放這塊記憶體,會正確釋放,因為,型別只是給你看的,系統自有自己的一套標識方法。
.
回覆:你說的這種情況只存在於debug,如果你用的是release,那就沒有邊界的

關於釋放void*,最常見問題出現在類中,用void*指向類的物件,delete的時候不會呼叫解構函式導致記憶體的洩露,具體情況如下:

class A;
void fun(){
	void* ptr=new A();
	delete ptr;//A的解構函式不會被呼叫
}

原文地址:
C++造成記憶體洩漏的原因彙總
void*指標及delete釋放void*記憶體(轉)
釋放void*指標 [推薦閱讀]

2.2 memset()導致的記憶體越界

程式從堆中分配的記憶體使用完畢後必須顯式釋放,否則這塊記憶體就不能被再次使用,即這塊記憶體洩漏了。記憶體洩漏導致軟體在執行過程中佔用越來越多的記憶體,程式的效率會越來越低。

使用malloc、new等運算子動態分配的記憶體,必須使用free、delete顯式釋放;memset函式可能導致指向某塊記憶體的指標的值發生了變化,從而導致釋放記憶體失敗,造成記憶體越界。

//以下程式碼會造成記憶體越界,但是編譯不會報錯,也有可能不會崩潰[崩潰偶爾會發生]
	int * x0 = new int[10];
    int * x1 = new int[10];
    int * x2 = new int[10];
    memset(x0, 0, 3 * 10 * sizeof(int));
    int * x3 = new int[10];

相關原文地址:
.exe 已觸發了一個斷點
memset和memcpy使用不當而引起的memory溢位
memset函式導致記憶體洩露的問題

2.3 執行庫的連結可能會導致記憶體越界

msvcrt.lib,可能會導致記憶體越界,因為動態連結的庫和靜態連結的庫可能會過載了new運算子,導致了不同的堆存在,可能會在一個堆上申請的空間在另一個堆上釋放!
這裡有兩點需要注意:

  1. 儘量不要混合靜態連結和動態連結,因為他們會申請不同的堆。
  2. 注意任何編譯的警告,[warning LNK4098: 預設庫“msvcrtd.lib”與其他庫的使用衝突;請使用 /NODEFAULTLIB:library]

這裡已經明確提示開發者可能會出現衝突問題了,所以應該解決掉他!
原文連結:C++中delete崩潰的問題

2.4 其他情況

以下幾種情況屬於個人寫程式碼中出現過的問題,坑都踩過,需要記錄:

  • 陣列下標訪問越界[自己寫了某種神奇計算索引的方程導致索引越界 ]
  • strcpy等系列函式的使用,字元陣列空間不夠[計算size的問題 ]
  • delete之後不置位NULL,再次訪問[需要養成良好習慣 ]
  • 使用了已經被銷燬的函式返回的地址或者引用[確定自己使用的東西存在 ]
  • 指標指向臨時物件,該物件在某語句塊執行完畢之後被析構[指標指向空間不復存在 ]
  • 型別強轉導致的訪問越界[派生類指標指向積累物件 ]
    參考連結:基類指標指向派生類物件
  • 多執行緒訪問共享物件時,可能該物件已經被某執行緒析構掉

原文地址:C++什麼時候出現訪問越界?

3. 個人程式碼出現問題的原因

3.1 描述

VS執行QT程式的過程中是不是會在delete[]的位置出現中斷,沒有中斷資訊的提示,僅僅顯示一個叉號,有的時候會直接斷開在QT main函式中的最後一句,甚至是會出現的ntdll.dll中,但是此時提示沒有相關的pdb訊息無法除錯。

//以下幾處出現中斷
void fun(){
	...
	delete[] tempPtr1;//maybe crash
	...
	delete[] tempPtr2;//maybe crash
	...
	...
	delete[] tempPtr3;//maybe crash
}

int main(){
	...
	return w.exec();//maybe crash	
}

3.2 忽略項

Relese模式:我的VS目前是reles模式下,沒有辦法精確的定位出錯的位置。

relese模式下,這種與記憶體有關的bug並不是100%的命中,因此bug並不是每次都會出現,更為奇葩的是,發生中斷的位置並不一定是你寫的可能產生陣列越界的位置,此時,你可能一直在檢查根本沒有發生記憶體越界的那部分程式碼!。一定要調到Debug模式下,才能夠更快的找到錯誤。


言:記憶體一定要處理妥當,要手動的,主動的,準確的進行釋放,否則不僅僅會出現記憶體佔用大的情況,還會出現訪問越界導致程式崩潰,更可怕的是出現記憶體洩露,影響程式及其使用者的安全。

相關文章