作者:小康,C/C++編程博主
關(guān)鍵詞:內(nèi)存布局、棧、堆、BSS段、數(shù)據(jù)段、代碼段、rodata、mmap、內(nèi)存分區(qū)、C語言
有幾道題,很多人答不準(zhǔn):
int a = 10; 寫在函數(shù)外面和寫在函數(shù)里面,存的地方一樣嗎?char *s = "hello"; 這個字符串存在哪?為什么修改它會崩?malloc 申請的內(nèi)存和局部變量,誰管誰?int g; 和 int g = 0; 有什么區(qū)別?這些問題,教材大多一筆帶過,但它們指向同一個知識點:程序運行時的內(nèi)存布局。
搞清楚內(nèi)存分區(qū),上面這些問題全部迎刃而解,而且你會對"段錯誤"、"棧溢出"、"內(nèi)存泄漏"這些詞有完全不同的理解。
一個 C 程序跑起來,操作系統(tǒng)會給它分配一塊虛擬地址空間。這塊空間從低地址到高地址,分成幾個區(qū)域,每個區(qū)域存放不同類型的數(shù)據(jù)。

這張圖就是本文的核心,建議截圖保存。接下來我們逐區(qū)拆解。
你寫的所有函數(shù),編譯后變成機器指令,全部存在 .text 段里。
它的兩個關(guān)鍵屬性:
int add(int a, int b) {
return a + b;
}
// add 函數(shù)編譯后的機器碼存在 .text 段
// printf、malloc 等庫函數(shù)也在(動態(tài)庫的 .text 段)
同一個程序的多個進程可以共享同一份 .text 段的物理內(nèi)存——代碼只讀,共享安全,節(jié)省內(nèi)存。這也是為什么 十幾個 nginx worker 進程跑起來,代碼頁在物理內(nèi)存里只有一份。
這個段很多教材沒有單獨講,但它是很多 bug 的根源。
char *s1 = "hello"; // "hello" 存在 .rodata,s1 只是指向它的指針
char s2[] = "hello"; // "hello" 從 .rodata 復(fù)制到棧上,s2 是獨立副本
s1[0] = 'H'; // ? 段錯誤!修改了只讀內(nèi)存
s2[0] = 'H'; // ? 合法,修改的是棧上的副本
兩行代碼,一個崩一個不崩,區(qū)別就在這里:char *s 讓指針指向 .rodata,那塊內(nèi)存是只讀的;char s[] 在棧上創(chuàng)建了一份可寫的副本。
.rodata 里還有什么?
const int MAX = 100; // 全局 const 變量 → .rodata
const char msg[] = "ok"; // 全局 const 字符數(shù)組 → .rodata
只要是"程序運行期間不應(yīng)該改變的全局?jǐn)?shù)據(jù)",編譯器都會放進 .rodata,操作系統(tǒng)保護它不被寫入。
int g1 = 42; // 全局變量,有初值 → .data
static int s1 = 100; // 靜態(tài)局部變量,有初值 → .data
void func() {
static int count = 0; // 靜態(tài)局部變量,有初值 → .data
// 注意:count 只初始化一次,函數(shù)返回后仍然存在
}
.data 段的數(shù)據(jù)存在可執(zhí)行文件里——你能直接從二進制文件里找到 42 和 100 的字節(jié)值。這也是為什么有初值的全局變量會讓可執(zhí)行文件變大。
int g2; // 無初值的全局變量 → .bss
int g3 = 0; // 初值為 0 → 也在 .bss(0 值不需要存儲)
static int s2; // 無初值的靜態(tài)變量 → .bss
.bss 段有個神奇特性:它在可執(zhí)行文件里幾乎不占空間,只記錄"這里有 N 字節(jié)的數(shù)據(jù)"。程序啟動時,操作系統(tǒng)把這段內(nèi)存全部清零,再交給程序用。
這就是為什么 C 語言的全局變量和靜態(tài)變量默認(rèn)值是 0,不是隨機值——是操作系統(tǒng)清零保證的,不是編譯器的魔法。
// 如果你定義了這個全局?jǐn)?shù)組,不會讓可執(zhí)行文件增大 1MB
int big_array[250000]; // 250000 * 4 = 1MB,存在 .bss,文件里只記個大小
data 和 bss 的區(qū)別,用一張圖看最直觀:

