1. 程式人生 > >【轉】C/C++ Memory Layout寫的很好

【轉】C/C++ Memory Layout寫的很好

為什麼需要知道C/C++的記憶體佈局和在哪可以可以找到想要的資料?知道記憶體佈局對除錯程式非常有幫助,可以知道程式執行時,到底做了什麼,有助於寫出乾淨的程式碼。本文的主要內容如下:

  • 原始檔轉換為可執行檔案
  • 可執行程式組成及記憶體佈局
  • 資料儲存類別
  • 一個例項
  • 總結

原始檔轉換為可執行檔案

原始檔經過以下幾步生成可執行檔案:

  • 1、預處理(preprocessor):對#include、#define、#ifdef/#endif、#ifndef/#endif等進行處理
  • 2、編譯(compiler):將原始碼編譯為彙編程式碼
  • 3、彙編(assembler):將彙編程式碼彙編為目的碼
  • 4、連結(linker):將目的碼連結為可執行檔案

編譯器和彙編器建立的目標檔案包含:二進位制程式碼(指令)、原始碼中的資料;連結器將多個目標檔案連結成一個;裝載器吧目標檔案載入到記憶體。

image

圖1 原始檔到可執行檔案的步驟

可執行程式組成及記憶體佈局

通過上面的小節,我們知道將源程式轉換為可執行程式的步驟,典型的可執行檔案分為兩部分:

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

源程式編譯後連結到一個以0地址為始地址的線性或多維虛擬地址空間。在Linux中,每個使用者程序都可以訪問4GB線性虛擬記憶體空間。而且每個程序都擁有這樣一個空間,每個指令和資料都在這個虛擬地址空間擁有確定的地址,把這個地址稱為虛擬地址(Virtual Address)。將程序中的目的碼、資料等的虛擬地址組成的虛擬空間稱為虛擬儲存器(Virtual Memory)。典型的虛擬儲存器中有類似的佈局:

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

如下圖所示:

image

圖2 程序記憶體佈局

當程序被建立時,核心為其提供一塊實體記憶體,將虛擬記憶體對映到實體記憶體,這些都是由作業系統來做的。

資料儲存類別

討論C/C++中的記憶體佈局,不得不提的是資料的儲存類別!資料在記憶體中的位置取決於它的儲存類別。一個物件是記憶體的一個位置,解析這個物件依賴於兩個屬性:儲存類別、資料型別。

  • 儲存類別決定物件在記憶體中的生命週期。
  • 資料型別決定物件值的意義,在記憶體中佔多大空間。

C/C++中由(auto、 extern、 register、 static)儲存類別和物件宣告的上下文決定它的儲存類別。

1、自動物件(automatic objects)

autoregister將宣告的物件指定為自動儲存類別。他們的作用域是區域性的,諸如一個函式內,一個程式碼塊{***}內等。操作了作用域,物件會被銷燬。

  • 在一個程式碼塊中宣告一個物件,如果沒有執行auto,那麼預設是自動儲存類別。
  • 宣告為register的物件是自動儲存類別,儲存在計算機的快速暫存器中。不可以對register物件做取值操作“&”。

2、靜態物件(static objects)

靜態物件可以區域性的,也可以是全域性的。靜態物件一直保持它的值,例如進入一個函式,函式中的靜態物件仍保持上次呼叫時的值。包含靜態物件的函式不是執行緒安全的、不可重入的,正是因為它具有“記憶”功能。

  • 區域性物件宣告為靜態之後,將改變它在記憶體中儲存的位置,由動態資料--->靜態資料,即從堆或棧變為資料段或bbs段。
  • 全域性物件宣告為靜態之後,而不會改變它在記憶體中儲存的位置,仍然是在資料段或bbs段。但是static將改變它的作用域,即該物件僅在本原始檔有效。此相反的關鍵字是extern,使用extern修飾或者什麼都不帶的全域性物件的作用域是整個程式。

一個例項

下面我們分析一段程式碼:

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int a;
05 static int b;
06 void func( void )
07 {
08 char c;
09 static int d;
10 }
11 int main( void )
12 {
13 int e;
14 int *pi = ( int *) malloc ( sizeof ( int ));
15 func ();
16 func ();
17 free (pi );
18 return (0);
19 }

程式中宣告的變數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 例子的記憶體佈局

總結

本文介紹了C/C++中由源程式到可執行檔案的步驟,和可執行程式的記憶體佈局,資料儲存類別,最後還通過一個例子來說明。可執行程式中的變數在記憶體中的佈局可以總結為如下:

  • 變數(函式外):如果未初始化,則存放在BSS段;否則存放在data段
  • 變數(函式內):如果沒有指定static修飾符,則存放在棧中;否則同上
  • 常量:存放在文字段.text
  • 函式引數:存放在棧或暫存器中

記憶體可以分為以下幾段:

  • 文字段:包含實際要執行的程式碼(機器指令)和常量。它通常是共享的,多個例項之間共享文字段。文字段是不可修改的。
  • 初始化資料段:包含程式已經初始化的全域性變數,.data。
  • 未初始化資料段:包含程式未初始化的全域性變數,.bbs。該段中的變數在執行之前初始化為0或NULL。
  • 棧:由系統管理,由高地址向低地址擴充套件。
  • 堆:動態記憶體,由使用者管理。通過malloc/alloc/realloc、new/new[]申請空間,通過free、delete/delete[]釋放所申請的空間。由低地址想高地址擴充套件。

作者:吳秦
出處:http://www.cnblogs.com/skynet/