內(nèi)存分配
盡管現(xiàn)在的許多高級語言已經(jīng)不需要程序員去直接處理內(nèi)存分配和垃圾回收黄绩,但是內(nèi)存的管理是學習編程過程中的一個很重要的概念,理解相關概念和應用能夠讓我們對編程和計算機有更深的理解蚜退。
一般來講,程序由下面幾部分組成:
棧:局部變量以及每次函數(shù)調(diào)用時所需要保存的信息都存放在此區(qū)域中利耍。每次函數(shù)調(diào)用時望抽,其返回地址以及調(diào)用者的環(huán)境信息都存放在棧中。然后最近被調(diào)用的函數(shù)在棧上為其局部變量分配存儲空間煌妈。
堆:堆用于存放程序運行中被動態(tài)分配的內(nèi)存儡羔,大小并不固定。
bss段:也叫未初始化數(shù)據(jù)段璧诵,存放程序中未初始化過的全局變量和靜態(tài)變量汰蜘,在程序開始執(zhí)行之前,內(nèi)核將此段的數(shù)據(jù)初始化為0之宿。
數(shù)據(jù)段:也叫初始化數(shù)據(jù)段族操,存放程序中已經(jīng)明確地初始化的全局變量和靜態(tài)變量。(如C程序中任何函數(shù)之外的聲明)
正文段:存放程序的執(zhí)行代碼,它的大小在程序運行前就已經(jīng)確定色难。通常泼舱,正文段是可以共享的,多個此程序執(zhí)行的進程在內(nèi)存中只需要一個副本枷莉。另外娇昙,正文段常常是只讀的,以防止程序由于意外而修改指令笤妙。該段也可能包含一些只讀的常數(shù)冒掌,如字符串常量等。
對C程序中內(nèi)存布局的探索
所有程序均在Linux CentOS上運行蹲盘。
數(shù)據(jù)段與bss段
運行下面代碼股毫,查看全局變量的地址(注釋為對應地址):
#include <stdio.h>
int a = 0;
int b;
int main(int argc, const char * argv[]) {
printf("%p\n", &a); //0x601038
printf("%p\n", &b); //0x60103c
return 0;
}
可以看到,未初始化的b
的地址正好在初始化過的a
的地址之上召衔,這是巧合嗎铃诬?我們再探索一下:
#include <stdio.h>
int a = 3;
int b;
int c = 5;
int d;
int main(int argc, const char * argv[]) {
printf("%p\n", &a); //0x601034
printf("%p\n", &b); //0x601044
printf("%p\n", &c); //0x601038
printf("%p\n", &d); //0x601040
return 0;
}
初始化的a
和c
在低地址,而b
和d
在高地址苍凛,所以說這并不是偶然氧急。
另外,編譯器通常對bss段的處理方式是:只描述大小毫深,不增加目標文件體積吩坝。我們可以使用size
命令來看一下編譯后的a.out
的各段大小,作為對比哑蔫,我們先對一個沒有聲明任何函數(shù)和變量的程序執(zhí)行size
命名:
$ size a.out
text data bss dec hex filename
1129 540 4 1673 689 a.out
接下來钉寝,我們聲明一個未初始化的全局的數(shù)組int a[65535];
:
#include <stdio.h>
int bss[65535];
int main(int argc, const char * argv[]) {
bss[0] = 1;
return 0;
}
執(zhí)行size
:
text data bss dec hex filename
1145 540 262176 263861 406b5 a.out
很明顯,bss段變大了闸迷,再看一下a.out
的大星陡佟:
$ ls -l a.out
-rwxr-xr-x. 1 thdlee thdlee 8848 5月 11 17:49 a.out
目標文件的大小遠遠小于65535個int
,這次直接將數(shù)組賦一個初值int bss[65535] = {1};
腥沽,再進行同樣的操作:
$ size a.out
text data bss dec hex filename
1129 262708 4 263841 406a1 a.out
$ ls -l a.out
-rwxr-xr-x. 1 thdlee thdlee 270680 5月 11 17:49 a.out
不出所料逮走,data段增大了,文件也變成了應有的大小今阳。
代碼段
代碼段的內(nèi)存地址可以用函數(shù)指針來檢測:
#include <stdio.h>
void foo() {
}
int a;
int main(int argc, const char * argv[]) {
printf("%p\n", &a); \\0x601034
printf("%p\n", &foo); \\0x40052d
return 0;
}
函數(shù)是存放在代碼段的师溅,所以看到函數(shù)的內(nèi)存地址比數(shù)據(jù)區(qū)還要低。另外盾舌,文本區(qū)一般還存放著字符串常量墓臭,我們先來看看下面這個例子:
#include <stdio.h>
int main(int argc, const char * argv[]) {
char *a = "Hello World!";
char *b = "Hello World!";
char *c = "Hello";
printf("%p\n", a); \\0x400630
printf("%p\n", b); \\0x400630
printf("%p\n", c); \\0x40063d
return 0;
}
從例子中可以看到,字符串的地址在與函數(shù)地址差不多的地方妖谴,而且對于指向相同字符串的指針變量窿锉,它們的地址是相同的。
堆和棧
一般來說,堆是由低地址向高地址增長的嗡载,而棧是由高地址向低地址增長窑多。
按照慣例,我們還是用一個小程序來探索一下:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
char *p1 = malloc(16);
char *p2 = malloc(16);
printf("%p\n", &a); \\0x7ffe145134dc
printf("%p\n", &b); \\0x7ffe145134d8
printf("%p\n", p1); \\0x1478010
printf("%p\n", p2); \\0x1478030
free(p1);
free(p2);
return 0;
}
根據(jù)變量的聲明順序洼滚,可以看到棧是向下增長怯伊,而堆是向上增長的。這里要注意的一點是判沟,指針p1
和p2
存儲的是指向?qū)Φ牡刂罚撬鼈儍蓚€的存儲位置是在棧上的崭篡。我們再來看看函數(shù)調(diào)用中變量地址的變化挪哄。
#include <stdio.h>
void foo2() {
int c;
printf("%p\n", &c); \\0x7fffb97a99ec
}
void foo1() {
int b;
printf("%p\n", &b); \\0x7fffb97a9a0c
foo2();
}
void bar() {
int d;
printf("%p\n", &d); \\0x7fffb97a9a0c
}
int main(int argc, const char * argv[]) {
int a;
printf("%p\n", &a); \\0x7fffb97a9a3c
foo1();
bar();
return 0;
}
隨著函數(shù)的開始,棧也開始向下擴展琉闪。當函數(shù)結束時迹炼,分配在棧上的空間也跟著收回。
更進一步地探討
至此颠毙,我們就會對程序中內(nèi)存分配和管理有了一定的了解斯入,但是程序中的內(nèi)存地址是怎么來的呢?它們是計算機中的物理地址嗎蛀蜜?如果不是刻两,那又和物理地址有什么關系呢?
這幾個問題牽扯到了編譯器的和操作系統(tǒng)的一些相關知識滴某,但對這些內(nèi)容深入地探討超出了本文的范圍磅摹,因此本文只能盡量描述清楚其中的關系。
進入正題霎奢,編譯器將代碼轉換為可執(zhí)行程序時户誓,必須為代碼產(chǎn)生的各個值分別分配一個存儲位置。編譯器必須理解值的類型幕侠、長度帝美、可見性和生命周期。編譯器必須考慮一系列對代碼的內(nèi)存處理問題來定義一組約定來解決這些問題晤硕。
為分配存儲悼潭,編譯器必須理解全系統(tǒng)范圍內(nèi)對內(nèi)存分配和使用的約定。編譯器舞箍、操作系統(tǒng)和處理器協(xié)助女责,以確保多個程序能夠以交錯的方式(時間片)安全地執(zhí)行。
除了創(chuàng)建棧创译,對于大多數(shù)語言抵知,編譯器都需要創(chuàng)建堆,以便為動態(tài)分配的數(shù)據(jù)結構提供內(nèi)存。為保證高效地利用內(nèi)存空間刷喜,堆和棧被置于開放空間的兩端残制,彼此相向增長,所以就出現(xiàn)了我們所觀察到的現(xiàn)象掖疮。當然初茶,將堆和棧互換位置效果也是一樣的浊闪。下圖是單個程序編譯后所用地址空間的典型布局恼布,具體的實現(xiàn)和細節(jié)可能會因編譯器和語言的不同而不同:
這只是編譯器的視角所看到的地址空間,編譯器會為編譯的每個程序分配一個獨立的地址空間搁宾,讓程序以為自己擁有獨立的內(nèi)存(其實也讓程序員以為程序有獨立的內(nèi)存)折汞,其實這只是假象。在執(zhí)行程序時盖腿,操作系統(tǒng)會將這些邏輯地址空間映射到處理器支持的物理地址空間中爽待。
所以說,我們所看到的只是邏輯地址翩腐,我們在代碼中對所謂內(nèi)存的操作鸟款,也只是對邏輯地址的操作,這些操作的具體過程是由編譯器和操作系統(tǒng)以及硬件為我們完成的茂卦。