內(nèi)存、棧、堆的一點(diǎn)小總結(jié)
-
程序的內(nèi)存布局
【前言】在32位系統(tǒng)中,大家可能認(rèn)為我們可以用一個(gè)32位的指針訪問任意內(nèi)存地址。如下:
int *p = (int *)0x12345678;
++*p;
??但事實(shí)上用戶可以直接讀取的內(nèi)存大小是達(dá)不到4GB的。大多數(shù)操作系統(tǒng)都會(huì)將其中的一部分分配給內(nèi)核使用,應(yīng)用程序是無法直接訪問這一段內(nèi)存的,這部分被稱為內(nèi)核空間。Linux默認(rèn)將高地址的1GB空間分配給內(nèi)核;win默認(rèn)下將高地址的2GB分配給內(nèi)核,但是也可以人為配置成1GB。(前面已介紹)
??但是處理上述的內(nèi)核空間之外的用戶空間中也有一些特殊的空間有特殊的用處,而應(yīng)用程序能用的內(nèi)存空間是如下的“默認(rèn)區(qū)域”:-
棧
??用于維護(hù)函數(shù)調(diào)用的上下文,離開了棧,函數(shù)的調(diào)用就辦法實(shí)現(xiàn)了。棧通常在用戶更高的地址空間處分配,通常有數(shù)兆字節(jié)的大小。 -
堆
??堆用來容納應(yīng)用程序動(dòng)態(tài)分配的內(nèi)存區(qū)域,當(dāng)程序使用malloc或new的時(shí)候就是得到來自堆中的內(nèi)存。堆統(tǒng)稱在棧的下方(低地址方向,但是不是緊鄰的)。堆一般比棧要更大一點(diǎn),一般會(huì)達(dá)到幾十甚至是數(shù)百兆字節(jié)。 -
可執(zhí)行文件映像
??顧名思義,這里存儲(chǔ)著可執(zhí)行文件在內(nèi)存里的映像。裝載器在裝載的時(shí)候會(huì)將可執(zhí)行文件讀取或者映射到這里。 -
保留區(qū)
??是對(duì)內(nèi)存中受到保護(hù)而禁止訪問的內(nèi)存區(qū)域的總稱。這個(gè)區(qū)域大家應(yīng)該都比較熟悉,比如,大多數(shù)操作系統(tǒng)中,極小的地址區(qū)域都是不允許訪問的,如NULL。
??相關(guān)的內(nèi)存布局如下:
??上圖中還有一個(gè)區(qū)域,“動(dòng)態(tài)鏈接庫映射區(qū)”,這個(gè)區(qū)域用于映射裝載的動(dòng)態(tài)鏈接庫。比如,如果可執(zhí)行文件依賴于其他的共享庫,那么系統(tǒng)就會(huì)為他從0x40000000開始的地址分配相應(yīng)的空間,并將該共享庫載入到該空間(動(dòng)態(tài)鏈接共享對(duì)象的內(nèi)存地址分配)。
??【注意】棧向低地址增長;堆向高地址增長。當(dāng)棧或者堆的現(xiàn)有大小不夠用的時(shí)候,它將按照?qǐng)D中的增長方向擴(kuò)大自身的尺寸,直到預(yù)留的空間(unused)被用完。
??【補(bǔ)充】在Linux或者是win內(nèi)存中,有些地址是始終不能讀寫的,例如0地址,當(dāng)指針指向這些地址的時(shí)候,就會(huì)出現(xiàn)“段錯(cuò)誤(segment fault)”。造成這種情況的兩種最普遍的原因:
1.程序員將指針初始化為NULL,但是沒有賦予合理的初值就開始使用。
2.程序員沒有初始化棧上的指針,指針的值一般是隨機(jī)數(shù),之后就開始使用該指針。
-
棧
-
棧
- 在經(jīng)典的操作系統(tǒng)中,棧總是向下(低地址)增長的。
- 堆棧幀或活動(dòng)記錄
??棧保存一個(gè)函數(shù)調(diào)用所需要的維護(hù)信息,常備稱為堆棧幀或者是活動(dòng)記錄,堆棧幀一般包括:
(1)函數(shù)的返回地址和參數(shù);
(2)臨時(shí)變量:包括函數(shù)的非靜態(tài)局部變量以及編譯器生成的其他局部變量;
(3)保存的上下文:包括在函數(shù)調(diào)用前后保持不變的寄存器。 - 棧中有兩個(gè)重要的寄存器:esp和ebp
(1)esp
??該寄存器標(biāo)明棧頂,在棧上壓入數(shù)據(jù)會(huì)導(dǎo)致esp減小,反之esp增大。再者,減小esp的值等于在棧上開辟空間,而增大esp的值等效于在棧上回收空間。
??esp不僅僅指向棧的頂部,同時(shí)也就意味著指向當(dāng)前整個(gè)函數(shù)活動(dòng)記錄的頂部(見下圖)。
(2)ebp
??ebp寄存器指向函數(shù)活動(dòng)記錄的一個(gè)固定位置。ebp寄存器又被稱為幀指針。如下:
??ebp具體的固定位置如上圖所示。如圖,(注意棧向低地址增長)ebp之前是該函數(shù)的返回地址,再之前是壓入棧中的參數(shù)。而ebp指向的數(shù)據(jù)是調(diào)用該函數(shù)之前的ebp的值,保存舊的ebp的值的原因是,函數(shù)返回時(shí),可以通過讀取該值恢復(fù)到調(diào)用之前的ebp的值(回到之前指向的位置)。
-
調(diào)用慣例
- 概念
??函數(shù)的調(diào)用方和被調(diào)用方對(duì)于函數(shù)如何調(diào)用需要有一個(gè)明確的約定(比如參數(shù)入棧的順序等)。這樣的約定就被稱為調(diào)用慣例。 - 調(diào)用慣例一般包括:
(1)函數(shù)參數(shù)的傳遞順序和方式。可以有很多方式,最常見的就是通過棧傳遞,參數(shù)入棧的順序要事先由調(diào)用慣例確定;有些調(diào)用慣例為了提升性能還會(huì)選擇用寄存器進(jìn)行參數(shù)傳遞。
(2)棧的維護(hù)方式。參數(shù)入棧后,函數(shù)體會(huì)被調(diào)用,參數(shù)使用的時(shí)候會(huì)被彈出的,可以是函數(shù)調(diào)用方進(jìn)行參數(shù)的彈出或者是由函數(shù)本身進(jìn)行參數(shù)的彈出。
(3)名字修飾的策略。C語言實(shí)際上存在多種調(diào)用慣例,一般默認(rèn)的(沒有顯示指定的情況下)是cdecl(不同的編輯器寫法有別):
int _cdecl foo(int n, float m);
-
foo函數(shù)布局
具體如下圖所示:
??當(dāng)foo函數(shù)返回的時(shí)候,程序會(huì)首先使用pop恢復(fù)保存在棧中的寄存器,然后從棧里取出返回地址,并返回至調(diào)用方,調(diào)用方再調(diào)整esp將堆棧恢復(fù)。下面給出一個(gè)具體的多級(jí)調(diào)用棧的布局:
- 概念
-
函數(shù)返回值傳遞
- 如果返回值的可以用4個(gè)字節(jié)表達(dá),我們經(jīng)常講返回值保存在eax寄存器里面。返回后,函數(shù)的調(diào)用方將讀取eax寄存器中的值即可。
- 對(duì)于返回4~8字節(jié)對(duì)象的情況,幾乎所有的調(diào)用慣例都采用eax和edx聯(lián)合返回的方式進(jìn)行的。
- 超過8字節(jié)的返回值(大致思路):
先看一段代碼:
#include<stdio.h>
typedef struct big_thing
{
char buf[128];
}big_thing;
big_thing return_test()
{
big_thing b;
b.buf[0] = 0;
return b;
}
int main()
{
big_thing n = return_test();
return 0;
}
- 大致解讀
- 首先main函數(shù)在棧上額外開辟一片空間,并將這塊空間的一部分作為傳遞返回值的臨時(shí)對(duì)象,這里稱為temp;
- 將temp對(duì)象的地址作為隱藏參數(shù)傳遞給return_test函數(shù);
- return_test函數(shù)將數(shù)據(jù)拷貝給temp對(duì)象,并將temp的地址用eax傳出;
- return_test函數(shù)返回后,main函數(shù)將eax指向的temp對(duì)象的內(nèi)容拷貝給了n。
返回值傳遞流程如下:

