(C語言記憶體九)Linux下C語言程式的記憶體佈局(記憶體模型)
在《虛擬地址空間以及編譯模式》一節中講到,虛擬地址空間在32位環境下的大小為 4GB,在64位環境下的大小為 256TB,那麼,一個C語言程式的記憶體在整個地址空間中是如何分佈的呢?資料在哪裡?程式碼在哪裡?為什麼要這樣分佈?這些就是本節要講解的內容。
程式記憶體在地址空間中的分佈情況稱為記憶體模型(Memory Model)。記憶體模型由作業系統構建,在Linux和Windows下有所差異,並且會受到編譯模式的影響,本節我們講解Linux下32位環境和64位環境的記憶體模型。
核心空間和使用者空間
對於32位環境,理論上程式可以擁有 4GB 的虛擬地址空間,我們在C語言中使用到的變數、函式、字串等都會對應記憶體中的一塊區域。
但是,在這 4GB 的地址空間中,要拿出一部分給作業系統核心使用,應用程式無法直接訪問這一段記憶體,這一部分記憶體地址被稱為核心空間(Kernel Space)。
Windows 在預設情況下會將高地址的 2GB 空間分配給核心(也可以配置為1GB),而 Linux 預設情況下會將高地址的 1GB 空間分配給核心。也就是說,應用程式只能使用剩下的 2GB 或 3GB 的地址空間,稱為使用者空間(User Space)。
Linux下32位環境的使用者空間記憶體分佈情況
我們暫時不關心核心空間的記憶體分佈情況,下圖是Linux下32位環境的一種經典記憶體模型:
對各個記憶體分割槽的說明:
記憶體分割槽 | s說明 |
---|---|
程式程式碼區(code) | 存放函式體的二進位制程式碼。一個C語言程式由多個函式構成,C語言程式的執行就是函式之間的相互呼叫。 |
常量區(constant) | 存放一般的常量、字串常量等。這塊記憶體只有讀取許可權,沒有寫入許可權,因此它們的值在程式執行期間不能改變。 |
全域性資料區(global data) | 存放全域性變數、靜態變數等。這塊記憶體有讀寫許可權,因此它們的值在程式執行期間可以任意改變。 |
堆區(heap) | 一般由程式設計師分配和釋放,若程式設計師不釋放,程式執行結束時由作業系統回收。malloc()、calloc()、free() 等函式操作的就是這塊記憶體,這也是本章要講解的重點。注意:這裡所說的堆區與資料結構中的堆不是一個概念,堆區的分配方式倒是類似於連結串列。 |
動態連結庫 | 用於在程式執行期間載入和解除安裝動態連結庫。 |
棧區(stack) | 存放函式的引數值、區域性變數的值等,其操作方式類似於資料結構中的棧。 |
在這些記憶體分割槽中(暫時不討論動態連結庫),程式程式碼區用來儲存指令,常量區、全域性資料區、堆、棧都用來儲存資料。對記憶體的研究,重點是對資料分割槽的研究。
程式程式碼區、常量區、全域性資料區在程式載入到記憶體後就分配好了,並且在程式執行期間一直存在,不能銷燬也不能增加(大小已被固定),只能等到程式執行結束後由作業系統收回,所以全域性變數、字串常量等在程式的任何地方都能訪問,因為它們的記憶體一直都在。
常量區和全域性資料區有時也被合稱為靜態資料區,意思是這段記憶體專門用來儲存資料,在程式執行期間一直存在。
函式被呼叫時,會將引數、區域性變數、返回地址等與函式相關的資訊壓入棧中,函式執行結束後,這些資訊都將被銷燬。所以區域性變數、引數只在當前函式中有效,不能傳遞到函式外部,因為它們的記憶體不在了。
常量區、全域性資料區、棧上的記憶體由系統自動分配和釋放,不能由程式設計師控制。程式設計師唯一能控制的記憶體區域就是堆(Heap):它是一塊巨大的記憶體空間,常常佔據整個虛擬空間的絕大部分,在這片空間中,程式可以申請一塊記憶體,並自由地使用(放入任何資料)。堆記憶體在程式主動釋放之前會一直存在,不隨函式的結束而失效。在函式內部產生的資料只要放到堆中,就可以在函式外部使用。
一個例項
為了加深對記憶體佈局的理解,請大家看下面一段程式碼:
#include <stdio.h>
char *str1 = "c.biancheng.net"; //字串在常量區,str1在全域性資料區
int n; //全域性資料區
char* func(){
char *str = "C語言中文網"; //字串在常量區,str在棧區
return str;
}
int main(){
int a; //棧區
char *str2 = "01234"; //字串在常量區,str2在棧區
char arr[20] = "56789"; //字串和arr都在棧區
char *pstr = func(); //棧區
int b; //棧區
printf("str1: %#X\npstr: %#X\nstr2: %#X\n", str1, pstr, str2);
puts("--------------");
printf("&str1: %#X\n &n: %#X\n", &str1, &n);
puts("--------------");
printf(" &a: %#X\n arr: %#X\n &b: %#X\n", &a, arr, &b);
puts("--------------");
printf("n: %d\na :%d\nb: %d\n", n, a, b);
puts("--------------");
printf("%s\n", pstr);
return 0;
}
執行結果:
str1: 0X400710
pstr: 0X400720
str2: 0X400731
&str1: 0X601040
&n: 0X60104C
&a: 0X19D0728C
arr: 0X19D07270
&b: 0X19D0726C
n: 0
a: -858993460
b: -858993460
C語言中文網
對程式碼的說明:
-
全域性變數的記憶體在編譯時就已經分配好了,它的預設初始值是 0(它所佔用的每一個位元組都是0值),區域性變數的記憶體在函式呼叫時分配,它預設初始值是不確定的,由編譯器決定,一般是垃圾值,這在《用一個例項來深入剖析函式進棧出棧的過程》中會詳細講解。
-
函式 func() 中的區域性字串常量"C語言中文網"也被儲存到常量區,不會隨著 func() 的執行結束而銷燬,所以最後依然能夠輸出。
-
字元陣列 arr[20] 在棧區分配記憶體,字串"56789"就儲存在這塊記憶體中,而不是在常量區,大家要注意區分。
Linux下64位環境的使用者空間記憶體分佈情況
在64位環境下,虛擬地址空間大小為 256TB,Linux 將高 128TB 的空間分配給核心使用,而將低 128TB 的空間分配給使用者程式使用。如下圖所示:
《虛擬地址空間以及編譯模式》一節中講到,在64位環境下,虛擬地址雖然佔用64位,但只有最低48位有效。這裡需要補充的一點是,任何虛擬地址的48位至63位必須與47位一致。
上圖中,使用者空間地址的47位是0,所以高16位也是0,換算成十六進位制形式,最高的四個數都是0;核心空間地址的47位是1,所以高16位也是1,換算成十六進位制形式,最高的四個數都是1。這樣中間的一部分地址正好空出來,也就是圖中的“未定義區域”,這部分記憶體無論如何也訪問不到。