1. 程式人生 > 其它 >(C語言記憶體九)Linux下C語言程式的記憶體佈局(記憶體模型)

(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語言中文網

對程式碼的說明:

  1. 全域性變數的記憶體在編譯時就已經分配好了,它的預設初始值是 0(它所佔用的每一個位元組都是0值),區域性變數的記憶體在函式呼叫時分配,它預設初始值是不確定的,由編譯器決定,一般是垃圾值,這在《用一個例項來深入剖析函式進棧出棧的過程》中會詳細講解。

  2. 函式 func() 中的區域性字串常量"C語言中文網"也被儲存到常量區,不會隨著 func() 的執行結束而銷燬,所以最後依然能夠輸出。

  3. 字元陣列 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。這樣中間的一部分地址正好空出來,也就是圖中的“未定義區域”,這部分記憶體無論如何也訪問不到。