C程序存儲空間佈局


從歷史上講,C程序一直由下面幾部分組成:

(1) 棧
由編譯器自動分配釋放管理。局部變量及每次函數調用時返回地址、以及調用者的環境信息(例如某些機器寄存器)都存放在棧中。新被調用的函數在棧上爲其自動和臨時變量分配存儲空間。通過以這種方式使用棧,C函數可以遞歸調用。遞歸函數每次調用自身時,就使用一個新的棧幀,因此一個函數調用實例中的變量集不會影響另一個函數調用實例中的變量。
   a.局部變量
   b.函數調用時返回地址
   c.調用者的環境信息(例如某些機器寄存器)

(2) 堆
需要由程序員分配釋放管理,若程序員不釋放,程序結束時可能由OS回收。通常在堆中進行動態存儲分配。
如程序中的malloc, calloc, realloc等函數都從這裏面分配。堆是從下向上分配的。

(3) 非初始化數據段
通常將此段稱爲bss段,這一名稱來源於早期彙編程序的一個操作符,意思是“block started by symbol(由符號開始的塊)”,未初始化的全局變量和靜態變量存放在這裏。在程序開始執行之前,內核將此段初始化爲0。函數外的說明:long sum[1000] ; 使此變量存放在非初始化數據段中。
   a.未初始化的全局變量
   b.未初始化的靜態變量

(4) 初始化的數據
通常將此段稱爲數據段,它包含了程序中需賦初值的變量。初始化的全局變量和靜態變量存放在這裏。例如,C程序中任何函數之外的說明:int maxcount = 99; 使此變量以初值存放在初始化數據段中。
   a.初始化的全局變量
   b.初始化的靜態變量

(5) 正文段
CPU執行的機器指令部分。通常,正文段是可共享的,所以即使是經常環境指針環境表環境字符串執行的程序(如文本編輯程序、C編譯程序、s h e l l等)在存儲器中也只需有一個副本,另外,正文段常常是隻讀的,以防止程序由於意外事故而修改其自身的指令。

下面的內存結構顯示了這些段的典型安排:

下面給出一般的c程序存儲佈局的典型安排:

用戶空間的程序使用低2G的虛擬內存,內核空間使用高2G

高地址    ——0x7FFFFFFF———

                命令行參數和環境變量

                 ——————————

                 棧空間,向下增長

             ___________________

                堆空間,向上增長

             ———————————

                 未初始化的數據

            ———————————

                已初始化的數據

        ———————————

               正文段

低地址—0x00000000————
C程序存儲空間佈局 - wangyunlong1118 - wangyunlong1118的博客

可以注意到未初始化的數據段的內容並不放在磁盤上的程序文件中,因爲,在程序開始運行前他們都被設置爲0。需要存放在程序文件中的只有正文段和初始化數據段。
參考:unix環境高級編程第二版p152
C程序存儲空間佈局(二)—— 內存對齊
本文測試環境是Linux系統,FC7,gcc 版本 4.1.2 20070502

表示方式:
    內存地址               (H)字節內容(L)      
0x0000 0000           B3B2 B1B0
0x0000 0004           B3B2 B1B0
0x0000 0008           B3B2 B1B0
...
...
...

首先看下面的程序:
#include <stdio.h>

char g_c1;
short g_s;
char g_c2;
int g_i;

