本文轉自徐飛翔的“C語言中的內存布局(memory layout)”
版權聲明:本文為博主原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
內存布局
根據經典的計算機馮洛伊曼模型,內存儲存著計算過程中的代碼和數據等。一般來說,內存是稱之為DRAM,其數據是掉電易失的,我們為了簡化編程過程,通常會把內存空間當作是連續的一大塊,也就是說如果給每個內存小塊進行編址的話,可以從0直接編碼到最大的內存空間上限,我們通常把這個一大塊連續的內存空間稱之為虛擬內存空間,為什么稱之為“虛擬”呢?那是因為物理硬件上的內存上不一定是連續的,其通過了一系列的映射才把可能是非連續的物理內存空間映射成了連續的虛擬內存空間,不過這個已經不在我們這篇文章的討論范疇了,我們這里知道我們編程中,我們的變量,代碼其實都是儲存在這一大塊的連續的虛擬內存空間就夠了。
當然,這么一大塊內存空間為了能夠被更好地管理,我們通常要對內存進行布局,也就是劃分功能塊,我們稱之為 內存布局(memory layout) 我們這里以c語言為例。通常我們的劃分是連續的,如Fig 1所示,通常我們把連續的虛擬內存空間,從低地址位到高地址位,劃分為五大段(segment):
- 文本段(test segment)
- 初始化后的數據段(initialized data segment)
- 未初始化的數據段(uninitialized data segment)
- 棧(stack)
- 堆(heap)
我們接下來分別介紹。
文本段
文本段又被稱之為代碼段,其中包含著程序代碼的可被執行的指令(CPU中的譯碼器將解釋這些指令,從而實現數值計算或邏輯計算等)。我們發現文本段是從最低地址位開始分配的,那是因為,如果放在堆棧的后面,如果堆棧溢出(overflow)了,那么文本段就可能會被覆蓋掉,造成不可預料的程序錯誤,因此為了避免這個問題,我們把文本段放在了最低位。
通常來說,代碼段中的代碼是可以被共享的(感覺有點像動態鏈接的意思,多個程序動態鏈接同一個庫的程序,而不嘗試去進行集成在一起,因為集成在一起將會造成多個同樣指令的多個副本,造成浪費),因此,對于同一個模塊(同一個庫),我們只需要在文本段保留一個副本就夠了。文本段通常是只讀的,從而避免程序在意外情況下改變了其中的指令。(如果真的造成了溢出,真的可能會不可預料地改變文本段的指令,這個通常是很危險的,會導致這個系統的崩潰)
初始化后的數據段
初始化后的數據段(initialized data segment),通常簡稱為數據段(data segment)。數據段中儲存的是程序中的全局變量或者是靜態變量,而這些變量是被程序員初始化過了的。注意到,數據段的數據并不意味著只是只讀的,其中的變量可能在程序運行中被改變。數據段又可以被劃分為初始化過了的只讀區(initialized read-only area)和初始化過了的讀寫區(initialized read-write area),這個由程序中的關鍵字進行修飾。舉例而言:
char s[] = "hello world";
如果這個語句在函數之外,定義了一個全局的字符數組,其儲存在了數據段的初始化過了的讀寫區。如果像是:
char *string = "hello world";
那么,這個字符實體"hello world"
將會被儲存在初始化過了的只讀區,但是其指針&string
本身儲存在了讀寫區。
未初始化的數據段
未初始化的數據段(Uninitialized data segment),也被稱之為BSS
段,其名字以一個古老的匯編操作符命名,其代表了“以符號為始的塊(Block Started by Symbol)”。在程序執行之前,在這個段的數據都會內核初始化成0。
未被初始化的這些數據從初始化過的數據段(也即是Initialized data segment)的結尾處開始,其中包含著所有的全局變量和靜態變量,注意到這些變量未曾在代碼中進行任何的顯式的初始化。例如:
static int i; // 未經過初始化的靜態變量,將會儲存在BSS中
int j; // 定義的全局變量j,其未經過初始化,也是會儲存在BSS中
棧區
棧區(stack)用于儲存自動變量,其里面是在函數每次被調用的時候,都會被保存的一些信息。每次當函數被調用的時候,一些信息,例如
- 應該在何處返回的地址
- 調用者的環境信息,比如一些寄存器信息等
將會被儲存在棧區中(保留現場信息)。這個被調用的函數則會在棧區中申請分配內存給函數里面定義的自動變量和臨時變量以供使用。這個就是為什么在C語言中迭代函數可以工作的原因了,每次迭代函數都調用了其自身的時候,其會使用一個新的棧區內存,因此不同棧區內存之間的內容不會相互干擾,即便他們從源代碼上看起來的確是同一個函數,但是他們的實際內存上的內容卻得到了隔離。
棧區(stack)一般是在堆區(heap)的鄰邊,并且棧區其數據地址的增長方式和堆區是相反的,也就是說堆區的數據按照初始化的順序,可能是從低地址位到高地址位分配的, 而棧區的數據可能按照 從高地址位到低地址位的方向分配,這種策略減少了數據溢出造成的危害。當堆區的指針和棧區指針相碰時,我們容易知道,已經沒有空余的內存可以分配了。(在現代大規模的地址空間和虛擬內存技術的幫助下,棧區和堆區可能被安置在任何地方,但是他們一般還是從相反的方向進行分配)
棧區包含著程序棧(program stack),其是一個LIFO(Last In First Out)的結構,一般會被安置在內存的高地址位。在標準的x86結構計算機上,它朝著地址0(也就是地址起始點)方向增長;然而在其他的一些結構的計算機中,它朝著反方向增長。一個“棧區指針”寄存器將會一直跟蹤著棧區的頭部(top of the stack),在每次數據壓入棧區的時候,它將會自動地調整。為了一個函數而壓入棧區的一系列值,我們稱之為棧幀(stack frame),一個棧幀至少要包括了返回地址,不然將會無法返回被調用函數,導致出錯。
堆區
堆區(heap)是用于分配動態內存的段。我們用代碼malloc(), realloc(), new
等分配的內存都儲存在堆區。堆區在BSS段的結尾處開始,并且其朝著高地址位的方向增長。正如我剛才所說的,堆區通過malloc(),realloc(),free
等進行管理著內存的分配和釋放,其可能會使用brk
或者sbrk
系統調用進行調整其大小(注意到brk/sbrk
的使用和一個最小堆區并不足以滿足malloc/realloc/free
這些命令功能的完整要求,其也許還需要通過mmap
內存映射去潛在地預定一些非連續的虛擬內存區域到進程的虛擬內存空間中)。堆區是被進程中的所有共享庫和動態加載模組所共享的,比如動態鏈接庫(.dll, .so
)等。
例子
現在有c語言代碼如:
// file name memory-layout.c
#include <stdio.h>
int main(void)
{
return 0;
}
我們可以通過指令size
對其使用的各部分的內存進行報告,如下所示:
[narendra@CentOS]$ gcc memory-layout.c -o memory-layout
[narendra@CentOS]$ size memory-layout
text data bss dec hex filename
960 248 8 1216 4c0 memory-layout
我們在原來代碼的基礎上添加一個全局變量,其未曾被初始化:
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
return 0;
}
同樣地我們觀察其內存報告:
[narendra@CentOS]$ gcc memory-layout.c -o memory-layout
[narendra@CentOS]$ size memory-layout
text data bss dec hex filename
960 248 12 1220 4c4 memory-layout
我們發現BSS
區增大了4個字節,那個正是新定義的全局變量的大小。
我們再添加一個未曾初始化的靜態變量試試看:
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
static int i; /* Uninitialized static variable stored in bss */
return 0;
}
同樣觀察報告,發現BSS
區增大到了16.
[narendra@CentOS]$ gcc memory-layout.c -o memory-layout
[narendra@CentOS]$ size memory-layout
text data bss dec hex filename
960 248 16 1224 4c8 memory-layout
如果對這個靜態變量進行初始化,那么其多出來的內存將會在數據段中,而不是在BSS
段中:
#include <stdio.h>
int global; /* Uninitialized variable stored in bss*/
int main(void)
{
static int i = 100; /* Initialized static variable stored in DS*/
return 0;
}
[narendra@CentOS]$ gcc memory-layout.c -o memory-layout
[narendra@CentOS]$ size memory-layout
text data bss dec hex filename
960 252 12 1224 4c8 memory-layout
Reference
[1]. https://www.geeksforgeeks.org/memory-layout-of-c-program/
[2]. https://www.geeksforgeeks.org/common-memory-pointer-related-bug-in-c-programs/
[3]. https://www.tutorialspoint.com/compiler_design/index.htm