在最近的代碼調試中,遇到一個比較棘手的崩潰問題,現象為程序在函數的返回值處崩潰,報警提示如圖:
經過排查,最終發現在對結構體內數組初始化賦值時出現了數組越界現象,導致函數在返回時出現異常,導致程序崩潰,借此機會,對內存棧內空間的函數占用空間總結學習:
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。而全局初始化段的變量(包括靜態,不做區分的)從低地址到高地址按申明的順序壓棧(不是指上面所指的棧區,請區別開來,這是就地址變化過程而言的,它與局部函數變量起始地址完全不同)。函數在程序代碼段中地址是按申明順序遞增的。函數局部變量在棧去是按照申明順序從高到低的地址進棧的。
常量的開始地址來看跟全局變量應該屬於一個區。堆區的地址開頭也是另外一個段。
補充一點:數組變量內部元素是按照元素下標從低地址到高地址壓棧的。
一般局部變量一般是從高低地址到低地址壓棧的。
通過以上的總結,也就解釋了本文開頭處的崩潰問題,當函數內的局部變量賦值時出現越界,導致了存放了函數返回地址的空間被影響,使得被調用的函數返回時出現異常,導致程序崩潰。
經驗有限,本文中可能存在着自己的一些理解錯誤之處,后續會繼續修改完善,也希望大家指正。