參考:https://www.cnblogs.com/edisonchou/p/4669098.html
一個由C/C++編譯的程序占用的內存分為以下幾個部分:
1、棧區(stack):又編譯器自動分配釋放,存放函數的參數值,局部變量的值等,其操作方式類似於數據結構的棧。
2、堆區(heap):一般是由程序員分配釋放,若程序員不釋放的話,程序結束時可能由OS回收,值得注意的是他與數據結構的堆是兩回事,分配方式倒是類似於數據結構的鏈表。
3、全局區(static):也叫靜態數據內存空間,存儲全局變量和靜態變量,全局變量和靜態變量的存儲是放一塊的,初始化的全局變量和靜態變量放一塊區域,沒有初始化的在相鄰的另一塊區域,程序結束后由系統釋放。
4、文字常量區:常量字符串就是放在這里,程序結束后由系統釋放。
5、程序代碼區:存放函數體的二進制代碼。
數據結構的棧和堆
參考:https://www.cnblogs.com/lln7777/archive/2012/03/14/2396164.html
首先在數據結構上要知道堆棧,盡管我們這么稱呼它,但實際上堆棧是兩種數據結構:堆和棧。
堆和棧都是一種數據項按序排列的數據結構。
棧就像裝數據的桶或箱子
我們先從大家比較熟悉的棧說起吧,它是一種具有后進先出性質的數據結構,也就是說后存放的先取,先存放的后取。
這就如同我們要取出放在箱子里面底下的東西(放入的比較早的物體),我們首先要移開壓在它上面的物體(放入的比較晚的物體)。
堆像一棵倒過來的樹
- 而堆就不同了,堆是一種經過排序的樹形數據結構,每個結點都有一個值。
- 通常我們所說的堆的數據結構,是指二叉堆。
- 堆的特點是根結點的值最小(或最大),且根結點的兩個子樹也是一個堆。
由於堆的這個特性,常用來實現優先隊列,堆的存取是隨意,這就如同我們在圖書館的書架上取書,雖然書的擺放是有順序的,但是我們想取任意一本時不必像棧一樣,先取出前面所有的書,書架這種機制不同於箱子,我們可以直接取出我們想要的書。
內存分配中的棧和堆
內存分配中的堆區和棧區並不是數據結構的堆和棧,之所以要說數據結構的堆和棧是為了和后面我要說的堆區和棧區區別開來,請大家一定要注意。
下面就說說C語言程序內存分配中的堆和棧,這里有必要把內存分配也提一下,大家不要嫌我啰嗦,一般情況下程序存放在Rom(只讀內存,比如硬盤)或Flash中,運行時需要拷到RAM(隨機存儲器RAM)中執行,RAM會分別存儲不同的信息,如下圖所示:
內存中的棧區處於相對較高的地址以地址的增長方向為上的話,棧地址是向下增長的。
棧中分配局部變量空間,堆區是向上增長的用於分配程序員申請的內存空間。另外還有靜態區是分配靜態變量,全局變量空間的;只讀區是分配常量和程序代碼空間的;以及其他一些分區。
1 int a = 0; //全局初始化區
2 char *p1; //全局未初始化區
3 main() 4 { 5 int b; //棧
6 char s[] = "abc"; //棧
7 char *p2; //棧
8 char *p3 = "123456"; //123456\0在常量區,p3在棧上。
9 static int c =0; //全局(靜態)初始化區
10 p1 = (char *)malloc(10); //堆
11 p2 = (char *)malloc(20); //堆
12 }
0.申請方式和回收方式不同
不知道你是否有點明白了。
堆和棧的第一個區別就是申請方式不同:棧(英文名稱是stack)是系統自動分配空間的,例如我們定義一個 char a;系統會自動在棧上為其開辟空間。而堆(英文名稱是heap)則是程序員根據需要自己申請的空間,例如malloc(10);開辟十個字節的空間。
由於棧上的空間是自動分配自動回收的,所以棧上的數據的生存周期只是在函數的運行過程中,運行后就釋放掉,不可以再訪問。而堆上的數據只要程序員不釋放空間,就一直可以訪問到,不過缺點是一旦忘記釋放會造成內存泄露。還有其他的一些區別我認為網上的朋友總結的不錯這里轉述一下:
1.申請后系統的響應
棧:只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的 delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。
也就是說堆會在申請后還要做一些后續的工作這就會引出申請效率的問題。
2.申請效率的比較
根據第0點和第1點可知。
棧:由系統自動分配,速度較快。但程序員是無法控制的。
堆:是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。
3.申請大小的限制
棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
4.堆和棧中的存儲內容
由於棧的大小有限,所以用子函數還是有物理意義的,而不僅僅是邏輯意義。
棧: 在函數調用時,第一個進棧的是主函數中函數調用后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
5.存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa"; char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;放在棧中。
而bbbbbbbbbbb是在編譯時就確定的;放在堆中。
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
入棧順序:
A:函數參數的入棧順序:自右向左
原因:
函數參數的入棧順序和具體編譯器的實現有關。有些參數是從左向右入棧,如:Pascal語言從左到右入棧(不支持變參),被調用者清棧;有些語言還可以通過修飾符進行指定,如:Visual C++;但是C語言(cdecl)采用自右向左的方式入棧,調用者清棧。
這是因為自右向左入棧順序的好處就是可以動態的變化參數個數。通過堆棧分析可知,自左向右入棧方式中,最前面的參數會被壓入棧底。除非知道參數個數,否則無法通過棧指針的相對位移求得最左邊的參數。這樣就無法實現可變參數。因此,C語言采用自右向左入棧順序,主要是因為實現可變長參數形式(如:printf函數)。可變長參數主要通過第一個定參數來確定參數列表,所以自右向左入棧后,函數調用時棧頂指針指向的就是參數列表的第一個確定參數,這樣就可以了。
例子1:
1 #include <stdio.h>
2
3 void print(int x, int y, int z) 4 { 5 printf("x = %d addr %p\n", x, &x); 6 printf("y = %d addr %p\n", y, &y); 7 printf("z = %d addr %p\n", z, &z); 8 } 9
10 int main() 11 { 12 print(1,2,3);//自右向入壓棧
13 return 0; 14 }
運行結果:
1 x = 1 addr 0xbfb5c760 //棧頂,后壓棧
2 y = 2 addr 0xbfb5c764
3 z = 3 addr 0xbfb5c768 //棧底,先入棧
B:局部變量的入棧順序:
在沒有棧溢出保護機制下編譯時,所有局部變量按系統為局部變量申請內存中棧空間的順序,即:先申請哪個變量,哪個先入棧,正向的。也就是說,編譯器給變量空間的申請是直接按照變量申請順序執行的。(見例子2)
在有棧溢出保護機制下編譯時,入棧順序有所改變,先按照類型划分,再按照定義變量的先后順序划分,即:char型先申請,int類型后申請(與編譯器溢出保護時的規定相關);然后棧空間的申請順序與代碼中變量定義順序相反(后定義的先入棧)。(見例子2)
例子2:stack.c
1 #include <stdio.h>
2
3 int main() 4 { 5 int a[5] = {1,2,3,4,5}; 6 int b[5] = {6,7,8,9,10}; 7 char buf1[6] = "abcde"; 8 char buf2[6] = "fghij"; 9 int m = -1; 10 int n = -2; 11 printf("a[0] = %3d, addr: %p\n", a[0], &a[0]); 12 printf("a[4] = %3d, addr: %p\n", a[4], &a[4]); 13 printf("b[0] = %3d, addr: %p\n", b[0], &b[0]); 14 printf("b[4] = %3d, addr: %p\n", b[4], &b[4]); 15 printf("buf1[0] = %3d, addr: %p\n", buf1[0], &buf1[0]); 16 printf("buf1[5] = %3d, addr: %p\n", buf1[5], &buf1[5]); 17 printf("buf2[0] = %3d, addr: %p\n", buf2[0], &buf2[0]); 18 printf("buf2[5] = %3d, addr: %p\n", buf2[5], &buf2[5]); 19 printf("m = %3d, addr: %p\n", m, &m); 20 printf("n = %3d, addr: %p\n", n, &n); 21 }
沒有棧溢出保護機制下的編譯:
1 $ gcc stack.c -g -o stack -fno-stack-protector 2 $ ./stack 3 a[0] = 1, addr: 0xbfa5185c //數組內部,地址由低到高不變
4 a[4] = 5, addr: 0xbfa5186c //棧底,高地址
5 b[0] = 6, addr: 0xbfa51848
6 b[4] = 10, addr: 0xbfa51858
7 buf1[0] = 97, addr: 0xbfa51842
8 buf1[5] = 0, addr: 0xbfa51847
9 buf2[0] = 102, addr: 0xbfa5183c
10 buf2[5] = 0, addr: 0xbfa51841
11 m = -1, addr: 0xbfa51838
12 n = -2, addr: 0xbfa51834 //棧頂,低地址
可以看出入棧順序:a -> b -> buf1 -> buf2 -> m -> n(先定義,先壓棧)
棧溢出保護機制下的編譯:
1 $ gcc stack.c -g -o stack 2 $ ./stack 3 a[0] = 1, addr: 0xbfc69130 //棧頂
4 a[4] = 5, addr: 0xbfc69140
5 b[0] = 6, addr: 0xbfc69144
6 b[4] = 10, addr: 0xbfc69154
7 buf1[0] = 97, addr: 0xbfc69160 //char類型,優先入棧
8 buf1[5] = 0, addr: 0xbfc69165
9 buf2[0] = 102, addr: 0xbfc69166
10 buf2[5] = 0, addr: 0xbfc6916b //棧底
11 m = -1, addr: 0xbfc69158
12 n = -2, addr: 0xbfc6915c //int類型,后壓棧
可以看出入棧順序:buf2 -> buf1 -> n -> m -> b -> a(char類型先入棧,int類型后入棧;先定義,后壓棧)
3).指針越界輸出:
例子3:stack1.c
1 #include <stdio.h>
2
3 int main() 4 { 5 char buf1[6] = "abcef"; 6 char buf2[6] = "fghij"; 7 int a[5] = {1,2,3,4,5}; 8 int b[5] = {6,7,8,9,10}; 9 int m = -1; 10 int n = -2; 11 char *p = &buf2[0]; 12 printf("a[0] = %3d, addr: %p\n", a[0], &a[0]); 13 printf("a[4] = %3d, addr: %p\n", a[4], &a[4]); 14 printf("b[0] = %3d, addr: %p\n", b[0], &b[0]); 15 printf("b[4] = %3d, addr: %p\n", b[4], &b[4]); 16 printf("buf1[0] = %3d, addr: %p\n", buf1[0], &buf1[0]); 17 printf("buf1[5] = %3d, addr: %p\n", buf1[5], &buf1[5]); 18 printf("buf2[0] = %3d, addr: %p\n", buf2[0], &buf2[0]); 19 printf("buf2[5] = %3d, addr: %p\n", buf2[5], &buf2[5]); 20 printf("m = %3d, addr: %p\n", m, &m); 21 printf("n = %3d, addr: %p\n", n, &n); 22 printf("p[0] = %3d, addr: %p\n", p[0], &p[0]); 23 printf("p[6] = %3d, addr: %p\n", p[6], &p[6]); 24 printf("p[-6] = %3d, addr: %p\n", p[-6], &p[-6]); 25 printf("p[-42] = %3d, addr: %p\n", p[-42], &p[-42]); 26 printf("p[-43] = %3d, addr: %p\n", p[-43], &p[-43]); 27 printf("p[-53] = %3d, addr: %p\n", p[-53], &p[-53]); 28 printf("p[-54] = %3d, addr: %p\n", p[-54], &p[-54]); 29 printf("p[-55] = %3d, addr: %p\n", p[-55], &p[-55]); 30 printf("p[-56] = %3d, addr: %p\n", p[-56], &p[-56]); 31 printf("p[-57] = %3d, addr: %p\n", p[-57], &p[-57]); 32 printf("p[-58] = %3d, addr: %p\n", p[-58], &p[-58]); 33 printf("p[-59] = %3d, addr: %p\n", p[-59], &p[-59]); 34 }
棧溢出保護機制下的編譯:
1 $ gcc stack1.c -g -o stack1 2 $ ./stack1 3 a[0] = 1, addr: 0xbff5ab6c //棧頂,0xbff5ab6c,低地址
4 a[4] = 5, addr: 0xbff5ab7c
5 b[0] = 6, addr: 0xbff5ab80
6 b[4] = 10, addr: 0xbff5ab90
7 buf1[0] = 97, addr: 0xbff5aba0 //&p[-6]
8 buf1[5] = 0, addr: 0xbff5aba5
9 buf2[0] = 102, addr: 0xbff5aba6 //&p[0]
10 buf2[5] = 0, addr: 0xbff5abab //棧底,0xbff5abab,高地址--->&p[6]:越界,值隨機
11 m = -1, addr: 0xbff5ab94
12 n = -2, addr: 0xbff5ab98
13 p[0] = 102, addr: 0xbff5aba6 //&buf2[0]
14 p[6] = 0, addr: 0xbff5abac //&buf2[6],越界,無初始值,值隨機
15 p[-6] = 97, addr: 0xbff5aba0 //&buf1[0],越界,已有初始值,buf1[0],p[-6]為97
16 p[-42] = 5, addr: 0xbff5ab7c //&a[4]
17 p[-43] = 0, addr: 0xbff5ab7b //&a[4] - 1字節,大小0x00 = 0
18 p[-53] = 0, addr: 0xbff5ab71 //&a[1] + 1字節,大小0x00 = 0
19 p[-54] = 2, addr: 0xbff5ab70 //&a[1]
20 p[-55] = 0, addr: 0xbff5ab6f //p[-55]到p[-58]能看出Linux是小端存儲。
21 p[-56] = 0, addr: 0xbff5ab6e //小端存儲:低地址存低位,高地址存高位
22 p[-57] = 0, addr: 0xbff5ab6d //a[0]=1,即:0x01 0x00 0x00 0x00(低位到高位)
23 p[-58] = 1, addr: 0xbff5ab6c //&a[0]
24 p[-59] = -65, addr: 0xbff5ab6b //&a[0] - 1字節,越界,無初始值,值隨機
入棧順序:
(棧底:高地址)buf2 -> buf1 -> n -> m -> b -> a[4] -> a[0](棧頂:低地址)
&p[6]—&p[0]—&p[-6]——————&p[-42]—&p[-58]—&p[-59]
提醒:指針p越界會出現問題,如果在p[-6] = ‘k’;那么會導致因越界覆蓋內存里面buf1[0]的值。
每個函數棧空間內存如何分配
參考:https://blog.csdn.net/u013318019/article/details/104040516
關於函數在調用過程中的壓棧和出棧問題在學習的時候就感覺很經典,對程序的把握可以提升一個台階。
一.首先讓我們寫出一個簡單的函數。(我是在vc6.0中實現,並不表示vs編譯器底下不可以實現)。
1 #include<stdio.h>
2
3 int add(num1,num2) 4 { 5 int ret = 0; 6 ret = num1+num2; 7 return ret; 8 } 9
10 int main() 11 { 12 int num1 = 1; 13 int num2 = 2; 14 int ret = add(num1,num2); 15 printf("%d ",ret); 16 return 0; 17 }
一、需要聲明是add函數中可以直接寫成"return num1+num2",我在寫博客的時候是故意寫成這樣,以便於后面的分析。
二、接下來,我們首先明確幾個知識點。
1).棧
首先必須明確一點也是非常重要的一點,棧是向下生長的,所謂向下生長是指從內存高地址->低地址的路徑延伸,那么就很明顯了,棧有棧底和棧頂,那么棧頂的地址要比棧底低。對x86體系的CPU而言,其中
—> 寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。
—> 寄存器esp(stack pointer)可稱為“ 棧指針”。
要知道的是:
—>ebp 在未受改變之前始終指向棧幀的開始,也就是棧底,所以ebp的用途是在堆棧中尋址用的。
—>esp是會隨着數據的入棧和出棧移動的,也就是說,esp始終指向棧頂。
2).
假設函數A調用函數B,我們稱A函數為"調用者",B函數為“被調用者”則函數調用過程可以這么描述:
(1)先將調用者(A)的堆棧的基址(ebp)入棧,以保存之前任務的信息。
(2)然后將調用者(A)的棧頂指針(esp)的值賦給ebp,作為新的基址(即被調用者B的棧底)。
(3)然后在這個基址(被調用者B的棧底)上開辟(一般用sub指令)相應的空間用作被調用者B的棧空間。
(4)函數B返回后,從當前棧幀的ebp即恢復為調用者A的棧頂(esp),使棧頂恢復函數B被調用前的位置;然后調用者A再從恢復后的棧頂可彈出之前的ebp值(可以這么做是因為這個值在函數調用前一步被壓入堆棧)。這樣,ebp和esp就都恢復了調用函數B前的位置,也就是棧恢復函數B調用前的狀態。
如下圖所示:
自己的理解:(棧空間中的局部變量如何訪問)
即在函數調用時先保存調用函數的現場情況到棧空間中之后將被調用函數的棧空間區間重新設置(重新設置棧頂和棧底指針),這樣被調用函數的局部變量保存在新開辟出來的棧空間中,其中的局部變量可以隨機訪問,而調用函數的棧空間不屬於調用函數的棧空間,所以調用函數不能訪問其他函數的棧空間(局部變量),在被調用函數執行完畢后,先將調用函數的現場恢復,然后重設棧頂指針和棧底指針恢復調用者的空間,繼續往下執行。
三.在明確了這些知識之后,讓我們返回上面那個簡單的函數。
1).首先來看看我畫出的圖:
上面的圖片能夠粗略的表現函數調用的過程。
2)所產生的匯編代碼:
上面兩幅圖片是mian函數的棧幀。
上面的圖片是add函數的棧幀。
3).在liunx平台下的匯編代碼
函數中使用的變量在棧上是如何申請空間的
參考:https://www.cnblogs.com/TaoR320/p/12680124.html
在定義變量之前,我們首先要知道,函數中使用的變量在棧上申請空間,至於原因我們下次在討論。那么對於棧這種數據結構來說,它是由高地址向低地址生長的一種結構。像我們平時在 main函數或是普通的函數中定義的變量都是由棧區來進行管理的。下面進行幾個實例以便於我們更加了解棧區的使用。
編寫如下C程序:
1 int main() 2 { 3
4 char str[] = { "hello world" }; 5 char str2[10]; 6
7 printf("%s \n",str); 8 printf("%s\n",str2); 9
10 return 0; 11 }
在 VS 2019中運行
我們在C源碼中,給 str
賦值為“Hello World”,而 str2
沒有進行賦值。
那為什么打印str2的時候會出現燙燙燙燙燙燙燙燙燙燙Hellow World這種情況呢?
這里要說明一點,在函數內部會根據函數所用到的空間大小生成函數的棧幀,而后對其內存空間進行 0xcccc cccc
的初始化賦值。而'cc'
在中文編碼下就是“燙”字符。有時候我們會說申請的局部變量(函數的作用域下)沒有進行賦值其內容會是隨機值。這么說其實也沒錯,原因很簡單,在內存中的某個內存塊上,無時無刻不伴隨着大量程序的使用,而在程序使用過后就會在該內存塊處留下一些數據,這些數據我們無法使用在我們看來就是隨機值。而在 VS 編譯器中為了防止隨機值對程序運行結果造成干擾,就通過用初始化為 0xcccc cccc
的方式進行統一的初始化。而字符串的輸出時靠字符串末尾的 \0
結束符來確定的,str2 ,中並沒有該字符,因此在輸出時一直順着棧向高地址尋找,直到找到 str 中的 \0
結束符。
還有一個有趣的例子:
代碼:
1 #include <stdio.h>
2
3 int main(void) 4 { 5 int a[10]; 6 int i; 7
8 for(i = 0; i<=10; ++i) 9 { 10 a[i] = 0; 11 } 12 /* .... */
13 }
這是一段最簡單不過的數組初始化代碼了,可是因為邊界判斷錯誤,導致數組訪問越界,運行時出現問題。
Linux環境下,運行程序,結果如下:

