我們在學習C程序開發時經常會遇到一些概念:代碼段、數據段、BSS段(Block Started by Symbol) 、堆(heap)和棧(stack)。先看一張教材上的示意圖(來源,《UNIX環境高級編程》一書),顯示了進程地址空間中典型的存儲區域分配情況。
從圖中可以看出:
從低地址到高地址分別為:代碼段、(初始化)數據段、(未初始化)數據段(BSS)、堆、棧、命令行參數和環境變數堆向高內存地址生長棧向低內存地址生長
還經常看到下面這個圖(來源,不詳):
先看一段程序。
- #include
- #include
- int global_init_a=1;
- int global_uninit_a;
- static int static_global_init_a=1;
- static int static_global_uninit_a;
- const int const_global_a=1;
- int global_init_b=1;
- int global_uninit_b;
- static int static_global_init_b=1;
- static int static_global_uninit_b;
- const int const_global_b=1; /*上面全部為全局變數,main函數中的為局部變數*/ int main()
- int local_init_a=1;
- int local_uninit_a;
- static int static_local_init_a=1;
- static int static_local_uninit_a;
- const int const_local_a=1;
- int local_init_b=1;
- int local_uninit_b;
- static int static_local_init_b=1;
- static int static_local_uninit_b;
- const int const_local_b=1;
- int * malloc_p_a;
- malloc_p_a=malloc(sizeof(int));
- printf(" &global_init_a=%p
- global_init_a=%d ",&global_init_a,global_init_a);
- printf(" &global_uninit_a=%p
- global_uninit_a=%d ",&global_uninit_a,global_uninit_a);
- printf(" &static_global_init_a=%p
- static_global_init_a=%d ",&static_global_init_a,static_global_init_a);
- printf("&static_global_uninit_a=%p
- static_global_uninit_a=%d ",&static_global_uninit_a,static_global_uninit_a);
- printf(" &const_global_a=%p
- const_global_a=%d ",&const_global_a,const_global_a);
- printf(" &global_init_b=%p
- global_init_b=%d ",&global_init_b,global_init_b);
- printf(" &global_uninit_b=%p
- global_uninit_b=%d ",&global_uninit_b,global_uninit_b);
- printf(" &static_global_init_b=%p
- static_global_init_b=%d ",&static_global_init_b,static_global_init_b);
- printf("&static_global_uninit_b=%p
- static_global_uninit_b=%d ",&static_global_uninit_b,static_global_uninit_b);
- printf(" &const_global_b=%p
- const_global_b=%d ",&const_global_b,const_global_b);
- printf(" &local_init_a=%p
- local_init_a=%d ",&local_init_a,local_init_a);
- printf(" &local_uninit_a=%p
- local_uninit_a=%d ",&local_uninit_a,local_uninit_a);
- printf(" &static_local_init_a=%p
- static_local_init_a=%d ",&static_local_init_a,static_local_init_a);
- printf(" &static_local_uninit_a=%p
- static_local_uninit_a=%d ",&static_local_uninit_a,static_local_uninit_a);
- printf(" &const_local_a=%p
- const_local_a=%d ",&const_local_a,const_local_a);
- printf(" &local_init_b=%p
- local_init_b=%d ",&local_init_b,local_init_b);
- printf(" &local_uninit_b=%p
- local_uninit_b=%d ",&local_uninit_b,local_uninit_b);
- printf(" &static_local_init_b=%p
- static_local_init_b=%d ",&static_local_init_b,static_local_init_b);
- printf(" &static_local_uninit_b=%p
- static_local_uninit_b=%d ",&static_local_uninit_b,static_local_uninit_b);
- printf(" &const_local_b=%p
- const_local_b=%d ",&const_local_b,const_local_b);
- printf(" malloc_p_a=%p
- *malloc_p_a=%d ",malloc_p_a,*malloc_p_a);
- return 0;
下面是輸出結果。
先仔細分析一下上面的輸出結果,看看能得出什麼結論。貌似很難分析出來什麼結果。好了我們繼續往下看吧。
接下來,通過查看proc文件系統下的文件,看一下這個進程的真實內存分配情況。(我們需要在程序結束前加一個死循環,不讓進程結束,以便我們進一步分析)。
在return 0前,增加 while(1); 語句
重新編譯后,運行程序,程序將進入死循環。
使用ps命令查看一下進程的pid
#ps -aux | grep a.out
查看/proc/2699/maps文件,這個文件顯示了進程在內存空間中各個區域的分配情況。
#cat /proc/2699/maps
上面紅顏色標出的幾個區間是我們感興趣的區間:
08048000-08049000 r-xp 貌似是代碼段08049000-0804a000 r--p 暫時不清楚,看不出來0804a000-0804b000 rw-p 貌似為數據段08a7e000-08a9f000 rw-p 堆bff73000-bff88000 rw-p 棧
我們把這些數據與最後一次的程序運行結果進行比較,看看有什麼結論。
&global_init_a=0x804a018 全局初始化:數據段 global_init_a=1
&global_uninit_a=0x804a04c 全局未初始化:數據段 global_uninit_a=0
&static_global_init_a=0x804a01c 全局靜態初始化:數據段 static_global_init_a=1
&static_global_uninit_a=0x804a038 全局靜態未初始化:數據段 static_global_uninit_a=0
&const_global_a=0x80487c0 全局只讀變數: 代碼段 const_global_a=1
&global_init_b=0x804a020 全局初始化:數據段 global_init_b=1
&global_uninit_b=0x804a048 全局未初始化:數據段 global_uninit_b=0
&static_global_init_b=0x804a024 全局靜態初始化:數據段 static_global_init_b=1
&static_global_uninit_b=0x804a03c 全局靜態未初始化:數據段 static_global_uninit_b=0
&const_global_b=0x80487c4 全局只讀變數: 代碼段 const_global_b=1
&local_init_a=0xbff8600c 局部初始化:棧 local_init_a=1
&local_uninit_a=0xbff86008 局部未初始化:棧 local_uninit_a=134514459
&static_local_init_a=0x804a028 局部靜態初始化:數據段 static_local_init_a=1
&static_local_uninit_a=0x804a040 局部靜態未初始化:數據段 static_local_uninit_a=0
&const_local_a=0xbff86004 局部只讀變數:棧 const_local_a=1
&local_init_b=0xbff86000 局部初始化:棧 local_init_b=1
&local_uninit_b=0xbff85ffc 局部未初始化:棧 local_uninit_b=-1074241512
&static_local_init_b=0x804a02c 局部靜態初始化:數據段 static_local_init_b=1
&static_local_uninit_b=0x804a044 局部靜態未初始化:數據段 static_local_uninit_b=0
&const_local_b=0xbff85ff8 局部只讀變數:棧 const_local_b=1
p_chars=0x80487c8 字元串常量:代碼段 p_chars=abcdef
malloc_p_a=0x8a7e008 malloc動態分配:堆 *malloc_p_a=0
通過以上分析我們暫時可以得到的結論如下,在進程的地址空間中:
數據段中存放:全局變數(初始化以及未初始化的)、靜態變數(全局的和局部的、初始化的以及未初始化的)代碼段中存放:全局只讀變數(const)、字元串常量堆中存放:動態分配的區域棧中存放:局部變數(初始化以及未初始化的,但不包含靜態變數)、局部只讀變數(const)
這裡我們沒有發現BSS段,但是我們將未初始化的數據按照地址進行排序看一下,可以發現一個規律。
&global_init_a=0x804a018 全局初始化:數據段 global_init_a=1
&static_global_init_a=0x804a01c 全局靜態初始化:數據段 static_global_init_a=1
&global_init_b=0x804a020 全局初始化:數據段 global_init_b=1
&static_global_init_b=0x804a024 全局靜態初始化:數據段 static_global_init_b=1
&static_local_init_a=0x804a028 局部靜態初始化:數據段 static_local_init_a=1
&static_local_init_b=0x804a02c 局部靜態初始化:數據段 static_local_init_b=1
&static_global_uninit_a=0x804a038 全局靜態未初始化:數據段 static_global_uninit_a=0
&static_global_uninit_b=0x804a03c 全局靜態未初始化:數據段 static_global_uninit_b=0
&static_local_uninit_a=0x804a040 局部靜態未初始化:數據段 static_local_uninit_a=0
&static_local_uninit_b=0x804a044 局部靜態未初始化:數據段 static_local_uninit_b=0
&global_uninit_b=0x804a048 全局未初始化:數據段 global_uninit_b=0
&global_uninit_a=0x804a04c 全局未初始化:數據段 global_uninit_a=0
這裡可以發現,初始化的和未初始化的數據好像是分開存放的,因此我們可以猜測BSS段是存在的,只不過數據段是分為初始化和未初始化(即BSS段)的兩部分,他們在載入到進程地址空間時是合併為數據段了,在進程地址空間中沒有單獨分為一個區域。
還有一個問題,靜態數據與非靜態數據是否是分開存放的呢?請讀者自行分析一下。
接下來我們從程序的角度看一下,這些存儲區域是如何分配的。首先我們先介紹一下ELF文件格式。
ELF(Executable and Linkable Format )文件格式是一個開放標準,各種UNIX系統的可執行文件都採用ELF格式,它有三種不同的類型:–可重定位的目標文件(Relocatable,或者Object File)–可執行文件(Executable)–共享庫(Shared Object,或者Shared Library)下圖為ELF文件的結構示意圖(來源,不詳):
一個程序編譯生成目標代碼文件(ELF文件)的過程如下,此圖引自《程序員的自我修養》一書的一個圖:
可以通過readelf命令查看EFL文件的相關信息,例如 readelf -a a.out ,我們只關心各個段的分配情況,因此我們使用以下命令:
# readelf -S a.out
將這裡的內存布局與之前看到的程序的運行結果進行分析:
&global_init_a=0x804a018 全局初始化:數據段 global_init_a=1
&global_uninit_a=0x804a04c 全局未初始化:BSS段 global_uninit_a=0
&static_global_init_a=0x804a01c 全局靜態初始化:數據段 static_global_init_a=1
&static_global_uninit_a=0x804a038 全局靜態未初始化:BSS段 static_global_uninit_a=0
&const_global_a=0x80487c0 全局只讀變數: 只讀數據段 const_global_a=1
&global_init_b=0x804a020 全局初始化:數據段 global_init_b=1
&global_uninit_b=0x804a048 全局未初始化:BSS段 global_uninit_b=0
&static_global_init_b=0x804a024 全局靜態初始化:數據段 static_global_init_b=1
&static_global_uninit_b=0x804a03c 全局靜態未初始化:BSS段 static_global_uninit_b=0
&const_global_b=0x80487c4 全局只讀變數: 只讀數據段 const_global_b=1
&static_local_init_a=0x804a028 局部靜態初始化:數據段 static_local_init_a=1
&static_local_uninit_a=0x804a040 局部靜態未初始化:BSS段 static_local_uninit_a=0
&static_local_init_b=0x804a02c 局部靜態初始化:數據段 static_local_init_b=1
&static_local_uninit_b=0x804a044 局部靜態未初始化:BSS段 static_local_uninit_b=0
p_chars=0x80487c8 字元串常量:只讀數據段 p_chars=abcdef
ELF 文件一般包含以下幾個段 :
.text section:主要是編譯后的源碼指令,是只讀欄位。.data section :初始化后的非const的全局變數、局部static變數。.bss:未初始化后的非const全局變數、局部static變數。.rodata欄位 是存放只讀數據
分析到這以後,我們在和之前分析的結果對比一下,會發現確實存在BSS段,地址為0804a030 ,大小為0x20,之前我們的程序中未初始化的的確存放在這個地址區間中了,只不過執行exec系統調用時,將這部分的數據初始化為0后,放到了進程地址空間的數據段中了,在進程地址空間中就沒有必要存在BSS段了,因此都稱做數據段。同理,.rodata欄位也是與text段放在一起了。
在ELF文件中,找不到局部非靜態變數和動態分配的內容。
[火星人 ] Linux下C程序進程地址空間布局已經有405次圍觀