1. 程式人生 > >程式或-記憶體區域分配(五個段)

程式或-記憶體區域分配(五個段)

一.

在學習之前我們先看看ELF檔案。

ELF分為三種類型:.o 可重定位檔案(relocalble file),可執行檔案以及共享庫(shared library),三種格式基本上從結構上是一樣的,只是具體到每一個結構不同。下面我們就從整體上看看這3種格式從檔案內容上儲存的方式,spec上有張圖是比較經典的:如上圖:
 其實從檔案儲存的格式來說,上面的兩種view實際上是一樣的,Segment實際上就是由section組成的,將相應的一些section對映到一起就叫segment了,就是說segment是由0個或多個section組成的,實際上本質都是section。在這裡我們首先來仔細瞭解一下section和segment的概念:section就是相同或者相似資訊的集合,比如我們比較熟悉的.text .data  .bss section,.text是可執行指令的集合,.data是初始化後資料的集合,.bss是未初始化資料的集合。實際上我們也可以將一個程式的所有內容都放在一起,就像dos一樣,但是將可執行程式分成多個section是很有好處的,比如說我們可以將.text section放在memory的只讀空間內,將可變的.data section放在memory的可寫空間內。

從可執行檔案的角度來講,如果一個數據未被初始化那就不需要為其分配空間,所以.data和.bss一個重要的區別就是.bss並不佔用可執行檔案的大小,它只是記載需要多少空間來儲存這些未初始化資料,而不分配實際的空間。

可以通過命令 $ readelf -l a.out 檢視檔案的格式和組成。

二.


站在組合語言的角度,一個程式分為:
資料段 -- DS
堆疊段 -- SS
程式碼段 -- CS
擴充套件段 -- ES

站在高階語言的角度,根據APUE,一個程式分為如下段:
text
data (initialized)
bss
stack
heap

        1.一般情況下,一個可執行二進位制程式(更確切的說,在Linux作業系統下為一個程序單元,在UC/OSII中被稱為任務)在儲存(沒有調入到記憶體執行)時擁有3個部分,分別是程式碼段(text)、資料段(data)和BSS段。這3個部分一起組成了該可執行程式的檔案。

★★可執行二進位制程式 = 程式碼段(text)+資料段(data)+BSS段★★

        2.而當程式被載入到記憶體單元時,則需要另外兩個域:堆域和棧域。圖1-1所示為可執行程式碼儲存態和執行態的結構對照圖。一個正在執行的C程式佔用的記憶體區域分為程式碼段、初始化資料段、未初始化資料段(BSS)、堆、棧5個部分。

★★正在執行的C程式 = 程式碼段+初始化資料段(data)+未初始化資料段(BSS)+堆+棧★★

       3.在將應用程式載入到記憶體空間執行時,作業系統負責程式碼段、資料段和BSS段的載入,並將在記憶體中為這些段分配空間。棧亦由作業系統分配和管理,而不需要程式設計師顯示地管理;堆段由程式設計師自己管理,即顯示地申請和釋放空間。

          4.動態分配與靜態分配,二者最大的區別在於:1. 直到Run-Time的時候,執行動態分配,而在compile-time的時候,就已經決定好了分配多少Text+Data+BSS+Stack。2.通過malloc()動態分配的記憶體,需要程式設計師手工呼叫free()釋放記憶體,否則容易導致記憶體洩露,而靜態分配的記憶體則在程序執行結束後系統釋放(Text, Data), 但Stack段中的資料很短暫,函式退出立即被銷燬。

 圖1-1(從可執行檔案a.out的角度來講,如果一個數據未被初始化那就不需要為其分配空間,所以.data和.bss一個重要的區別就是.bss並不佔用可執行檔案的大小,它只是記載需要多少空間來儲存這些未初始化資料,而不分配實際的空間)

三.

