對於程式員而言,詳細瞭解資料記憶體布局十分必要,否則自己常常犯一些錯誤卻不知為什麼。只有做到對記憶體布局心中有數,編寫程式才會遊刃有餘。遇到問題也能想對方向。下面就C語言記憶體布局做簡要分析。
一.幾個主要的位置段
1) .bss段,之前看過書上解釋其為blocked started by symbol。不去追究具體含義,簡單而言bas段放的是未初始化的和初始化為 0 的生命週期為全域性質的變數。先通過一段執行個體代碼來探個究竟。
lc@lc-Lenovo:~/work/test/demo$ cat test.c #include <stdio.h>#include <stdlib.h>char anArray1[1024*1024];char anArray2[1024*1024] = {0};int main(int argc, char* argv[]){static char anArray3[1024*1024];static char anArray4[1024*1024] = {0};return (0);}lc@lc-Lenovo:~/work/test/demo$ gcc test.c lc@lc-Lenovo:~/work/test/demo$ ls -l a.out -rwxrwxr-x 1 lc lc 7438 8月 20 15:42 a.out
可以看到可執行檔7k
lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out | grep bss 24 .bss 00400020 0804a020 0804a020 0000101c 2**5
可以看到bss段在4M.
如果我們初始化為1而不是0看結果為:
lc@lc-Lenovo:~/work/test/demo$ cat test.c #include <stdio.h>#include <stdlib.h>char anArray1[1024*1024];char anArray2[1024*1024] = {1};int main(int argc, char* argv[]){static char anArray3[1024*1024];//static char anArray4[1024*1024] = {0};return (0);}lc@lc-Lenovo:~/work/test/demo$ vim test.c lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out | grep bss 24 .bss 00200020 0814a040 0814a040 00101040 2**5
這裡可以驗證了我們的想法,bss段只佔用運行時候記憶體空間。
現代大多數作業系統, 在載入程式時,會把所有的 bss 全域變數清零。(在一些嵌入式裝置上,我們在編寫啟動代碼的時候,需要手動執行bss段清0.)但為保證程式的可移植性,手工把這些變數初始化為 0 也是一個好習慣,這樣這些變數都有個確定的初始值。當然作為全域變數,在整個程式的運行周期內,bss 資料是一直存在的。
2).data段
data段主要放的是初始化為非0的生命週期為全域性質的變數。上面的例子上,我們把數組的初始化修改為非0值,可以看到資料從bss轉移到data段.
lc@lc-Lenovo:~/work/test/demo$ cat test.c #include <stdio.h>#include <stdlib.h>char anArray1[1024*1024];char anArray2[1024*1024] = {0};int main(int argc, char* argv[]){static char anArray3[1024*1024] = {1};static char anArray4[1024*1024] = {0};return (0);}lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out |grep bss 24 .bss 00300020 0814a040 0814a040 00101040 2**5lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out |grep .data 14 .rodata 00000008 08048480 08048480 00000480 2**2 23 .data 00100020 0804a020 0804a020 00001020 2**5
可以看到data段放置了初始化為非0 的變數。
lc@lc-Lenovo:~/work/test/demo$ ls -l a.out -rwxrwxr-x 1 lc lc 1056050 8月 20 16:04 a.out
通過觀察可執行檔大小我們可以發現data段即佔用可執行檔空間,又佔用運行時候記憶體空間。而bss段只佔用運行時候記憶體空間。
3).rodata段
rodata 的意義同樣明顯,ro 代表 read only,rodata 就是用來存放常量資料的。關於 rodata 類型的資料,要注意以下幾點:
o 常量不一定就放在 rodata 裡,有的立即數直接和指令編碼在一起,存放在程式碼片段(.text)中。
o 對於字串常量,編譯器會自動去掉重複的字串,保證一個字串在一個可執行檔(EXE/SO)中只存在一份拷貝。o rodata 是在多個進程間是共用的,這樣可以提高Runspace利用率。
o 在有的嵌入式系統中,rodata 放在 ROM(或者 norflash)裡,運行時直接讀取,無需載入到RAM 記憶體中。
o 在嵌入式 linux 系統中,也可以通過一種叫作 XIP(就地執行)的技術,也可以直接讀取,而無需載入到 RAM 記憶體中。
o 常量是不能修改的,修改常量在 linux 下會出現段錯誤。由此可見,把在運行過程中不會改變的資料設為 rodata 類型的是有好處的:在多個進程間共用,可以大大提高空間利用率,甚至不佔用 RAM 空間。同 時由於 rodata 在唯讀記憶體頁面(page)中,是受保護的,任何試圖對它的修改都會被及時發現,這可以提高程式的穩定性。字串會被編譯器自動放到 rodata 中,其它資料要放到 rodata 中,只需要加 const 關鍵字修飾就好了。
lc@lc-Lenovo:~/work/test/demo$ cat test.c #include <stdio.h>#include <stdlib.h>const char anArray1[1024*1024] = {1};char anArray2[1024*1024] = {0};int main(int argc, char* argv[]){conststatic char anArray3[1024*1024] = {1};static char anArray4[1024*1024] = {0};return (0);}lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out |grep rodata 14 .rodata 00200020 08048480 08048480 00000480 2**5lc@lc-Lenovo:~/work/test/demo$ ls -l a.out -rwxrwxr-x 1 lc lc 2104590 8月 20 16:15 a.outlc@lc-Lenovo:~/work/test/demo$ objdump -h a.out |grep bss 24 .bss 00200020 0824a020 0824a020 0020101c 2**5lc@lc-Lenovo:~/work/test/demo$ objdump -h a.out |grep data 14 .rodata 00200020 08048480 08048480 00000480 2**5 23 .data 00000008 0824a014 0824a014 00201014 2**2
4) text 段
text 段存放代碼(如函數)和部分整數常量,它與 rodata 段很相似,相同的特性我們就不重複了,主要不同在於這個段是可以執行的。
5)stack段
棧用於存放臨時變數和函數參數。棧作為一種基本資料結構,我並不感到驚訝,用來實現函數調用,這也司空見慣的作法。直到我試圖找到另外一種方式實現遞迴操作時,我才感歎於它的巧妙。要實現遞迴操作,不用棧不是不可能,只是找不出比它更優雅的方式。儘管大多數編譯器在最佳化時,會把常用的參數或者局部變數放入寄存器中。但用棧來管理函數調用時的臨時變數(局部變數和參數)是通用做法,前者只是輔助手段,且只在當前函數中使用,一旦調用下一層函數,這些值仍然要存入棧中才行。通常情況下,棧向下(低地址)增長,每向棧中 PUSH 一個元素,棧頂就向低地址擴充,每從棧中
POP 一個元素,棧頂就向高地址回退。一個有興趣的問 題:在 x86 平台上,棧頂寄存器為 ESP,那麼 ESP 的值在是 PUSH 操作之前修改呢,還是在 PUSH 操作之後修改呢?PUSH ESP 這條指令會向棧中存入什麼資料呢?據說 x86 系列 CPU 中,除了 286 外,都是先修改 ESP,再壓棧的。由於 286 沒有 CPUID 指令,有的 OS 用 這種方法檢查 286 的型號。要注意的是,存放在棧中的資料只在當前函數及下一層函數中有效,一旦函數返回了,這些資料也自動釋放了,繼續訪問這些變數會造成意想不到的錯誤。
6).堆(heap)
堆是最靈活的一種記憶體,它的生命週期完全由使用者控制。標準 C 提供幾個函數: malloc 用來分配一塊指定大小的記憶體。 realloc 用來調整/重分配一塊存在的記憶體。 free 用來釋放不再使用的記憶體。 ... 使用堆記憶體時請注意兩個問題:alloc/free 要配對使用。記憶體配置了不釋放我們稱為記憶體泄露(memory leak),記憶體泄露多了遲早會出現 Out of memory 的錯誤,再分配記憶體就會失敗。當然釋放時也只能釋放分配出來的記憶體,釋放無效的記憶體,或者重複 free 都是不行的,會造成程式
crash。分配多少用多少。分配了 100 位元組就只能用 100 位元組,不管是讀還是寫,都只能在這個範圍內,讀多了會讀到隨機的資料,寫多了會造成的隨機的破壞。這種情況我們稱為緩衝區溢位(buffer overflow),這是非常嚴重的,大部分安全問題都是由緩衝區溢位引起的。手工檢查有沒有記憶體泄露或者緩衝區溢位是很困難的,幸好有些工具可以使用,比如 linux下有 valgrind,它的使用方法很簡單,大家下去可以試用一下,以後每次寫完程式都應該用valgrind 跑一遍.
參考文獻:http://blog.csdn.net/absurd