出現的結果,直接報出棧粉碎錯誤,程序奔潰。
win10環境下,運行程序,結果如下:

出現的結果,程序一直在運行,並沒有奔潰。
對於Linux有保護措施,程序直接奔潰,不容易發現問題。可以從win10的結果中分析,為什么程序會進入死循環??要想完整回答這個問題,需要認識C語言局部變量的棧空間分配。
局部變量的棧空間分配
我們知道,函數局部變量是調用該函數的時候才進行內存分配的,如果有多個局部變量,那么變量的分配應該有一個順序,C語言對局部變量的分配機制是采用棧的方式,貼出棧的概念圖:

參考以下文章:
https://blog.csdn.net/qq_19406483/article/details/77511447
在上述代碼中,C語言函數中的同類型局部變量,分配順序就是:順序局部變量、順序參數
假設有如下函數:
1 void fun(int a,int b) 2 { 3 int c; 4 int d; 5 /* ... */
6 }
那么調用這個函數的時候,局部變量分配順序是c、d、b、a,也就是先從上到下順序分配局部變量,再從右往左(視編譯器而定)順序分配參數。
回答程序進入死循環的問題********(重要)
現在可以完整回答程序為什么會進入死循環了,按照局部變量的棧空間分配,程序中變量儲存順序如下:

對於
1 for(i = 0; i<=10; ++i) 2 { 3 a[i] = 0; 4 }
最后的a[10]經過地址計算a+10之后就會指向變量 i 所在的內存,然后賦值為0,於是循環變量 i 又從10變到0,再次開啟下一次循環,周而復始,於是出現了死循環。
可以驗證這一說法,只需要輸出 i 的值查看即可:
1 for(i = 0; i<=10; ++i) 2 { 3 a[i] = 0; 4 printf("i=%d\n",i); 5 }
gcc運行結果:
最后,for循環中應該遵循左閉右開的區間規則,因為非常容易閱讀出循環次數,而上述的左閉右閉,閱讀的時候還要心算一會兒(10-0+1=11次)。