學習了操作系統的一些基本知識之后,有必要對C程序的結構進行一下學習,事實上它并不是那么簡單,這篇文章就對C語言程序在內存中的布局進行一下小結。
總述
一個典型的可執行的C語言程序在內存中應該有以下幾部份組成:
(1)代碼段
(2)初始化數據段(數據段)
(3)未初始化數據段(BSS段)
(4)棧
(5)堆
代碼段
代碼段中存放可執行的指令,在內存中,為了保證不會因為堆棧溢出被覆蓋,將其放在了堆棧段下面(從上圖可以看出)。通常來講代碼段是共享的,這樣多次反復執行的指令只需要在內存中駐留一個副本即可,比如C編譯器,文本編輯器等。代碼段一般是只讀的,程序執行時不能隨意更改指令,也是為了進行隔離保護。
初始化數據段
初始化數據段有時就稱之為數據段。數據段是一個程序虛擬地址空間的一部分,包括一全局變量和靜態變量,這些變量在編程時就已經被初始化。數據段是可以修改的,不然程序運行時變量就無法改變了,這一點和代碼段不同。
數據段可以細分為初始化只讀區和初始化讀寫區。這一點和編程中的一些特殊變量吻合。比如全局變量int global = 1就被放在了初始化讀寫區,因為global是可以修改的。而const int flag = 2就會被放在只讀區,很明顯,flag是不能修改的。
未初始化數據段
未初始化數據段有時稱之為BSS段,BSS是英文Block Started by Symbol的簡稱,BSS段屬于靜態內存分配。存放在這里的數據都由內核初始化為0。未初始化數據段從數據段的末尾開始,存放有全部的全局變量和靜態變量并被,默認初始化為0,或者代碼中沒有顯式初始化。比如static int i; 或者全局int j;都會被放到BSS段。
棧
棧區和堆區一般相鄰,但沿著相反方向增長。當棧指針和堆指針相等就說明堆棧內存耗盡。(現代大地址空間和虛擬內存技術可以將棧和堆放在任何地方,但是二者增長方向也是相反的)。棧區存放程序的棧,一種LIFO結構,一般都在內存的高地址段。在X86架構中棧地址是向0地址增長,其他一些架構中相反。棧指針寄存器記錄棧頂地址,每次有值push進棧就會對棧指針進行修改。一個函數push進棧的一組值被稱做堆棧幀,堆棧幀保存有返回地址的最小的返回地址。
棧中存放有自動變量和每次函數調用時的信息。每次函數調用返回地址,一些調用者環境信息(比如寄存器)都被存放在棧中。然后新調用的函數就在棧中為他們的自動或者臨時變量分配內存空間,這就是C中遞歸函數調用的過程。每次遞歸函數調用自己,新的堆棧幀就被創建,這樣新的變量集合就不會被其他函數實例的變量集合影響了。
堆
堆是動態內存分配區,堆地址起始于BSS段末端,然后從這里向高地址增長。堆中內存分配管理由malloc,remalloc和free標準庫函數來完成(這些庫函數的實現原理后續會討論)。堆可以被進程的所有共享庫以及動態加載模塊共享。
例子
用size命令來看代碼的內存布局,下面看一個最簡單的程序:
#include <stdio.h>
int main(int argc, char* argv[])
{
printf("Hello world!\n");
return 0;
}
下面編譯并查看其分配內存大小
[root@node216 tmp]# gcc hello.c -o hello
[root@node216 tmp]# size hello
text data bss dec hex filename
1156 492 16 1664 680 hello
增加一個全局變量global不進行初始化:
#include <stdio.h>
int global;
int main(int argc, char* argv[])
{
printf("Hello world!\n");
return 0;
}
編譯查看分配情況:
[root@node216 tmp]# gcc hello.c -o hello
[root@node216 tmp]# size hello
text data bss dec hex filename
1156 492 24 1672 688 hello
現在對global初始化為1:
#include <stdio.h>
int global=1;
int main(int argc, char* argv[])
{
printf("Hello world!\n");
return 0;
}
再看段分配:
[root@node216 tmp]# gcc hello.c -o hello
[root@node216 tmp]# size hello
text data bss dec hex filename
1156 496 16 1668 684 hello