程式碼段 --text(code segment/text segment)
text段在記憶體中被對映為只讀,但.data和.bss是可寫的。
text段是程式程式碼段,在AT91庫中是表示程式段的大小,它是由編譯器在編譯連線時自動計算的,當你在連結定位檔案中將該符號放置在程式碼段後,那麼該符號表示的值就是程式碼段大小,編譯連線時,該符號所代表的值會自動代入到源程式中。

資料段 -- data
data包含靜態初始化的資料,所以有初值的全域性變數和static變數在data區。段的起始位置也是由連線定位檔案所確定,大小在編譯連線時自動分配,它和你的程式大小沒有關係,但和程式使用到的全域性變數,常量數量相關。資料段屬於靜態記憶體分配。 

bss段--bss
bss是英文Block Started by Symbol的簡稱,通常是指用來存放程式中未初始化的全域性變數的一塊記憶體區域,在程式載入時由核心清0。BSS段屬於靜態記憶體分配。它的初始值也是由使用者自己定義的連線定位檔案所確定,使用者應該將它定義在可讀寫的RAM區內,源程式中使用malloc分配的記憶體就是這一塊,它不是根據data大小確定,主要由程式中同時分配記憶體最大值所確定,不過如果超出了範圍,也就是分配失敗,可以等空間釋放之後再分配。BSS段屬於靜態記憶體分配。

stack:
棧(stack)儲存函式的區域性變數(但不包括static宣告的變數, static 意味著 在資料段中 存放變數),引數以及返回值。是一種“後進先出”(Last In First Out,LIFO)的資料結構,這意味著最後放到棧上的資料,將會是第一個從棧上移走的資料。對於哪些暫時存貯的資訊,和不需要長時間儲存的資訊來說,LIFO這種資料結構非常理想。在呼叫函式或過程後,系統通常會清除棧上儲存的區域性變數、函式呼叫資訊及其它的資訊。棧另外一個重要的特徵是,它的地址空間“向下減少”,即當棧上儲存的資料越多,棧的地址就越低。棧(stack)的頂部在可讀寫的RAM區的最後。

heap:
堆(heap)儲存函式內部動態分配記憶體,是另外一種用來儲存程式資訊的資料結構,更準確的說是儲存程式的動態變數。堆是“先進先出”(First In first Out,FIFO)資料結構。它只允許在堆的一端插入資料,在另一端移走資料。堆的地址空間“向上增加”,即當堆上儲存的資料越多,堆的地址就越高。


下圖是APUE中的一個典型C記憶體空間分佈圖:

所以可以知道傳入的引數,區域性變數,都是在棧頂分佈,隨著子函式的增多而向下增長.
函式的呼叫地址(函式執行程式碼),全域性變數,靜態變數都是在分配記憶體的低部存在,而malloc分配的堆則存在於這些記憶體之上,並向上生長.

舉例1:

#include <stdio h="">
const int    g_A       = 10;         //程式碼段
int          g_B       = 20;         //資料段
static int   g_C       = 30;         //資料段
static int   g_D;                    //BSS段
int          g_E;                    //BSS段
char        *p1;                     //BSS段

void main( )
{
    int           local_A;            //棧
    int           local_B;            //棧
    static int    local_C = 0;        //資料段
    static int    local_D;            //資料段
    
    char        *p3 = "123456";     //123456在程式碼段,p3在棧上

    p1 = (char *)malloc( 10 );      //堆,分配得來得10位元組的區域在堆區
    strcpy( p1, "123456" );         //123456{post.content}放在常量區,編譯器可能會將它與p3所指向 的"123456"優化成一塊

    printf("hight address\n");
    printf("-------------棧--------------\n");
    printf( "棧,    區域性變數,                           local_A, addr:0x%08x\n", &local_A );
    printf( "棧,    區域性變數,(後進棧地址相對local_A低)  local_B, addr:0x%08x\n", &local_B );
    printf("-------------堆--------------\n");
    printf( "堆,    malloc分配記憶體,             p1,      addr:0x%08x\n", p1 );
    printf("------------BSS段------------\n");
    printf( "BSS段, 全域性變數,       未初始化    g_E,     addr:0x%08x\n", &g_E, g_E );    
    printf( "BSS段, 靜態全域性變數,   未初始化,   g_D,     addr:0x%08x\n", &g_D );
    printf( "BSS段, 靜態區域性變數,   初始化,     local_C, addr:0x%08x\n", &local_C);
    printf( "BSS段, 靜態區域性變數,   未初始化,   local_D, addr:0x%08x\n", &local_D);
    printf("-----------資料段------------\n");
    printf( "資料段,全域性變數,       初始化      g_B,     addr:0x%08x\n", &g_B);
    printf( "資料段,靜態全域性變數,   初始化,     g_C,     addr:0x%08x\n", &g_C);
    printf("-----------程式碼段------------\n");
    printf( "程式碼段,全域性初始化變數, 只讀const,  g_A,     addr:0x%08x\n\n", &g_A);
    printf("low address\n");
    return;
}
</stdio>


