由C語言代碼(文本檔案)形成可執行程式(二進制檔案),需要經過編譯-彙編-連結三個階段。編譯過程把C語言文本檔案生成彙程式設計式,彙編過程把彙程式設計式形成二進制機器代碼,連結過程則将各個源檔案生成的二進制機器代碼檔案組合成一個檔案。
C語言編寫的程式經過編譯-連接配接後,将形成一個統一格式的二進制可執行檔案,這個格式是一個依照可執行檔案格式的,可以被系統識别,并且加載到記憶體中執行的,它由幾個部分組成。在程式運作時又會産生其他幾個部分,各個部分代表了不同的存儲區域:
1、靜态區域(全局區域)
全局變量和靜态變量的存儲是放在一塊的,初始化的全局變量和靜态變量在一塊區域(RW data), 未初始化的全局變量和未初始化的靜态變量在相鄰的另一塊區域(BSS)。 程式結束後由系統釋放。
文本段(Text)
通常代碼段和隻讀資料段合成為文本段(Text), 包含實際要執行的代碼(機器指令)和常量。它通常是共享的,多個執行個體之間共享文本段。文本段是不可修改的。
代碼段(Code)
代碼段由程式中執行的機器代碼組成。在C語言中,程式語句進行編譯後,形成機器代碼。在執行程式的過程中,CPU的程式計數器指向代碼段的每一條機器代碼,并由處理器依次運作。
隻讀資料段(RO data,即常量區)
隻讀資料段是程式使用的一些不會被更改的資料,使用這些資料的方式類似查表式的操作,由于這些變量不需要更改,是以隻需要放置在隻讀存儲器中即可。
通常字元串常量就是放置在這裡,程式結束後由系統釋放。
注意:這個區域的存在與否,一直是一個争議的地方,但是我們這裡認同是存在的,因為我們的程式中的确出現了與其他資料段不同的一塊區域,但是往往很多時候大家把隻讀資料段(RO data)和下面的已初始化讀寫資料段(RW data)合成為資料段data,但是其實這個是不合适的,因為在執行過程中,這兩個區域的讀寫權限是不同的,顧名思義,隻讀資料段(RO data)是隻讀的,而已初始化讀寫資料段是可讀可寫的。
已初始化讀寫資料段(RW data – Initialized Data Segment)
已初始化資料是在程式中聲明,并且具有初值的變量,這些變量需要占用存儲器的空間,在程式執行時它們需要位于可讀寫的記憶體區域内,并具有初值,以供程式運作時讀寫。
未初始化資料段(BSS --Uninitialized Data Segment)
未初始化資料是在程式中聲明,但是沒有初始化的變量,這些變量在程式運作之前不需要占用存儲器的空間。 Block Started by Symbol,BSS段的變量隻有名稱和大小卻沒有值。
2、動态區域
堆(heap)
堆記憶體隻在程式運作時出現,一般由程式員配置設定和釋放。在具有作業系統的情況下,如果程式沒有釋放,作業系統可能在程式(例如一個程序)結束後回收記憶體。注意它與資料結構中的堆是兩回事,配置設定方式倒是類似于連結清單。
棧(stack)
棧記憶體隻在程式運作時出現,在函數内部使用的變量、函數的參數以及傳回值将使用棧空間,棧空間由編譯器自動配置設定和釋放。其操作方式類似于資料結構中的棧。
代碼段(Code)、隻讀資料段(RO data)、讀寫資料段(RW Data)、未初始化資料段(BSS)屬于靜态區域。
堆和棧屬于動态區域。
文本段(Text)、隻讀資料段(RO data)和初始化讀寫資料段(RW data)在程式連結後即産生,存在與可執行檔案中 但是未初始化資料段(BSS)将在程式初始化的時候開辟,而堆和棧作為動态區域在程式運作的過程中配置設定和釋放。
一個可執行程式分為映像和運作兩種狀态。在編譯連結後形成的映像中,将隻包含文本段(text)、隻讀資料段(RO data)和讀寫資料段(RW data)。
在程式運作之前加載的過程中,将動态生成未初始化資料段(BSS)。
在程式運作時将動态生成堆(Heap)和棧(Stack)區域。
在系統中,可執行檔案(或者程式)最終隻有放置在記憶體中才能運作的,程式的幾個段,最終也會轉化為記憶體中的幾個區域。
在記憶體中,從低位址向高位址,依次是隻讀段、讀寫段、未初始化代碼段、堆區域和棧區域。隻讀區域即文本段(Text)包含了代碼段(Code)和隻讀資料段(RO data),在記憶體區域中。
對于程式運作過程中的記憶體使用,堆和棧一般是相向擴充的。堆的配置設定由程式來配置設定,但是棧是由編譯器管理的。
3、示例代碼
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
//http://tech.ccidnet.com/art/302/20070108/995995_1.html
#define SHW_VAR_ADR(ID, I) \
printf("the %20s\t is at adr:%p\n", ID, &I); //列印變量位址宏
#define SHW_POT_ADR(ID, I) \
printf("the %20s\t is at adr:%p\n", ID, I); //列印指針指向位址宏
extern void afunc(void);
/*extern etext, edata, end;*/
/**
(1)全局變量和靜态變量的存儲是放在一塊的,
初始化的全局變量和靜态變量在一塊區域(RW data),
未初始化的全局變量和未初始化的靜态變量在相鄰的另一塊區域(BSS)。
程式結束後有系統釋放
如下面(1).1 (1).2 (1).3所述
**/
// (1).1 -- 隻要是靜态變量,即使是局部的,也不存儲在棧中,而是即存儲在靜态區域中,并依據其是否初始化,分别存儲在BSS段和DATA段
static int /*bss_*/unini_glo_sta_var; // 靜态未初始化全局變量,雖然系統會預設初始化為0,但仍然存放在BSS區
static int /*data_*/ini_glo_sta_var = 10; // 靜态初始化全局變量,存放在RW DATA區
// (1).2 -- 隻要是全局變量,即存儲在靜态區域,并依據其是否初始化,分别存儲在BSS段和DATA段
int /*bss_*/unini_glo_var; // 未初始化全局資料存儲在BSS資料區
int /*data_*/ini_glo_var = 42; // 初始化全局資料存儲在RW DATA資料區
// (1).3 -- 全局常量,其本身是全局變量,即存儲在靜态區域, 同(1).2
const int unini_glo_cnt_var; // 未初始化全局常量[不安全], 自動初始化為0, 但仍然存放在BSS區
const int ini_glo_cnt_var = 10; // 初始化全局常量,存儲在常量區
// 對于常量我們需要注意的問題在于,他們并不像我們期望的那樣存儲在常量區(RO data),
// 常量區隻用于存儲初始化好的全局常量以及字元串變量本身(不是是指針)
// 局部常量作為局部量仍然存儲與棧中
// 因為常量區與代碼段是在一起的(在有些段分類結果中,是不存在常量區的,常量區和代碼段合成為代碼區)
// 而本身來說常量隻是限制了其讀寫權限,這種讀寫權限的限制可以在編譯階段由編譯器進行制定和限制,
// 這樣在嚴格的編譯器審查結果下,運作階段的代碼就不存在對常量的讀寫操作,是以就沒必要将其他局部常量也存儲在常量區
// 否則将造成代碼段的臃腫。。。
static int unini_glo_sta_cnt_var;
static int ini_glo_sta_cnt_var = 10;
int main(void)
{
char *p_alloca = NULL, *b_malloc = NULL, *nb_malloc = NULL;
// (1).4 局部靜态變量,仍然是靜态變量,同(1).1
static int unini_sta_var; // 局部未初始化靜态變量,存儲在BSS段
static int ini_sta_var = 10; // 靜态初始化局部變量,存儲在DATA段中
// 局部非靜态變量存儲在棧中
// (2).1 -- 局部變量(不管初始化沒有)存儲在棧中
int unini_var; // 局部未初始化變量,
int ini_var = 10; // 局部初始化變量
// (2).2 -- 局部常量(不管初始化沒有)存儲在棧中, 同(2).1
const int unini_cnt_var; // 未被初始化的局部常量,不安全,存儲在棧中
const int ini_cnt_var = 10; // 局部常量,存儲在棧中
// (2).3 -- 指針常量和常量指針,其本質還是局部變量或者局部常量,存儲在棧中,同(2).1 (2).2
const int *p_cnt_var = &ini_cnt_var; // 指向常量的指針
int * const cnt_p_var = &ini_var; // 指針常量
const int * const cnt_p_cnt_var = &unini_cnt_var; // 指向常量的常指針
// (3) 字元串常量,存儲在常量區
/*const */char* str_cnt = "ABCDE"; // 字元串面變量, 存儲在常量區, 即(RO data)
// 本代碼等價于const char* str1 = "ABCDE"
char str_array[] = "ABCDE"; // 字元數組, 相當于初始化的局部變量,存儲在棧中
/* printf("Adr etext:%8x\t Adr edata %8x\t Adr end %8x\t\n", &etext, &edata, &end);*/
// TEXT段 -- 代碼段
printf("------------------------------------------------------\n");
printf(".Text Location:\n");
SHW_VAR_ADR("main", main); //檢視代碼段main函數位置
SHW_VAR_ADR("afunc", afunc); //檢視代碼段afunc函數位置
printf("------------------------------------------------------\n\n");
// BSS段 -- 未初始化全局變量區
printf("------------------------------------------------------\n");
printf(".Bss Location:\n");
SHW_VAR_ADR("unini_glo_sta_var", unini_glo_sta_var); // 全局未初始化靜态變量, 在BSS段
SHW_VAR_ADR("unini_sta_var", unini_sta_var); // 未初始化靜态變量,在BSS段
SHW_VAR_ADR("unini_glo_cnt_var", unini_glo_cnt_var); // 全局未初始化常量,在BSS段
SHW_VAR_ADR("unini_glo_var", unini_glo_var); // 全局未初始化變量在, BSS段
SHW_VAR_ADR("unini_glo_sta_cnt_var", unini_glo_sta_cnt_var); // 全局未初始化靜态常量,在BSS段
printf("------------------------------------------------------\n\n");
// RW DATA段 -- 可讀寫已初始化資料段
printf("------------------------------------------------------\n");
printf(".Data Location:\n");
SHW_VAR_ADR("ini_glo_sta_var", ini_glo_sta_var); // 全局初始化靜态變量存儲在RW data區域
SHW_VAR_ADR("ini_glo_var", ini_glo_var); // 全局初始化變量存儲在RW data
SHW_VAR_ADR("ini_sta_var", ini_sta_var); // 局部初始化靜态變量存儲在RW data區域
SHW_VAR_ADR("ini_glo_sta_cnt_var", ini_glo_sta_cnt_var); // 全局靜态已初始化常量,存儲在RW data區域
printf("------------------------------------------------------\n\n");
// RO data -- 隻讀資料段
printf("------------------------------------------------------\n\n");
printf("RW data");
SHW_VAR_ADR("ini_glo_cnt_var", ini_glo_cnt_var); // 初始化全局常量,同字元串面變量一樣,位于文本區,即常量區
SHW_POT_ADR("str_cnt", str_cnt); // 字元串面變量儲存在常量區,即文本區
SHW_VAR_ADR("str_cnt", str_cnt); // 指針str1本身在棧中
printf("------------------------------------------------------\n\n");
// STACK -- 棧
printf("------------------------------------------------------\n");
printf("Stack Locations:\n");
afunc(); // 遞歸調用5此afunc函數
p_alloca = (char *)alloca(32); // 從棧中配置設定空間, 用完立即釋放
if(p_alloca != NULL)
{
SHW_VAR_ADR("start", p_alloca);
SHW_VAR_ADR("end",p_alloca + 31);
}
// 局部變量(不管初始化沒有)存儲在棧中
SHW_VAR_ADR("unini_var", unini_var);
SHW_VAR_ADR("ini_var", ini_var);
// 局部常量(不管初始化沒有)存儲在棧中
SHW_VAR_ADR("unini_cnt_var", unini_cnt_var); // 未被初始化的局部常量,不安全,存儲在棧中
SHW_VAR_ADR("ini_cnt_var", ini_cnt_var); // 局部常量,存儲在棧中
// 指針常量和常量指針,其本質還是局部變量或者常量,存儲在棧中
SHW_VAR_ADR("p_cnt_var", p_cnt_var); // 該指向常量的指針,其本身其實是一個(初始化的)局部變量[同ini_var], 存儲在棧中
SHW_VAR_ADR("cnt_p_var", cnt_p_var); // 該指針常量,其本身其實是一個初始化的局部常量[同ini_cnt_var], 存儲在棧中
SHW_VAR_ADR("cnt_p_cnt_var", cnt_p_cnt_var); // 該指向常量的指針常量作為一個初始化的局部常量,存儲在棧中
SHW_POT_ADR("str_array", str_array); // 字元串數組,相當于初始化的局部變量,儲存在棧中
SHW_VAR_ADR("str_array", str_array); // 指針str2本身在棧中,其位址本身,就是字元串數組的位址
printf("------------------------------------------------------\n\n");
printf("------------------------------------------------------\n");
printf("Heap Locations:\n");
b_malloc = (char *)malloc(32 * sizeof(char)); //從堆中配置設定空間
nb_malloc = (char *)malloc(16 * sizeof(char)); //從堆中配置設定空間
printf("the Heap start: %p\n", b_malloc); //堆起始位置
printf("the Heap end:%p\n",(nb_malloc + 16 * sizeof(char)));//堆結束位置
// 指針指向的區域在堆中,但是指針本身在棧中
printf("\nb and nb in Stack\n");
SHW_VAR_ADR("b_malloc", b_malloc); //顯示棧中資料b的位置
SHW_VAR_ADR("b_malloc", nb_malloc); //顯示棧中資料nb的位置
free(b_malloc); //釋放申請的空間,以避免記憶體洩漏
b_malloc = NULL;
free(nb_malloc); //釋放申請的空間,以避免記憶體洩漏
nb_malloc = NULL;
printf("------------------------------------------------------\n\n");
return EXIT_SUCCESS;
}
void afunc(void)
{
static int long level=0; // 靜态資料存儲在資料段中
int stack_var; // 局部變量,存儲在棧區
if(++level==5) // 此函數遞歸調用5次
{
return;
}
printf("stack_var is at:%p\n",&stack_var);
// SHW_VAR_ADR("stack_var in stack section",stack_var);
// SHW_VAR_ADR("Level in data section",level);
afunc();
}
運作結果如下:
------------------------------------------------------
.Text Location:
the main is at adr:0x401152
the afunc is at adr:0x4015f4
------------------------------------------------------
------------------------------------------------------
.Bss Location:
the unini_glo_sta_var is at adr:0x404060
the unini_sta_var is at adr:0x404068
the unini_glo_cnt_var is at adr:0x404078
the unini_glo_var is at adr:0x40407c
the unini_glo_sta_cnt_var is at adr:0x404064
------------------------------------------------------
------------------------------------------------------
.Data Location:
the ini_glo_sta_var is at adr:0x404048
the ini_glo_var is at adr:0x40404c
the ini_sta_var is at adr:0x404054
the ini_glo_sta_cnt_var is at adr:0x404050
------------------------------------------------------
------------------------------------------------------
RW datathe ini_glo_cnt_var is at adr:0x402008
the str_cnt is at adr:0x40200c
the str_cnt is at adr:0x7fffbbee86b8
------------------------------------------------------
------------------------------------------------------
Stack Locations:
stack_var is at:0x7fffbbee869c
stack_var is at:0x7fffbbee867c
stack_var is at:0x7fffbbee865c
stack_var is at:0x7fffbbee863c
the start is at adr:0x7fffbbee86f8
the end is at adr:0x7fffbbee87f0
the unini_var is at adr:0x7fffbbee86e4
the ini_var is at adr:0x7fffbbee86e0
the unini_cnt_var is at adr:0x7fffbbee86dc
the ini_cnt_var is at adr:0x7fffbbee86d8
the p_cnt_var is at adr:0x7fffbbee86d0
the cnt_p_var is at adr:0x7fffbbee86c8
the cnt_p_cnt_var is at adr:0x7fffbbee86c0
the str_array is at adr:0x7fffbbee86b2
the str_array is at adr:0x7fffbbee86b2
------------------------------------------------------
------------------------------------------------------
Heap Locations:
the Heap start: 0x14a9270
the Heap end:0x14a92b0
b and nb in Stack
the b_malloc is at adr:0x7fffbbee86f0
the b_malloc is at adr:0x7fffbbee86e8
------------------------------------------------------
上面我們發現了一個意想不到的問題,就是在于常量的存儲,常量并沒有按照我們的要求全部存儲在常量區,相反,僅僅是全局未初始化變量和字元串變量存儲在了常量區,其他的常量依據其他規則存儲(比如局部常量存儲在棧中),這是 因為常量與普通變量隻是讀寫權限的限制,這個限制我們可以在編譯時由編譯器指定,規矩我們的代碼,這樣通過編譯器的靜态檢查,在運作時就可以保證常量的讀寫權限。
我們可以使用如下代碼驗證:
#include <stdio.h>
#include <stdlib.h>
static const int a = 10;
static int car = 10;
int main()
{
printf("%p %p\n", &a, &car);
char str1[] = "abcd";
char *str2 = "abcd";
printf("%p %p\n", str1, &str1); // 棧區域
printf("%p %p\n", str2, &str2); // str2,即字元串"abcd"在常量區, 而指針本身的位址&str3在棧中
const char *str3 = "abcd"; // 常量指針, 指向常量的指針
printf("%p %p\n", str3, &str3); // str3,即字元串"abcd"在常量區, 而指針本身的位址&str3在棧中
char * const str4 = "abcd"; // 指針常量, 指針本身是常量
printf("%p %p\n", str4, &str4); // str4,即字元串"abcd"在常量區, 而指針本身的位址&str4在棧中
const char * const str5 = "abcd";
printf("%p %p\n", str5, &str5); // str5, 即字元串"abcd"在常量區,而指針本身的位址&str5在棧中
}
運作結果:
0x402004 0x404030
0x7fffa369011b 0x7fffa369011b
0x40200f 0x7fffa3690110
0x40200f 0x7fffa3690108
0x40200f 0x7fffa3690100
0x40200f 0x7fffa36900f8
我們會發現隻有字元串本身是存儲在常量區的,而我們的變量(數組名也是一個局部常量)或者指針都是存儲在棧中,而且我們會很容易發現他把字元串本身存放在常量區是有其他用處的,這樣所有的指針指向的字元串“abcd”在記憶體中隻需要一個備份即可。 代碼中str3, str4, str5都指向了同一個位址“abcd”這也是編譯器的一個優化。
4、資料存儲類别
讨論C/C++中的記憶體布局,不得不提的是資料的存儲類别!資料在記憶體中的位置取決于它的存儲類别。一個對象是記憶體的一個位置,解析這個對象依賴于兩個屬性:存儲類别、資料類型。
- 存儲類别決定對象在記憶體中的生命周期。(活多久)
- 資料類型決定對象值的意義,在記憶體中占多大空間。(多大)
C/C++中由(auto、 extern、 register、 static)存儲類别和對象聲明的上下文決定它的存儲類别。
4.1 自動對象(automatic objects)
auto和register将聲明的對象指定為自動存儲類别。他們的作用域是局部的,諸如一個函數内,一個代碼塊{***}内等。超過了作用域,對象會被銷毀。
在一個代碼塊中聲明一個對象,如果沒有執行auto,那麼預設是自動存儲類别。
聲明為register的對象是自動存儲類别,存儲在計算機的快速寄存器中。不可以對register對象做取值操作“&”。
4.2 靜态對象(static objects)
靜态對象可以局部的,也可以是全局的。靜态對象一直保持它的值,例如進入一個函數,函數中的靜态對象仍保持上次調用時的值。包含靜态對象的函數不是線程安全的、不可重入的,正是因為它具有“記憶”功能。
局部對象聲明為靜态之後,将改變它在記憶體中儲存的位置,由動态資料—>靜态資料,即從堆或棧變為資料段或bbs段。
全局對象聲明為靜态之後,而不會改變它在記憶體中儲存的位置,仍然是在資料段或bbs段。但是static将改變它的作用域,即該對象僅在本源檔案有效。此相反的關鍵字是extern,使用extern修飾或者什麼都不帶的全局對象的作用域是整個程式。