int main(void)
{
   char c1;
   short s;
   char c2;
   int i;
  
    printf("sizeof: char=%d,short=%d,int=%d\n", sizeof(char), sizeof(short), sizeof(int));
    printf(    "Global variable init value is 0: g_c1=%d, g_s=%d, g_c2=%d, g_i=%d\n",     g_c1, g_s, g_c2, g_i);
    printf("Local variable init value is random: c1=%d, s=%d, c2=%d, g_i=%d\n", c1, s, c2, i);
    printf("Global variable: g_c1=%p, g_s=%p, g_c2=%p, g_i=%p\n", &g_c1, &g_s,    &g_c2, &g_i);
    printf("Local variable: c1=%p, s=%p, c2=%p, i=%p\n", &c1, &s, &c2, &i);
    return 0;
}
運行輸出:
sizeof: char=1,short=2,int=4
Global variable init value is 0: g_c1=0, g_s=0, g_c2=0, g_i=0
Local variable init value is random: c1=80, s=12276, c2=79, g_i=1096046812
Global variable: g_c1=0x8049944, g_s=0x8049946, g_c2=0x8049948, g_i=0x804994c
Local variable: c1=0xbfe9562f, s=0xbfe9562c, c2=0xbfe9562b, i=0xbfe95624
全局變量由編譯器自動初始化爲0,局部變量初始化的值是隨機的。

下面看變量的內存分配:

全局變量(放在靜態數據存儲區,全局可見):

    內存地址                (H)字節內容(L)       

g_s 和 g_c1            BBxB 由低位字節開始    , g_c1 放置在B0字節, g_s 放置在B3B2兩個字節處(g_c1=0x8049944)
0x0804 9944            B3B2 B1B0

g_c2                    xxxB 由低位字節開始    , g_c2 放置在B0字節(g_c2=0x8049948)
0x0804 9948            B3B2 B1B0

i                        BBBB    g_i 放置在 B3B2B1B0 四個字節,一個機器字長
0x0804 994c            B3B2 B1B0

內存存放時候有字節序對齊,以32位爲準,且在內存中存放的順序與定義時的順序一致,即先定義的變量在內存中的低地址,後定義的變量在高地址。
同時,不足四字節的變量,優先放在一個機器字長(4字節)低位。


局部變量(放在棧空間,局部可見):
    內存地址                (H)字節內容(L)       

i                     BBBB    i 放置在 B3B2B1B0 四個字節,一個機器字長
0xbfe9 5624            B3B2 B1B0

c2                       Bxxx 由高位字節開始    ,   c2 放置在B3字節(c2=0xbfe9562b)
0xbfe9 5628            B3B2 B1B0

c1 和 s                BxBB 由高位字節開始    , c1 放置在 B3 高位字節,s放在 B1B0 低位字節
0xbfe9 562C            B3B2 B1B0
內存存放時候有字節序對齊(內存對齊),以32位爲準,在內存中存放的順序與定義時的順序正好相反,即先定義的變量在內存中的高地址,後定義的變量在低地址。
同時,不足四字節的變量,優先放在一個機器字長(4字節)高位。