**【注意】結(jié)果返回值對(duì)象會(huì)被拷貝兩次,所以不到萬不得已不要返回大尺寸的對(duì)象。**
- 堆
-
簡介
??堆是一塊巨大的內(nèi)存空間,常常占用整個(gè)虛擬空間的絕大部分。在這片空間里,程序可以請(qǐng)求一塊連續(xù)內(nèi)存,并自由使用,這塊內(nèi)存在程序主動(dòng)放棄之前都會(huì)一直保存。 -
malloc的實(shí)現(xiàn)
??不能采用系統(tǒng)調(diào)用的方式,開銷較大;較好的做法是程序向操作系統(tǒng)申請(qǐng)一會(huì)適合大小的堆空間,然后由程序自己管理這塊空間,具體來講,管理著堆空間分配的往往是程序的運(yùn)行庫。
??運(yùn)行庫相當(dāng)于向操作系統(tǒng)”批發(fā)“了一塊較大的堆空間,之后”零售“給程序使用,如全部售完或者程序有大量的內(nèi)存需求,再根據(jù)實(shí)際情況向操作系統(tǒng)“進(jìn)貨”。 - Linux進(jìn)程堆管理
??提供兩種堆分配方式,即兩個(gè)系統(tǒng)調(diào)用:brk()系統(tǒng)調(diào)用;mmap()系統(tǒng)調(diào)用。-
brk()
該函數(shù)的C語言聲明如下:
int brk(void *end_data_segment)
??該函數(shù)申請(qǐng)堆的方式是:設(shè)置進(jìn)程的數(shù)據(jù)段的結(jié)束地址,即可以擴(kuò)大或者是縮小數(shù)據(jù)段(Linux下數(shù)據(jù)段和BSS段合稱數(shù)據(jù)段)。如果我們將數(shù)據(jù)段的結(jié)束地址向高地質(zhì)移動(dòng)(數(shù)據(jù)段變小),那么擴(kuò)大的那部分空間常常用來作為堆空間。 -
mmap()
??該函數(shù)的作用是向操作系統(tǒng)申請(qǐng)一段虛擬空間,這塊虛擬地址空間可以映射到某個(gè)文件,當(dāng)它不進(jìn)行映射的時(shí)候,我么又稱這塊空間為匿名空間,匿名空間就可以用來作為堆空間。
??glibc的malloc函數(shù)是這樣處理用戶的空間請(qǐng)求的:
(1)對(duì)于小于128kb的請(qǐng)求,它會(huì)在現(xiàn)有的堆空間里面,按照堆空間分配算法為其分配一塊地址并返回;
(2)對(duì)于大于128kb的請(qǐng)求,它會(huì)使用mmap()函數(shù)為它分配一塊匿名空間。使用mmap()函數(shù)實(shí)現(xiàn)malloc的代碼如下:
-
brk()
-
簡介
void *malloc(size_t nbytes)
{
void *ret = mmap(0,nbytes,PROT_READ | PROT_WRTIE, MAP_PRIVATE | MAP_ANONYMOUS,0,0);
if(ret == MAP_FAILED)
return 0;
return ret;
}
【需要注意的是】mmap()函數(shù)和VirtualAloc()類似,他們都是虛擬空間的申請(qǐng)函數(shù),**它們申請(qǐng)的空間的其實(shí)地址和大小必須是系統(tǒng)頁的大小的整數(shù)倍**。
- 堆分配算法
-
空閑鏈表法
- 簡介
??該方法將堆中的各個(gè)空閑塊按照鏈表的方式連接起來,當(dāng)用戶請(qǐng)求一塊空間的時(shí)候,可以遍歷整個(gè)鏈表,直到找到合適大小的塊并將它拆分,當(dāng)用戶釋放空間的時(shí)候?qū)⑺喜⒌娇臻e鏈表中。 -
結(jié)構(gòu)
??在堆里的每一個(gè)空閑空間的開頭(或結(jié)尾)有一個(gè)頭(header),頭結(jié)構(gòu)里面有兩個(gè)指針prev和next,如下圖:
- 操作過程
??首先在空閑鏈表查找足夠容納請(qǐng)求大小的一個(gè)空閑塊,然后將這個(gè)塊分為兩部分,一部分是程序請(qǐng)求的空間,另一部分是剩余的空閑空間。
【注意】當(dāng)采用空閑鏈表的方式時(shí)需要釋放空閑空間的時(shí)候,有一個(gè)簡單的解決方法:當(dāng)用戶請(qǐng)求k個(gè)字節(jié)的時(shí)候,我們實(shí)際分配k+4個(gè)字節(jié),這四個(gè)字節(jié)用來存儲(chǔ)該分配的大小。
- 簡介
-
位圖
- 核心思想
??將整個(gè)堆劃分為大量的大小相等的“塊”。當(dāng)用戶請(qǐng)求的時(shí)候,總是分配整數(shù)個(gè)塊的空間給用戶。分配的塊中,第一個(gè)塊我們稱為已分配區(qū)域的頭(Head),其余的稱為已分配區(qū)域的主體(Body)。我們使用整數(shù)數(shù)組來記錄塊的使用情況,由于每個(gè)塊只有頭/主體/空閑三種狀態(tài),因此可以使用兩個(gè)bit位來表示一個(gè)塊。 - 舉例
??假設(shè)堆的大小為1MB,那么我們讓一個(gè)塊大小為128字節(jié),則總的塊數(shù):8k/(32/2)=512個(gè)int來存儲(chǔ),。這有512個(gè)int的數(shù)組就是一個(gè)位圖,其中每兩個(gè)bit位代表一個(gè)塊。 - 優(yōu)點(diǎn)
(1)速度快。由于整個(gè)堆的空閑信息都存儲(chǔ)在一個(gè)數(shù)組內(nèi),因此訪問該數(shù)據(jù)的時(shí)候cache容易命中。
(2)穩(wěn)定性好。為避免用戶越界讀寫破壞數(shù)據(jù),我們只需簡單地備份一下位圖即可。而且即使部分?jǐn)?shù)據(jù)被破壞,也不會(huì)導(dǎo)致整個(gè)堆無法工作。
(3)塊不需要額外信息,易于管理 - 缺點(diǎn)
(1)分配內(nèi)存的時(shí)候容易產(chǎn)生碎片。例如,分配300字節(jié)時(shí),實(shí)際上分配了3個(gè)塊即384個(gè)字節(jié),這樣就浪費(fèi)了84個(gè)字節(jié)(128*3,按照整數(shù)個(gè)塊進(jìn)行分配)。
(2)如果堆很大但是設(shè)定的塊很小(這樣可以減少碎片的數(shù)量),但同時(shí)也會(huì)導(dǎo)致位圖的規(guī)模很大,可能會(huì)失去cache命中率高的優(yōu)勢(shì),同時(shí)也會(huì)浪費(fèi)一定的空間。針對(duì)這種情況,我們可以使用多級(jí)位圖(不在介紹)。
- 核心思想
-
對(duì)象池
- 使用情況
??被分配的大小是較為固定的幾個(gè)值。 - 大致思路
??如果每次分配的空間大小都一樣,那么就可以按照這個(gè)每次請(qǐng)求分配的大小作為一個(gè)單位,把整個(gè)堆空間劃分為大量的小塊,每次請(qǐng)求的時(shí)候只需要找到一個(gè)小塊就可以了。 - 管理
??對(duì)象池的管理方法可以是空閑鏈表或者是位圖。
- 使用情況
- 實(shí)際應(yīng)用
??實(shí)際的應(yīng)用中,堆的分配算法其實(shí)是采取多種算法的復(fù)合。
-
空閑鏈表法