【轉】C/C++ Memory Layout寫的很好
為什麼需要知道C/C++的記憶體佈局和在哪可以可以找到想要的資料?知道記憶體佈局對除錯程式非常有幫助,可以知道程式執行時,到底做了什麼,有助於寫出乾淨的程式碼。本文的主要內容如下:
- 原始檔轉換為可執行檔案
- 可執行程式組成及記憶體佈局
- 資料儲存類別
- 一個例項
- 總結
原始檔轉換為可執行檔案
原始檔經過以下幾步生成可執行檔案:
- 1、預處理(preprocessor):對#include、#define、#ifdef/#endif、#ifndef/#endif等進行處理
- 2、編譯(compiler):將原始碼編譯為彙編程式碼
- 3、彙編(assembler):將彙編程式碼彙編為目的碼
- 4、連結(linker):將目的碼連結為可執行檔案
編譯器和彙編器建立的目標檔案包含:二進位制程式碼(指令)、原始碼中的資料;連結器將多個目標檔案連結成一個;裝載器吧目標檔案載入到記憶體。
圖1 原始檔到可執行檔案的步驟
可執行程式組成及記憶體佈局
通過上面的小節,我們知道將源程式轉換為可執行程式的步驟,典型的可執行檔案分為兩部分:
- 程式碼段(Code),由機器指令組成,該部分是不可改的,編譯之後就不再改變,放置在文字段(.text)。
- 資料段(Data),它由以下幾部分組:
-
- 常量(constant),通常放置在只讀read-only的文字段(.text)
- 靜態資料(static data),初始化的放置在資料段(.data
- 動態資料(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
如下圖所示:
圖2 程序記憶體佈局
當程序被建立時,核心為其提供一塊實體記憶體,將虛擬記憶體對映到實體記憶體,這些都是由作業系統來做的。
資料儲存類別
討論C/C++中的記憶體佈局,不得不提的是資料的儲存類別!資料在記憶體中的位置取決於它的儲存類別。一個物件是記憶體的一個位置,解析這個物件依賴於兩個屬性:儲存類別、資料型別。
- 儲存類別決定物件在記憶體中的生命週期。
- 資料型別決定物件值的意義,在記憶體中佔多大空間。
C/C++中由(auto、 extern、 register、 static)儲存類別和物件宣告的上下文決定它的儲存類別。
1、自動物件(automatic objects)
auto和register將宣告的物件指定為自動儲存類別。他們的作用域是區域性的,諸如一個函式內,一個程式碼塊{***}內等。操作了作用域,物件會被銷燬。
- 在一個程式碼塊中宣告一個物件,如果沒有執行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之間
用圖表示如下:
圖3 例子的記憶體佈局
總結
本文介紹了C/C++中由源程式到可執行檔案的步驟,和可執行程式的記憶體佈局,資料儲存類別,最後還通過一個例子來說明。可執行程式中的變數在記憶體中的佈局可以總結為如下:
- 變數(函式外):如果未初始化,則存放在BSS段;否則存放在data段
- 變數(函式內):如果沒有指定static修飾符,則存放在棧中;否則同上
- 常量:存放在文字段.text
- 函式引數:存放在棧或暫存器中
記憶體可以分為以下幾段:
- 文字段:包含實際要執行的程式碼(機器指令)和常量。它通常是共享的,多個例項之間共享文字段。文字段是不可修改的。
- 初始化資料段:包含程式已經初始化的全域性變數,.data。
- 未初始化資料段:包含程式未初始化的全域性變數,.bbs。該段中的變數在執行之前初始化為0或NULL。
- 棧:由系統管理,由高地址向低地址擴充套件。
- 堆:動態記憶體,由使用者管理。通過malloc/alloc/realloc、new/new[]申請空間,通過free、delete/delete[]釋放所申請的空間。由低地址想高地址擴充套件。
作者:吳秦
出處:http://www.cnblogs.com/skynet/