爲什麼會有內存對齊

   以下內容節選自《Intel Architecture 32 Manual》。
   字,雙字,和四字在自然邊界上不需要在內存中對齊。(對字,雙字,和四字來說,自然邊界分別是偶數地址,可以被4整除的地址,和可以被8整除的地址。)
   無論如何,爲了提高程序的性能,數據結構(尤其是棧)應該儘可能地在自然邊界上對齊。原因在於,爲了訪問未對齊的內存,處理器需要作兩次內存訪問;然而,對齊的內存訪問僅需要一次訪問。
   一個字或雙字操作數跨越了4字節邊界,或者一個四字操作數跨越了8字節邊界,被認爲是未對齊的,從而需要兩次總線週期來訪問內存。一個字起始地址是奇數但卻沒有跨越字邊界被認爲是對齊的,能夠在一個總線週期中被訪問。
   某些操作雙四字的指令需要內存操作數在自然邊界上對齊。如果操作數沒有對齊,這些指令將會產生一個通用保護異常(#GP)。雙四字的自然邊界是能夠被 16整除的地址。其他的操作雙四字的指令允許未對齊的訪問(不會產生通用保護異常),然而,需要額外的內存總線週期來訪問內存中未對齊的數據。


下面再看看結構體的內存空間:
#include <stdio.h>

struct foo
{
   char c1;
   short s;
   char c2;
   int i;
};

struct bar
{
   char c1;
   char c2;
   short s;
   int i;
};

#pragma pack(1)
struct foo_pack
{
   char c1;
   short s;
   char c2;
   int i;
};
#pragma pack()

int main(void)
{
   struct foo a;
   struct bar b;
   struct foo_pack p;


   printf("struct foo c1=%p, s=%p, c2=%p, i=%p\n", &a.c1,&a.s,&a.c2,&a.i);
   printf("struct bar c1=%p, s=%p, c2=%p, i=%p\n", &b.c1,&b.s,&b.c2,&b.i);
   printf("struct foo_pack c1=%p, s=%p, c2=%p, i=%p\n", &p.c1,&p.s,&p.c2,&p.i);
  
   printf("sizeof foo is %d\n", sizeof(struct foo));
   printf("sizeof bar is %d\n", sizeof(struct bar));
   printf("sizeof foo_pack is %d\n", sizeof(struct foo_pack));
  
   return 0;
}
程序輸出:
struct foo c1=0xbfe40dd8, s=0xbfe40dda, c2=0xbfe40ddc, i=0xbfe40de0
struct bar c1=0xbfe40dd0, s=0xbfe40dd2, c2=0xbfe40dd1, i=0xbfe40dd4
struct foo_pack c1=0xbfe40dc8, s=0xbfe40dc9, c2=0xbfe40dcb, i=0xbfe40dcc
sizeof foo is 12
sizeof bar is 8
sizeof foo_pack is 8


   缺省情況下,c/c++編譯器默認將結構、棧中的成員數據進行內存對齊。因此,
struct foo
{
    char c1;
    short s;
    char c2;
    int i;
};
printf("struct foo c1=%p, s=%p, c2=%p, i=%p\n", &a.c1,&a.s,&a.c2,&a.i);
的輸出爲:
struct foo c1=0xbfe675f8, s=0xbfe675fa, c2=0xbfe675fc, i=0xbfe67600
c1、s各佔兩字節,c2和i各佔4字節

編譯器將未對齊的成員向後移,將每一個都成員對齊到自然邊界上,從而也導致了整個結構的尺寸變大。儘管會犧牲一點空間(成員之間有空洞),但提高了性能。
也正是這個原因,我們不可以斷言sizeof(foo) == 8。在這個例子中,sizeof(foo) == 12。

如何避免內存對齊的影響

   那麼,能不能既達到提高性能的目的,又能節約一點空間呢?有一點小技巧可以使用。比如我們可以將上面的結構改成:

struct bar
{
   char c1;
   char c2;
   short s;
   int i;
};
   這樣一來,每個成員都對齊在其自然邊界上,從而避免了編譯器自動對齊。在這個例子中,sizeof(bar) == 8。
  
如何使用c/c++中的對齊選項
有3種辦法:

1. gcc的編譯選項“-fpack-struct”

可以去除struct結構中額外的hole,缺點是,影響應用中所有的struct,包括從其它庫中引入的struct結構

2. __attribute__ ((packed)) 聲明

struct {char a; double b;} xyz __attribute__ ((packed));

不過這種簡單格式的聲明只對C有效,在C++中,你必須對struct中的每個成員(size>1)進行這樣的聲明:

struct {

char a;

double b __attribute__ ((packed));

int c __attribute__ ((packed));

} xyz;

3. 預編譯選項 #pragma pack(n)

這裏的n是通知編譯器對此“pragma”行後出現的所有數據結構(包括stuct/union)採用n字節方式對齊(align)。如果n=1,那麼表示全緊湊,struct中不會出現任何佔位的hole。如果n是空,表示回覆到編譯器缺省的設置(一般=8)

#pragma pack(1)

//此段內所有數據結構全緊湊

#pragma pack()

比如:

#pragma pack(1)
struct foo_pack
{
   char c1;
   short s;
   char c2;
   int i;
};
#pragma pack()  
這樣sizeof(struct foo_pack)=8
發佈了14 篇原創文章 · 獲贊 6 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章