(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。這樣中間的一部分地址正好空出來,也就是圖中的“未定義區域”,這部分內存無論如何也訪問不到。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM