程式執行時的記憶體空間分佈
我們在寫程式時,既有程式的邏輯程式碼,也有在程式中定義的變數等資料,那麼當我們的程式進行時,我們的程式碼和資料究竟是存放在哪裡的呢?下面就來總結一下。
一、程式執行時的記憶體空間情況
其實在程式執行時,由於記憶體的管理方式是以頁為單位的,而且程式使用的地址都是虛擬地址,當程式要使用記憶體時,作業系統再把虛擬地址對映到真實的實體記憶體的地址上。所以在程式中,以虛擬地址來看,資料或程式碼是一塊塊地存在於記憶體中的,通常我們稱其為一個段。而且程式碼和資料是分開存放的,即不儲存於同於一個段中,而且各種資料也是分開存放在不同的段中的。
下面以一個簡單的程式來看一下在Linux下的程式執行空間情況,程式碼檔名為space.c
123456789 | #include <unistd.h> #include <stdio.h> intmain(){printf("%d\n" |
這個程式非常簡單,輸出當前程序的程序號,然後進入一個死迴圈,這個死迴圈的目的只是讓程式不退出。而在Linux下有一個目錄/proc/$(pid),這個目錄儲存了程序號為pid的程序執行時的所有資訊,其中有一個檔案maps,它記錄了程式執行過程中的記憶體空間的情況。編譯執行上面的程式碼,其執行結果如圖1所示:
從上面的圖中,我們可以看到這樣一個簡單的程式,在執行時,需要哪些庫和哪些空間。上面的圖的各列的意思,不一一詳述,只對重要的進行說明。
第一列的是一個段的起始地址和結束地址,第二列這個段的許可權,第三列段的段內相對偏移量,第六列是這個段所存放的內容所對應的檔案。從上圖可以看到我們的程式進行首先要載入系統的兩個共享庫,然後再載入我們寫的程式的程式碼。
對於第二列的許可權,r:表示可讀,w:表示可寫,x:表示可執行,p:表示受保護(即只對本程序有效,不共享),與之相對的是s,意是就是共享。
從上圖我們可以非常形象地看到一個程式進行時的記憶體分佈情況。下面我們將會結合上圖,進行更加深入的對記憶體中的資料段的解說。
二、程式執行時記憶體的各種資料段
1.bss 段
該段用來存放沒有被初始化或初始化為0的全域性變數,因為是全域性變數,所以在程式執行的整個生命週期內都存在於記憶體中。有趣的是這個段中的變數只佔用程式執行時的記憶體空間,而不佔用程式檔案的儲存空間。可以用以下程式來說明這點,檔名為bss.c
12345678 | #include <stdio.h> intbss_data[1024*1024];intmain(){return0;} |
這個程式非常簡單,定義一個4M的全域性變數,然後返回。編譯成可執行檔案bss,並檢視可執行檔案的檔案屬性如圖2所示:
從可執行檔案的大小4774B可以看出,bss資料段(4M)並不佔用程式檔案的儲存空間,在下面的data段中,我們可以看到data段的資料是佔用可執行檔案的儲存空間的。
在圖1中,有檔名且屬性為rw-p的記憶體區間,就是bss段。
2.data段
初始化過的全域性變數資料段,該段用來儲存初始化了的非0的全域性變數,如果全域性變數初始化為0,則編譯有時會出於優化的考慮,將其放在bss段中。因為也是全域性變數,所以在程式執行的整個生命週期內都存在於記憶體中。與bss段不同的是,data段中的變數既佔程式執行時的記憶體空間,也佔程式檔案的儲存空間。可以用下面的程式來說明,檔名為data.c:
12345678 | #include <stdio.h> intdata_data[1024*1024]={1};intmain(){return0;} |
這個程式與上面的bss唯一的不同就是全域性變數int型陣列data_data,其中第0個元素的值初始化為1,其他元素的值初始化成預設的0,而因為陣列的地址是連續的,所以只要有一個元素在data段中,則其他的元素也必然在data段中。編譯連線成可執行檔案data,並檢視可執行檔案的檔案屬性如圖3所示:
從可執行檔案的大小來看,data段資料(data_data陣列的大小,4M)佔用程式檔案的儲存空間。
在圖1中,有檔名且屬性為rw-p的記憶體區間,就是data段,它與bss段在記憶體中是共用一段記憶體的,不同的是,bss段資料不佔用檔案,而data段資料佔用檔案儲存空間。
3.rodata段
該段是常量資料段,用於存放常量資料,ro就是Read Only之意。但是注意並不是所有的常量都是放在常量資料段的,其特殊情況如下:
1)有些立即數與指令編譯在一起直接放在程式碼段(text段,下面會講到)中。
2)對於字串常量,編譯器會去掉重複的常量,讓程式的每個字串常量只有一份。
3)有些系統中rodata段是多個程序共享的,目的是為了提高空間的利用率。
在圖1中,有檔名的屬性為r–p的記憶體區間就是rodata段。可見他是受保護的,只能被讀取,從而提高程式的穩定性。
4.text段
text段就是程式碼段,用來存放程式的程式碼(如函式)和部分整數常量。它與rodata段的主要不同是,text段是可以執行的,而且不被不同的程序共享。
在圖1中,有檔名且屬性為r-xp的記憶體區間就是text段。就如我們所知道的那樣,程式碼段是不能被寫的。
5.stack段
該段就是棧段,用來儲存臨時變數和函式引數。程式中的函式呼叫就是以棧的方式來實現的,通常棧是向下(即向低地址)增長的,當向棧中push一個元素,棧頂指標就會向低地址移動,當從棧中pop一個元素,棧頂指標就會向高地址移動。棧中的資料只在當前函式或下一層函式中有效,當函式返回時,這些資料自動被釋放,如果繼續對這些資料進行訪問,將發生未知的錯誤。通常我們在程式中定義的不是用malloc系統函式或new出來的變數,都是存放在棧中的。例如,如下函式:
123456 | voidfunc(){inta=0;int*n_ptr=malloc(sizeof(int));char*c_ptr=newchar;} |
整型變數a,整型指標變數n_ptr和char型指標變數c_ptr,都存放在棧段中,而n_ptr和c_ptr指向的變數,由於是malloc或new出來的,所以存放在堆中。當函式func返回時,a、n_ptr、c_ptr都會被釋放,但是n_ptr和c_ptr指向的記憶體卻不會釋放。因為它們是存在於堆中的資料。
在圖1中,檔名為stack的記憶體區間即為棧段。
6.heap 段
heap(堆)是最自由的一種記憶體,它完全由程式來負責記憶體的管理,包括什麼時候申請,什麼時候釋放,而且對它的使用也沒有什麼大小的限制。在C/C++中,用alloc系統函式和new申請的記憶體都存在於heap段中。
以上面的程式為例,它向堆申請了一個int和一個char的記憶體,因為沒有呼叫free或delete,所以當函式返回時,堆中的int和char變數並沒有釋放,造成了記憶體洩漏。
由於在圖1所對應的程式碼中沒有使用alloc系統函式或new來申請記憶體,所以heap段並沒有在圖1中顯示出來,所以以下面的程式來說明heap段的位置,程式碼檔案為heap.c,程式碼如下:
123456789101112 | #include <unistd.h> #include <stdlib.h> #include <stdio.h> intmain(){int*n_ptr=malloc(sizeof(int));printf("%d\n",getpid());while(1);free(n_ptr);return0;} |
檢視其執行時記憶體空間分佈如下:
可以看到檔名為heap的記憶體區間就是heap段。從上圖,也可以看出,雖然我們只申請4個位元組(sizeof(int))的空間,但是在作業系統中,記憶體是以頁的方式進行管理的,所以在分配heap記憶體時,還是一次分配就為我們分配了一個頁的記憶體。注:無論是圖1,還是上圖,都有一些沒有檔名的記憶體區間,其實沒用檔名的記憶體區間表示使用mmap對映的匿名空間。