C/C++內存佈局及對齊

1.源文件轉換爲可執行文件

源文件通過如下幾步生成可執行文件:html

  • 一、預處理(preprocessor):對#include、#define、#ifdef/#endif、#ifndef/#endif等進行處理
  • 二、編譯(compiler):將源碼編譯爲彙編代碼
  • 三、彙編(assembler):將彙編代碼彙編爲目標代碼
  • 四、連接(linker):將目標代碼連接爲可執行文件

編譯器和彙編器建立的目標文件包含:二進制代碼(指令)、源碼中的數據;連接器將多個目標文件連接成一個;裝載器吧目標文件加載到內存。ios

 image

圖1 源文件到可執行文件的步驟程序員

2.可執行程序組成及內存佈局

經過上面的小節,咱們知道將源程序轉換爲可執行程序的步驟,典型的可執行文件分爲兩部分:小程序

  • 代碼段(Code),由機器指令組成,該部分是不可改的,編譯以後就再也不改變,放置在文本段(.text)。
  • 數據段(Data),它由如下幾部分組:
    • 常量(constant),一般放置在只讀read-only的文本段(.text)
    • 靜態數據(static data),初始化的放置在數據段(.data);未初始化的放置在BSS段(.bss,Block Started by Symbol,BSS段的變量只有名稱和大小卻沒有值)
    • 動態數據(dynamic data),這些數據存儲在堆(heap)或棧(stack)

源程序編譯後連接到一個以0地址爲始地址的線性或多維虛擬地址空間。並且每一個進程都擁有這樣一個空間,每一個指令和數據都在這個虛擬地址空間擁有肯定的地址,把這個地址稱爲虛擬地址(Virtual Address)。將進程中的目標代碼、數據等的虛擬地址組成的虛擬空間稱爲虛擬存儲器(Virtual Memory)。典型的虛擬存儲器中有相似的佈局:安全

  • Text Segment (.text)
  • Initialized Data Segment (.data)
  • Uninitialized Data Segment (.bss)
  • The Stack
  • The Heap

以下圖所示:app

 image

圖2 進程內存佈局less

當進程被建立時,內核爲其提供一塊物理內存,將虛擬內存映射到物理內存,這些都是由操做系統來作的。ide

3.數據存儲類別

討論C/C++中的內存佈局,不得不提的是數據的存儲類別!數據在內存中的位置取決於它的存儲類別。一個對象是內存的一個位置,解析這個對象依賴於兩個屬性:存儲類別、數據類型。函數

  • 存儲類別決定對象在內存中的生命週期。
  • 數據類型決定對象值的意義,在內存中佔多大空間。

C/C++中由(auto、 extern、 register、 static)存儲類別和對象聲明的上下文決定它的存儲類別。佈局

3.1 自動對象(automatic objects)

auto和register將聲明的對象指定爲自動存儲類別。他們的做用域是局部的,諸如一個函數內,一個代碼塊{***}內等。操做了做用域,對象會被銷燬。

  • 在一個代碼塊中聲明一個對象,若是沒有執行auto,那麼默認是自動存儲類別。
  • 聲明爲register的對象是自動存儲類別,存儲在計算機的快速寄存器中。不能夠對register對象作取值操做「&」。

3.2 靜態對象(static objects)

靜態對象能夠局部的,也能夠是全局的。靜態對象一直保持它的值,例如進入一個函數,函數中的靜態對象仍保持上次調用時的值。包含靜態對象的函數不是線程安全的、不可重入的,正是由於它具備「記憶」功能。

  • 局部對象聲明爲靜態以後,將改變它在內存中保存的位置,由動態數據--->靜態數據,即從堆或棧變爲數據段或bbs段。
  • 全局對象聲明爲靜態以後,而不會改變它在內存中保存的位置,仍然是在數據段或bbs段。可是static將改變它的做用域,即該對象僅在本源文件有效。此相反的關鍵字是extern,使用extern修飾或者什麼都不帶的全局對象的做用域是整個程序。

4.一個實例

下面咱們分析一段代碼:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 int a;
 5 static int b;
 6 
 7 void func( void )
 8 {
 9     char c;
10     static int d;
11 }
12 
13 int main( void )
14 {
15     int e;
16     int *pi = ( int *) malloc ( sizeof ( int ));
17     func ();
18     func ();
19     free (pi );
20     return (0);
21 }