執行結果:
 

hight address  
-------------棧--------------  
棧,    區域性變數,                           local_A, addr:0xffa70c1c  
棧,    區域性變數,(後進棧地址相對local_A低)  local_B, addr:0xffa70c18  
-------------堆--------------  
堆,    malloc分配記憶體,             p1,      addr:0x087fe008  
------------BSS段------------  
BSS段, 全域性變數,       未初始化    g_E,     addr:0x08049a64  
BSS段, 靜態全域性變數,   未初始化,   g_D,     addr:0x08049a5c  
BSS段, 靜態區域性變數,   初始化,     local_C, addr:0x08049a58  
BSS段, 靜態區域性變數,   未初始化,   local_D, addr:0x08049a54  
-----------資料段------------  
資料段,全域性變數,       初始化      g_B,     addr:0x08049a44  
資料段,靜態全域性變數,   初始化,     g_C,     addr:0x08049a48  
-----------程式碼段------------  
程式碼段,全域性初始化變數, 只讀const,  g_A,     addr:0x08048620  
  
low address 


注意:
編譯時需要-g選項,這樣才可以看elf資訊;
readelf -a a.out 

檢視這個執行檔案的elf資訊,摘錄部分如下:重點注意其中data段,text段還要有bss段的地址,然後比較這個地址和上面的執行結果,是否是在elf檔案的各個段的地址之內。


Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        08048114 000114 000013 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            08048128 000128 000020 00   A  0   0  4
  [ 3] .gnu.hash         GNU_HASH        08048148 000148 000020 04   A  4   0  4
  [ 4] .dynsym           DYNSYM          08048168 000168 000070 10   A  5   1  4
  [ 5] .dynstr           STRTAB          080481d8 0001d8 000058 00   A  0   0  1
  [ 6] .gnu.version      VERSYM          08048230 000230 00000e 02   A  4   0  2
  [ 7] .gnu.version_r    VERNEED         08048240 000240 000020 00   A  5   1  4
  [ 8] .rel.dyn          REL             08048260 000260 000008 08   A  4   0  4
  [ 9] .rel.plt          REL             08048268 000268 000028 08   A  4  11  4
  [10] .init             PROGBITS        08048290 000290 000017 00  AX  0   0  4
  [11] .plt              PROGBITS        080482a8 0002a8 000060 04  AX  0   0  4
  [12] .text             PROGBITS        08048310 000310 0002e8 00  AX  0   0 16
  [13] .fini             PROGBITS        080485f8 0005f8 00001c 00  AX  0   0  4
  [14] .rodata           PROGBITS        08048614 000614 000326 00   A  0   0  4
  [15] .eh_frame         PROGBITS        0804893c 00093c 000004 00   A  0   0  4
  [16] .ctors            PROGBITS        08049940 000940 000008 00  WA  0   0  4
  [17] .dtors            PROGBITS        08049948 000948 000008 00  WA  0   0  4
  [18] .jcr              PROGBITS        08049950 000950 000004 00  WA  0   0  4
  [19] .dynamic          DYNAMIC         08049954 000954 0000c8 08  WA  5   0  4
  [20] .got              PROGBITS        08049a1c 000a1c 000004 04  WA  0   0  4
  [21] .got.plt          PROGBITS        08049a20 000a20 000020 04  WA  0   0  4
  [22] .data             PROGBITS        08049a40 000a40 00000c 00  WA  0   0  4
  [23] .bss              NOBITS          08049a4c 000a4c 00001c 00  WA  0   0  4
  [24] .comment          PROGBITS        00000000 000a4c 000114 00      0   0  1
  [25] .debug_aranges    PROGBITS        00000000 000b60 000020 00      0   0  1
  [26] .debug_pubnames   PROGBITS        00000000 000b80 00003a 00      0   0  1
  [27] .debug_info       PROGBITS        00000000 000bba 0001f4 00      0   0  1
  [28] .debug_abbrev     PROGBITS        00000000 000dae 00006f 00      0   0  1
  [29] .debug_line       PROGBITS        00000000 000e1d 000058 00      0   0  1
  [30] .debug_frame      PROGBITS        00000000 000e78 00003c 00      0   0  4
  [31] .debug_str        PROGBITS        00000000 000eb4 00000d 00      0   0  1
  [32] .debug_loc        PROGBITS        00000000 000ec1 000043 00      0   0  1
  [33] .shstrtab         STRTAB          00000000 000f04 000143 00      0   0  1
  [34] .symtab           SYMTAB          00000000 0015e8 000560 10     35  60  4
  [35] .strtab           STRTAB          00000000 001b48 0002ad 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)


