C程式記憶體布局
- 2021 年 10 月 21 日
- 筆記
作為電腦專業的來說,程式入門基本都是從C語言開始的,了解C程式中的記憶體布局,對我們了解整個程式運行,分析程式出錯原因,會起到事半功倍的作用 。
C程式的記憶體布局包含五個段,分別是STACK(棧段),HEAP(堆段),BSS(以符號開頭的塊),DS(數據段)和TEXT(文本段)。
每個段都有自己的讀取,寫入和可執行許可權。如果程式嘗試以不允許的方式訪問記憶體,則會發生段錯誤,也就是我們常說的coredump。
段錯誤是導致程式崩潰的常見問題。核心文件(核心轉儲文件)也與段錯誤相關聯,開發人員使用該文件來查找崩潰的根本原因(段錯誤)。
下面我們將深入這五個段,更加詳細的講解每個段在程式開發或者運行中的作用。
High Addresses ---> .----------------------.
| Environment |
|----------------------|
| | Functions and variable are declared
| STACK | on the stack.
base pointer -> | - - - - - - - - - - -|
| | |
| v |
: :
. . The stack grows down into unused space
. Empty . while the heap grows up.
. .
. . (other memory maps do occur here, such
. . as dynamic libraries, and different memory
: : allocate)
| ^ |
| | |
brk point -> | - - - - - - - - - - -| Dynamic memory is declared on the heap
| HEAP |
| |
|----------------------|
| BSS | Uninitialized data (BSS)
|----------------------|
| Data | Initialized data (DS)
|----------------------|
| Text | Binary code
Low Addresses ----> '----------------------'
棧
- 它位於較高的地址,與堆段的增長和收縮方向正好相反。
- 函數的局部變數存在於棧上
- 調用函數時,將在棧中創建一個棧幀。
- 每個函數都有一個棧幀。
- 棧幀包含函數的局部變數參數和返回值。
- 棧包含一個LIFO結構。函數變數在調用時被壓入棧,返回時將函數變數從棧彈出。
- SP(棧指針)暫存器跟蹤棧的頂部。
#include
int main(void) {
int data; // 局部變數,存儲在棧上
return 0;
}
堆
- 用於在運行時分配記憶體。
- 由記憶體管理函數(如malloc、calloc、free等)管理的堆區域,這些函數可以在內部使用brk和sbrk系統調用來調整其大小。
- 堆區域由進程中的所有共享庫和動態載入的模組共享。
- 它在堆棧的相反方向上增長和收縮。
#include
int main(void) {
char *pStr = malloc(sizeof(char)*4); //pStr指向堆地址
return 0;
}
BSS(未初始化的數據塊)
- 包含所有未初始化的全局和靜態變數。
- 此段中的所有變數都由零或者空指針初始化。
- 程式載入器在載入程式時為BSS節分配記憶體。
#include
int data1; // 未初始化的全局變數存儲在BSS段
int main(void) {
static int data2; // 未初始化的靜態變數存儲在BSS段
return 0;
}
DS(初始化的數據塊)
- 包含顯式初始化的全局變數和靜態變數。
- 此段的大小由程式源程式碼中值的大小決定,在運行時不會更改。
- 它具有讀寫許可權,因此可以在運行時更改此段的變數值。
- 該段可進一步分為初始化只讀區和初始化讀寫區。
#include
int data1 = 10 ; //初始化的全局變數存儲在DS段
int main(void) {
static int data2 = 3; //初始化的靜態變數存儲在DS段
return 0;
}
TEXT
- 該段包含已編譯程式的二進位文件。
- 該段是一個只讀段,用於防止程式被意外修改。
- 該段是可共享的,因此對於文本編輯器等頻繁執行的程式,記憶體中只需要一個副本。
深入
現在有一個簡單的程式,程式碼如下:
#include
int main(void) {
return 0;
}
我們通過如下命令進行編譯
gcc -g a.cc -o a
然後通過size命令,可以看到各個段的大小
[root@build src]# gcc a.c -o a
[root@build src]# size a
text data bss dec hex filename
1040 484 16 1540 604 a
其中前三列分別為可執行程式a的text、data以及bss段的大小,第四列為該三段大小之和,第四列為該大小的十六進位表示,最後一列是文件名。
增加一個未初始化的靜態變數
#include
int main(void) {
static int data;
return 0;
}
通過size命令
[root@build src]# size a
text data bss dec hex filename
1040 484 24 1548 60c a
從上面可以看出,bss段size變大
增加一個初始化的靜態變數
#include
int main(void) {
static int data = 10;
return 0;
}
通過size命令
[root@build src]# size a
text data bss dec hex filename
1040 488 16 1544 608 a
從上面可以看出,data段size變大
增加一個未初始化的全局變數
#include
int data;
int main(void) {
return 0;
}
通過size命令
[root@build src]# size a
text data bss dec hex filename
1040 484 24 1548 60c a
從上面可以看出,bss段size變大
數據段的只讀區域和讀寫區域
#include
char str[]= "Hello world";
int main(void) {
printf("%s\n",str);
str[0]='K';
printf("%s\n",str);
return 0;
}
輸出
Hello world
Kello world
可以看到上面的示例str是一個全局數組,因此它將進入數據段。 還可以看到能夠更改該值,因此它具有讀取和寫入許可權。
現在查看其他示例程式碼
#include
char *str= "Hello world";
int main(void) {
str[0]='K';
printf("%s\n",str);
return 0;
}
在上面的示例中,我們無法更改數組字元是因為它是文字字元串。常量字元串不僅會出現在數據部分,而且所有類型的const全局數據都將進入該部分。
數據塊只讀部分,通常除了const變數和常量字元串外,程式的文本部分(通常是.rodata段)也存在於數據塊的只讀部分,因為通常無法通過程式進行修改。