程序中聲明的變量a、b、c、d、e、pi的存儲類別和生命期以下所述:

  • a是一個未初始化的全局變量,做用域爲整個程序,生命期是整個程序運行期間,在內存的bbs段
  • b是一個未初始化的靜態全局變量,做用域爲本源文件,生命期是整個程序運行期間,在內存的bbs段
  • c是一個未初始化的局部變量,做用域爲函數func體內,即僅在函數體內可見,生命期也是函數體內,在內存的棧中
  • d是一個未初始化的靜態局部變量,做用域爲函數func體內,即僅在函數體內可見,生命期是整個程序運行期間,在內存的bbs段
  • e是一個未初始化的局部變量,做用域爲函數main體內,即僅在函數體內可見,生命期是main函數內,在內存的棧中
  • pi是一個局部指針,指向堆中的一塊內存塊,該塊的大小爲sizeof(int),pi自己存儲在內存的棧中,生命期是main函數內
  • 新申請的內存塊在堆中,生命期是malloc/free之間

用圖表示以下:

 image

圖3 例子的內存佈局

綜合1~4,介紹了C/C++中由源程序到可執行文件的步驟,和可執行程序的內存佈局,數據存儲類別,最後還經過一個例子來講明。

可執行程序中的變量在內存中的佈局能夠總結爲以下:

  • 變量(函數外):若是未初始化,則存放在BSS段;不然存放在data段
  • 變量(函數內):若是沒有指定static修飾符,則存放在棧中;不然同上
  • 常量:存放在文本段.text
  • 函數參數:存放在棧或寄存器中

內存能夠分爲如下幾段:

  • 文本段:包含實際要執行的代碼(機器指令)和常量。它一般是共享的,多個實例之間共享文本段。文本段是不可修改的。
  • 初始化數據段:包含程序已經初始化的全局變量,.data。
  • 未初始化數據段:包含程序未初始化的全局變量,.bbs。該段中的變量在執行以前初始化爲0或NULL。
  • 棧:由系統管理,由高地址向低地址擴展。
  • 堆:動態內存,由用戶管理。經過malloc/alloc/realloc、new/new[]申請空間,經過free、delete/delete[]釋放所申請的空間。由低地址想高地址擴展。

1~4引自吳秦先生的博文。

做者:吳秦
出處:http://www.cnblogs.com/skynet/
本文基於署名 2.5 中國大陸許可協議發佈,歡迎轉載,演繹或用於商業目的,可是必須保留本文的署名吳秦(包含連接).

5.內存對齊

5.1 一個例子

先來看一個例子:

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class test 
 5 {
 6 private:
 7     char c = '1';    // 1byte 
 8     int i;            // 4byte
 9     short s = 2;    // 2byte
10 };
11 
12 int main()
13 {
14     cout << sizeof(test) << endl;
15     return 0;
16 }
View Code

輸出是12.

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class test 
 5 {
 6 private:
 7     int i;            // 4byte
 8     char c = '1';    // 1byte 
 9     short s = 2;    // 2byte
10 };
11 
12 int main()
13 {
14     cout << sizeof(test) << endl;
15     return 0;
16 }
View Code

輸出是8.

咱們能夠看到,類test和test2的成員變量徹底同樣,只是定義順序不同,卻形成了2個類佔用內存大小不同。這就是編譯器內存對齊的緣故。

5.2 對齊規則

一、第一個數據成員放在offset爲0的地方,之後每一個數據成員的對齊按照#pragma pack指定的數值和這個數據成員自身長度中,比較小的那個進行。

二、在數據成員完成各自對齊以後,類(結構或聯合)自己也要進行對齊,對齊將按照#pragma pack指定的數值和結構(或聯合)最大數據成員長度中,比較小的那個進行。

很明顯#pragma pack(n)做爲一個預編譯指令用來設置多少個字節對齊的。值得注意的是,n的缺省數值是按照編譯器自身設置,默認爲8。其語法以下:

Syntax Diagram

where:


1 | 2 | 4 | 8 | 16 Members of structures are aligned on the specified byte-alignment, or on their natural alignment boundary, whichever is less, and the specified value is pushed on the stack.
nopack No packing is applied, and "nopack" is pushed onto the pack stack
pop The top element on the pragma pack stack is popped.
(no argument specified) Specifying #pragma pack() has the same effect as specifying #pragma pack(pop).

5.3 例子分析

5.3.1 對於類test的內存空間

 

內存分配過程:

1)char和編譯器默認的內存缺省分割大小比較,char比較小,分配一個字節給它。

2)int和編譯器默認的內存缺省分割大小比較,int比較小,佔4字節。只能空3個字節,從新分配4個字節。

3)short和編譯器默認的內存缺省分割大小比較,short比較小,佔2個字節,分配2個字節給它。

