現代操作系統的內存分配以頁為單位進行管理,而頁通過段進行管理,組成了段頁式內存管理。
本文對C++程序的各段進行簡單的區分,並厘清各段在可執行程序與進程中的狀態關系。
程序大體被划分為兩部分,只讀部分和讀/寫部分,這源於歷史上ROM和RAM兩類存儲器的划分。盡管現代存儲器的發展早就突破了這種分類方式,程序內存的某些部分不應該被修改的想法卻被保留了下來。
text段
代碼段(code segment/text segment)用於存放可執行程序,也就是對應架構下程序的指令序列。代碼段包含在可執行程序中,大小是確定的;加載到進程中之后,所在內存區域通常被MMU設置為只讀,以保護代碼段不會被意外改寫(如出錯時)。當然,沒有MMU的系統,就沒有這種寫保護。代碼段也可能包含一些只讀的常量,如字符串常量等。
data段、bss段、rodata段
- 數據段(data segment)用於存放編譯時就能確定的全局數據,包括已初始化的全局變量和靜態變量。數據段包含在可執行程序中,大小是確定的;加載到進程中,所在內存區域可讀可寫。數據段屬於靜態內存分配。
- bss是英文Block Started by Symbol的縮寫(奇怪的歷史遺留),用於存放編譯階段無法確定的全局數據,包括未初始化的全局變量和靜態變量。可執行程序不含bss段,只記錄區域大小;進程為bss段開辟內存空間,並清零。從優化的角度出發,初始化為零的全局變量,也會被放進bss段從大壓縮可執行文件的大小。
- 常量區rodata段存放的是只讀數據,比如字符串常量、全局const變量。常量區在程序中大小確定;在進程中內存只讀。
注意,並不是所有的常量都會被保存到rodata段,存在一些特殊情況:
- 有些立即數會直接編碼到指令里,位於代碼段;
- 重復的字符串常量會合並,程序中只保留一份;
- 某些系統中rodata段會被多個進程共享,用於提高空間利用率。 —— amazing~
另外,有的嵌入式系統中,rodata不會加載到RAM中,而是直接在ROM中讀取。
由此可見,將不會修改的變量設為const是有很多好處的,不僅提高空間利用率,還能得到系統的寫保護,有利於提高系統穩定性。
rel.text段、rel.data段
分別是針對text段和data段的重定位表。
strtab段、symtab段、shstrtab段
- .strtab是字符串表(String Table);
- .shstrtab是段表字符串表(Section Header String Table),針對段表;
- .symtab是符號表,一般是變量、函數;
shstrtab及symtab會引用strtab中的字符串。
stack段、heap段
堆和棧都是進程中的概念,可執行程序中並不包括這兩個段,更不會預先指定其大小。堆和棧的大小都受到具體的機器限制,當堆和棧用盡系統內存的時候,分別會發生常見的 Stack Overflow
、Out of Memory
錯誤。
- stack在內存空間中位於用戶空間的頂部,自頂向下增長。棧底是main函數,在執行完bss段清零等初始化工作后才進入main函數開始執行。
- heap在內存空間中位於用戶空間的底部,但是在text、bss、data等各段之上,與stack段相向增長。堆的空間增長和回收由malloc/free、new/delete來操作,並可通過為系統配置不同的內存分配庫如
ptmalloc(glibc標配)
、tcmalloc(google)
、jemalloc(facebook)
來切換不同的內存分配算法,以提高內存分配效率。
總結
進程中地址從小到大包含四部分:代碼、數據、堆、棧;其中數據又由三部分構成 —— 常量、已初始全局/靜態變量、未初始全局/靜態變量。
可執行程序中確定包含的段有:text/code段、data段、rodata段,可能包含的段有:符號表和重定位表。
編譯時gcc -g
選項可以打開調試信息。重定位分為鏈接時重定位和裝載時重定位,鏈接時重定位是把各個不同.o文件的符號合並到目標地址,從而形成一個統一的可執行文件;裝載時重定位是因為可能引用外部的動態鏈接庫.so文件中的變量或函數,無法預先確定偏移量。如果沒有引用外部.so,則無需裝載時重定位。
一些驗證代碼
- 驗證全局變量位於data段還是bss段
#include <stdio.h>
int a[100]; // case 1
// int a[100] = {1}; // case 2
// int a[100] = {0}; // case 3
int main() {
printf("%d\n", a[0]);
return 0;
}
在本地對於以上三種情況分別進行編譯,並使用 ls -al
查看文件大小顯示分別為 8328、8744、8328。顯然,未初始化和初始化為零的大小一樣,都是作為bss段處理,並不占用可執行程序大小;初始化為非零值之后,除了100個int元素應占的400B空間之外,另有16B額外空間開銷(可能是用於strtab或者rel.data開銷?)。