函式記憶體分配
在最近的程式碼除錯中,遇到一個比較棘手的崩潰問題,現象為程式在函式的返回值處崩潰,報警提示如圖:
經過排查,最終發現在對結構體內陣列初始化賦值時出現了陣列越界現象,導致函式在返回時出現異常,導致程式崩潰,藉此機會,對記憶體棧內空間的函式佔用空間總結學習:
1. 程序的記憶體佈局
對於一個程序來說,它在記憶體中的佈局如下所示:
程式碼區與常量區等不再贅述,堆區是由程式碼動態的申請與釋放,只在部分情況下如果程式碼中沒有對申請的記憶體進行釋放,在程式結束的時候OS會進行釋放。棧區的空間是由編譯器自動的分配和釋放的。此處需要注意的是:
(1)堆向高記憶體地址生長,而棧向低記憶體地址生長;
(2)堆和棧相向生長,他們之間有個臨界點,稱為stkbrk。
2. 程式如何在程序中對映
對於一個程式,它的執行順序為:
main( )-->fun1( )-->fun2( )...fun3( ),即主函式逐個呼叫內部子函式,其內部的對映如下所示:
如上圖:
1)隨著函式呼叫層次的增加,函式幀棧逐塊向低地址方向延伸;
2)當各函式呼叫返回後(程序中函式的呼叫層減少),幀棧會被逐塊被遺棄而向高記憶體地址方向縮回;
3)每個子函式的幀棧大小與函式的性質相關,由函式內部非區域性變數數量決定;
4)未初始化資料區(BSS):用於存放程式的靜態變數,這部分記憶體都是被初始化為零的,而初始化資料區用於存放可執行檔案的程序所共享;
5)當出現未初始化資料區(BSS)或者Stack(棧區)的增長耗盡了系統分配給程序的自由記憶體的情況下,程序 將會被阻塞,重新被作業系統用更大的記憶體模組來排程執行;
6)每一個函式的幀棧包括:函式的引數(關於被呼叫函式的引數時放在呼叫函式的幀棧還是被呼叫函式的幀棧,依賴於不同的系統實現),函式的幀棧中的區域性變數以及恢復該函式的主調函式的棧幀(前一個棧幀)所需的資料,以及主調函式的下一條指令的地址。
3. 函式的棧幀
一個函式建立的棧幀包含如下的資訊:
1)主調函式的返回地址。
2)為函式的區域性變數分配的棧空間。
3)位被調函式的引數分配的空間。
4)函式的返回地址。
關於函式內資料的佔用空間細節,可以通過以下程式碼瞭解:
//全域性初始化區 int i1 = 0; int i2 = 0; int i3 = 0; //全域性初始化區 static int i4 = 0; static int i5 = 0; static int i6 = 0; //全域性未初始化區 int i7; int i8; int i9; int main() { cout << "列印全域性初始化區變數i1-i3的地址:" << endl; cout << &i1 << " " << &i2 << " " << &i3 << endl; cout << "列印全域性初始化區靜態變數i4-i6的地址:" << endl; cout << &i4 << " " << &i5 << " " << &i6 << endl; cout << "列印全域性未初始化區變數i7-i9的地址:" << endl; cout << &i7 << " " << &i8 << " " << &i9 << endl; int data1 = 'a'; int data2 = 'b'; int data3; int data4; cout << "列印函式內區域性變數data1-data4地址,其中data3,data4未初始化" << endl; cout << &data1 << " " << &data2 << " " << &data3 << " " << &data4 << " " << endl; char *pStr1 = "12345";//12345在常量區,pStr在棧上 char *pStr2 = "1122"; void *p = pStr1; void *q = pStr2; cout << "列印常量地址" << endl; cout << p << endl; cout << q << endl; static int aData = 0;//全域性(靜態)初始化區 cout << "列印區域性函式中定義靜態變數地址,對比其他全域性區地址" << endl; cout << &aData << endl; int *p1 = new int;//堆區 int *p2 = new int;//堆區 cout << "列印堆區地址" << endl; cout << p1 << endl; cout << p2 << endl; getchar(); return 0; }
執行結果如下:
觀察上面的地址列印資訊可以發現,全域性未初始化區的變數是按從高到低地址按申明定義的順序壓棧,變數i7緊鄰全域性初始化段的第一個變數i1。而全域性初始化段的變數(包括靜態,不做區分的)從低地址到高地址按申明的順序壓棧(不是指上面所指的棧區,請區別開來,這是就地址變化過程而言的,它與區域性函式變數起始地址完全不同)。函式在程式程式碼段中地址是按申明順序遞增的。函式區域性變數在棧去是按照申明順序從高到低的地址進棧的。
常量的開始地址來看跟全域性變數應該屬於一個區。堆區的地址開頭也是另外一個段。
補充一點:陣列變數內部元素是按照元素下標從低地址到高地址壓棧的。
一般區域性變數一般是從高低地址到低地址壓棧的。
通過以上的總結,也就解釋了本文開頭處的崩潰問題,當函式內的區域性變數賦值時出現越界,導致了存放了函式返回地址的空間被影響,使得被呼叫的函式返回時出現異常,導致程式崩潰。
經驗有限,本文中可能存在著自己的一些理解錯誤之處,後續會繼續修改完善,也希望大家指正。