4)對齊結束類自己也要對齊,因此最後空餘的2個字節也被test佔用。

5.3.2 對於類test2的內存空間

 

1)int和編譯器默認的內存缺省分割大小比較,int比較小,佔4字節。分配4個字節給int。

2)char和編譯器默認的內存缺省分割大小比較,char比較小,分配一個字節給它。

3)short和編譯器默認的內存缺省分割大小比較,short比較小,此時前面的char分配完畢還餘下3個字節,足夠short的2個字節存儲,因此short緊挨着。分配2個字節給short。

4)對齊結束類自己也要對齊,因此最後空餘的1個字節也被test佔用。

5.3.3 使用#pragma pack(n)

 1 #include<iostream>
 2 using namespace std;
 3 
 4 #pragma pack(1)//設定爲1字節對齊
 5 
 6 class test 
 7 {
 8 private:
 9     char c = '1';    //1byte 
10     int i;            //4byte
11     short s = 2;    //2byte
12 };
13 
14 class test2 
15 {
16 private:
17     int i;            //4byte
18     char c = '1';    //1byte 
19     short s = 2;    //2byte
20 };
21 
22 int main()
23 {
24     cout << sizeof(test) << endl;
25     cout << sizeof(test2) << endl;
26     return 0;
27 }
View Code

輸出結果:

能夠看到,當咱們把編譯器的內存分割大小設置爲1後,類中全部的成員變量都緊密的連續分佈。

5.4 內存對齊的做用

要嚴重參考一IBM的文章:Data alignment: Straighten up and fly right,PDF版本可從這裏下載獲得。

平臺緣由(移植緣由):不是全部的硬件平臺都能訪問任意地址上的任意數據的;某些硬件平臺只能在某些地址處取某些特定類型的數據,不然拋出硬件異常。

性能緣由:通過內存對齊後,CPU的內存訪問速度大大提高。具體緣由稍後解釋。

圖一:


這是普通程序員心目中的內存印象,由一個個的字節組成,而CPU並非這麼看待的。

圖二:

How Some Processors See Memory

CPU把內存當成是一塊一塊的,塊的大小能夠是2,4,8,16字節大小,所以CPU在讀取內存時是一塊一塊進行讀取的。塊大小成爲memory access granularity(粒度) 能夠把它翻譯爲「內存讀取粒度」 。

 

假設CPU要讀取一個int型4字節大小的數據到寄存器中,分兩種狀況討論:

1)數據從0字節開始

2)數據從1字節開始

 假設內存讀取粒度爲4。

圖三:

 

當該數據是從0字節開始時,很CPU只需讀取內存一次便可把這4字節的數據徹底讀取到寄存器中。

當該數據是從1字節開始時,問題變的有些複雜,此時該int型數據不是位於內存讀取邊界上,這就是一類內存未對齊的數據。

圖四:

 

此時CPU先訪問一次內存,讀取0—3字節的數據進寄存器,並再次讀取4—5字節的數據進寄存器,接着把0字節和6,7,8字節的數據剔除,最後合併1,2,3,4字節的數據進寄存器。對一個內存未對齊的數據進行了這麼多額外的操做,大大下降了CPU性能。

這還屬於樂觀狀況了,上文提到內存對齊的做用之一爲平臺的移植緣由,由於以上操做只有有部分CPU肯幹,其餘一部分CPU遇到未對齊邊界就直接罷工了。

5.5 內存對齊對結構體成員變量訪問影響

  先看下邊一小程序:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 struct MyStruct
 5 {
 6     int a;
 7     int b;
 8     int c;
 9 };
10 
11 int main()
12 {
13     struct MyStruct myStruct = {1, 2, 3};
14 
15     struct MyStruct *ptr = &myStruct;
16     cout << ptr->a << endl;
17     cout << ptr->b << endl;
18     cout << ptr->c << endl;
19 
20     int *pstr = (int *)&myStruct;
21     cout << *pstr << endl;
22     cout << *(pstr + 1) << endl;
23     cout << *(pstr + 2) << endl;
24 
25     return 0;
26 }

  上邊程序中第16~18和第21~23行輸出的結果是同樣的。但若是咱們考慮到字節填充的問題時,採用pstr那種訪問方式就不大對了。因此要採用ptr那種訪問方式。

6.參考資料 

  C/C++ Memory Layout

  Data alignment: Straighten up and fly right

  內存對齊的規則以及做用

  C++內存對齊總結

  #pragma pack

  更多關於C++內存佈局請參考:

  C++ 對象的內存佈局

相關文章
相關標籤/搜索