★★★★注意靜態變數初始化為零和全域性靜態變數初始化為零的情況,都是儲存在bss段★★★★

從上面的elf檔案可以看出,

  [23] .bss              NOBITS          08049a4c 000a4c 00001c 00  WA  0   0  4

  [22] .data             PROGBITS    08049a40000a40 00000c 00  WA  0   0  4

  [12] .text             PROGBITS      08048310000310 0002e8 00  AX  0   0 16

但是在結果中顯示: BSS段, 靜態區域性變數,   初始化,    local_C, addr:0x08049a58 

 (0x08049a58 大於0x08049a4c 屬於bss段)是初始化的靜態區域性變數但是卻屬於bss段,為什麼?

原因是:local_C是區域性靜態變數但是卻初始化為零。這和沒有初始化,預設是零的情況一樣,都儲存在bss段,如果初始化為其他的值,那麼local_C這個變數就會儲存在data段。

可執行檔案大小由什麼決定?

可執行檔案在儲存時分為程式碼段、資料段和BSS段三個部分。

【例一】
程式1:
int ar[30000];
void main()
{
    ......

程式2:
int ar[300000] =  {1, 2, 3, 4, 5, 6 };
void main()
{
    ......

發現程式2編譯之後所得的.exe檔案比程式1的要大得多。當下甚為不解,於是手工編譯了一下,並使用了/FAs編譯選項來查看了一下其各自的.asm,發現在程式1.asm中ar的定義如下:
_BSS SEGMENT
     [email protected]@3PAHA DD 0493e0H DUP (?)    ; ar
_BSS ENDS 
而在程式2.asm中,ar被定義為:
_DATA SEGMENT
     [email protected]@3PAHA DD 01H     ; ar
                DD 02H
                DD 03H
                ORG $+1199988
_DATA ENDS 
區別很明顯,一個位於.bss段,而另一個位於.data段,兩者的區別在於:全域性的未初始化變數存在於.bss段中,具體體現為一個佔位符;全域性的已初始化變數存於.data段中;而函式內的自動變數都在棧上分配空間。

.bss是不佔用.exe檔案空間的,其內容由作業系統初始化(清零);而.data卻需要佔用,其內容由程式初始化,因此造成了上述情況。


以上僅僅做為學習只用!!

參考材料:
可執行檔案(ELF)格式的理解
http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html
C程式記憶體區域分配(5個段作用)
http://www.cnblogs.com/bigbigtree/archive/2012/11/23/2784137.html


https://blog.csdn.net/love_gaohz/article/details/41310597