堆是動態(tài)內(nèi)存的家。malloc、calloc、realloc、free,操作的全是這里。
堆從低地址向高地址增長,由 brk / mmap 系統(tǒng)調(diào)用向操作系統(tǒng)申請擴展。
int *p = malloc(sizeof(int) * 100); // 從堆上分配 400 字節(jié)
p[0] = 42;
free(p); // 必須手動歸還,否則內(nèi)存泄漏
p = NULL; // 好習(xí)慣:防止懸空指針
堆的三大特點:
① 生命周期你說了算:malloc 后一直有效,直到你 free 或者進程退出
② 大小幾乎不受限(受物理內(nèi)存和虛擬地址空間限制,但遠(yuǎn)比棧大)
③ 分配慢于棧:malloc 需要找空閑塊、維護分配器元數(shù)據(jù),有一定開銷
堆內(nèi)存忘記 free,就是內(nèi)存泄漏。程序運行時間越長,泄漏越多,最終 OOM(Out of Memory)被系統(tǒng)干掉。
這個段經(jīng)常被忽視,但它非常重要。
程序加載動態(tài)庫(.so)時,內(nèi)核用 mmap 把庫文件映射進這塊區(qū)域。所以你調(diào)用 printf,實際上執(zhí)行的是 mmap 段里 libc.so 的代碼。
# 用 /proc/PID/maps 看進程的內(nèi)存映射
cat /proc/$(pgrep myapp)/maps
輸出里你會看到類似:
7f8a2c000000-7f8a2c200000 r-xp libc.so.6 ← libc 代碼段(mmap)
7f8a2c400000-7f8a2c401000 rw-p libc.so.6 ← libc 數(shù)據(jù)段(mmap)
mmap 還有另一個常見用途——文件映射,讓你把文件內(nèi)容直接當(dāng)內(nèi)存讀寫,不用 read/write:
int fd = open("data.bin", O_RDWR);
void *p = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
// 直接讀寫 p,就是在讀寫文件
munmap(p, 4096);
數(shù)據(jù)庫(如 SQLite、LMDB)大量用 mmap 來讀寫數(shù)據(jù)文件,性能遠(yuǎn)比反復(fù) read/write 好。
棧是我們最熟悉又最容易踩坑的區(qū)域。局部變量、函數(shù)參數(shù)、返回地址,全在這里。
棧從高地址向低地址增長,由操作系統(tǒng)自動管理——函數(shù)調(diào)用時自動分配棧幀,函數(shù)返回時自動釋放。
void func() {
int a = 10; // 棧上,func 返回后自動消失
int arr[100]; // 棧上,400 字節(jié),func 返回后消失
int *p = &a;
// return p; // ? 返回棧上變量的地址!func 返回后 a 消失,p 成懸空指針
}
棧的三大特點:
① 快:分配只需移動 rsp 指針,幾乎無開銷
② 自動管理:函數(shù)返回自動清理,不用操心釋放
③ 有上限:默認(rèn) 8MB(Linux),超了就是棧溢出(段錯誤)
深度遞歸、在棧上分配大數(shù)組,是棧溢出最常見的兩個原因:
// 危險:在棧上分配大數(shù)組
void dangerous() {
int buf[1000000]; // 4MB,直接爆棧
}
// 正確做法:大數(shù)組放堆上
void safe() {
int *buf = malloc(sizeof(int) * 1000000);
// ...
free(buf);
}
講了這么多,來一個綜合例子,把各類變量和它們的存儲位置全部過一遍:
#include <stdlib.h>
// ① 全局變量,有初值 → .data
int global_a = 10;
// ② 全局變量,無初值 → .bss(啟動時自動清零)
int global_b;
// ③ 全局 const → .rodata(只讀)
constint MAX = 100;
// ④ 字符串字面量 → .rodata(只讀)
constchar *greeting = "hello";
void func() {
// ⑤ 局部變量 → 棧(函數(shù)返回后消失)
int local = 42;
// ⑥ 靜態(tài)局部變量,有初值 → .data(只初始化一次,一直存活)
staticint call_count = 0;
call_count++;
// ⑦ 動態(tài)分配 → 堆(需要手動 free)
int *p = malloc(sizeof(int) * 10);
free(p);
}
對號入座:
global_a | ||
global_b | ||
MAX | ||
"hello" | ||
local | ||
call_count | ||
malloc |
不用猜,直接查:
gcc -o demo demo.c
size demo
輸出類似:
text data bss dec hex filename
1234 320 1024 2578 A12 demo
text + rodata 合并顯示為 text;data 是有初值數(shù)據(jù)段;bss 是無初值數(shù)據(jù)段。
加個大的未初始化全局?jǐn)?shù)組,再看:
int big[100000]; // 添加這行,約 400KB
text data bss dec hex filename
1234 320 401408 402962 628D2 demo
bss 漲了 400KB,但 text 和 data 幾乎沒變——證明了 bss 不占文件空間,只在內(nèi)存里。
用一句話總結(jié):
程序的內(nèi)存不是一鍋粥,而是有序分區(qū)的:代碼只讀可執(zhí)行(.text),常量只讀不可寫(.rodata),有初值全局變量(.data),無初值自動清零(.bss),手動申請釋放(heap),系統(tǒng)自動管理函數(shù)調(diào)用(stack),動態(tài)庫和文件映射(mmap)。
理解了內(nèi)存布局,你就能解釋清楚:為什么字符串字面量不能修改,為什么全局變量默認(rèn)是 0,為什么局部變量不能返回地址,為什么大數(shù)組要放堆上……
這些不是零散的"注意事項",都是同一套內(nèi)存模型自然推導(dǎo)出來的結(jié)論。
覺得有收獲,點贊、推薦、轉(zhuǎn)發(fā)給朋友,讓更多人看到,感謝~ ??

END
作者:xiaokang1998