C語言之程式在記憶體中的分佈以及記憶體越界問題
C語言程式在記憶體中的分佈:
bss段:該段用來存放沒有被初始化或者初始化為0的全域性變數,以及被static修飾的未初始化的區域性變數。在程式執行的整個生命週期內都存在於記憶體中。這個段中的變數只佔用程式執行時的記憶體空間,而不佔用程式檔案的儲存空間。
舉個例子:定義一個1MB的未初始化的全域性變數 (char型別只佔一個位元組 定義1024*1024個char型別說明有1MB)
#include <stdio.h>
char bss[1024*1024];
int main()
{ return 0;}
現在來看看程式的可執行檔案大小
定義 char bss[1024*1024]={0};也是這個結果。
可以看到 bss的大小並沒有1MB 說明未初始化的全域性變數不佔程式檔案的儲存空間。
data段(資料段):存放初始化過的全域性變數和static修飾的初始化過的變數,程式執行的整個生命週期內都存在於記憶體中。這個段中的變數不僅佔用程式執行時的記憶體空間,也佔用程式檔案的儲存空間。
這裡我們也舉個例子: 定義一個1MB的初始化過的全域性變數
#include<stdio.h>
char data[1024*1024]={1};
int main()
{return 0;}
執行結果:
可以看到可執行檔案data的大小有1MB 因為陣列分配的記憶體是連續的 所以就算只初始化了一個元素 後續記憶體依然會被開闢出來
rodata段(只讀段):儲存的是一些只能讀取不能修改的數,一般是程式裡面的只讀變數(如const修飾的變數)和字串字面量。ro就是Read Only的意思。
char* p = "12345"
字串"12345"可能在只讀段,也有可能在程式碼段,看編譯器而定。
text段(程式碼段):程式的二進位制資料,這段記憶體只能讀,不能修改,這部分割槽域的大小在程式執行之前就已經確定。在程式碼段中,也有可能包含一些只讀的常數變數,例如字串常量等。程式段為程式程式碼在記憶體中的對映,一個程式可以在記憶體中有多個副本。
stack(棧區):儲存函式的區域性變數,引數以及返回值(但不包括static宣告的變數)。是一種“後進先出”(Last In First Out,LIFO)的資料結構,這意味著最後放到棧上的資料,將會是第一個從棧上移走的資料。對於哪些暫時存貯的資訊,和不需要長時間儲存的資訊來說,LIFO這種資料結構非常理想。在呼叫函式或過程後,系統通常會清除棧上儲存的區域性變數、函式呼叫資訊及其它的資訊。棧另外一個重要的特徵是,它的地址空間“向下減少”,即當棧上儲存的資料越多,棧的地址就越低。
heap(堆區):堆是用於存放程序執行中被動態分配的記憶體段,更準確的說是儲存程式的動態變數。它完全由程式來負責記憶體的管理,包括什麼時候申請,什麼時候釋放,而且對它的使用也沒有什麼大小的限制。
如何使用堆記憶體:
C語言是沒有操作記憶體的語句的,只能使用標準庫提供的函式stdlib.h 使用堆記憶體要指標配置使用。
記憶體申請函式 size表示要申請的位元組數 void*返回的是申請到的記憶體的首地址。
void *malloc(size_t size);
記憶體釋放函式 ptr是要釋放的記憶體的首地址 記憶體釋放後ptr要及時的設定為空。
void free(void *ptr);
舉個例子:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
printf("%d\n",getpid()); //得到程序id
char* p = malloc(sizeof(char)*10);
char str1[7] = "123avbd";
char* str2 = "101";
printf("&p:%p\np:%p\n",&p,p);
printf("str1:%p\nstr2:%p\n",str1,str2);
while(1){}; //死迴圈不讓程式結束
free(p);
return 0;
}
讓我們看一下結果:
然後新開一個終端 輸入 vim/proc/3149/maps 檢視記憶體分配情況
可以看到 在堆區的只有p 而&p則是在棧區
str1存放在棧區
str2存放在只讀區
注意:在堆區申請完記憶體使用完畢後一定要記得釋放,即free,否則會造成記憶體洩漏
其他的有關記憶體的操作函式:
1.申請適合陣列使用的記憶體,size指的是一次申請多少個位元組的記憶體,nmemb指的是申請多少次size,申請到的記憶體會被設定為0
void *calloc(size_t nmemb, size_t size);
2.調整記憶體的大小,可以把ptr指向的記憶體,變大或變小。如果記憶體被調小,資料不會立即刪除,會一直存在,直到被別人覆蓋;如果記憶體調大,如果後面沒有被使用,則在原來的基礎上調大,如有人使用,會重新開闢一塊記憶體,再把原來的資料複製過去。
void *realloc(void *ptr, size_t size);
記憶體操作輔助函式:
malloc函式申請到的記憶體內容是隨機的以下函式可以把記憶體清理了。
s為要清理的記憶體的首地址,n為記憶體的位元組數 (標頭檔案是strings.h)
void bzero(void *s, size_t n);
s為要設定資料的記憶體的首地址,c為要設定的值(以位元組為單位),n記憶體的位元組數 (標頭檔案是 string.h)
void *memset(void *s, int c, size_t n);
碎片問題:對於堆來講,頻繁的malloc()勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低,也就是說當如果沒有足夠大小的空間,malloc()申請記憶體可能會失敗。
虛擬記憶體:
每個程式啟動後,就有了0~4G的記憶體空間地址,但不能直接使用,因為它們是虛擬的。
相當於作業系統給的一張空頭支票,如果需要使用這些記憶體,需要讓作業系統把這些記憶體與實體記憶體聯絡起來。
記憶體對映:
把虛擬記憶體與實體記憶體對應起來的過程叫記憶體對映,此時記憶體歸malloc函式所有。
當第一次向malloc申請記憶體時,作業系統會一次給程式對映33頁記憶體(1頁=4096byte)。
記憶體分配:
把記憶體使用權從malloc手裡要過來,記憶體的釋放也只是把記憶體使用許可權交還給malloc。
在使用malloc管理記憶體的過程,會有一些記憶體用來記錄malloc的分配情況。
記憶體越界:
1、超過對映的範圍,會出現段錯誤。
2、在對映範圍內,會出現髒資料。
3、當把malloc分配情況的資訊修改了,會造成申請和釋放記憶體的錯誤。
我們看個例子:
#include<stdio.h>
#include<string.h>
int main()
{
char str[10]={};
char arr[10]="0123456789";
printf("str:%p\narr:%p\n",str,arr);
puts(str); //列印str
puts(arr); //列印arr
strcpy(str,"abcdef123456");
puts(str);
puts(arr);
printf("len=%d\n",strlen(str));
printf("size=%d\n",sizeof(str)/sizeof(str[0]));
return 0;
}
這裡定義了兩個陣列一個為空,一個初始化了,然後將字串通過strcpy(字串拷貝函式)賦給了str
此時這串字串的長度已經大於str定義的長度10了,可是編譯結果卻沒有報錯 看下編譯結果:
str甚至能夠完整的輸出 長度通過strlen(求字串長度函式)來看也變成了12 但是通過sizeof可以看到str的長度還是10
而且這裡arr的值卻變成了56 這是為什麼呢?
看下輸出的前兩行,列印的是兩個陣列的首地址 ,str的地址+10之後就是arr的首地址了,當使用strcpy函式時,發現str本身的大小不夠拷貝了,於是它就會往後找,正好後面開闢了一段記憶體可以用,於是strcpy就把arr的記憶體給佔用了,導致arr本來的資料被覆蓋 ,這就是髒資料,也是所謂的記憶體越界的形式之一。
記憶體越界訪問造成的後果非常嚴重,是程式穩定性的致命威脅之一。更麻煩的是,它造成的後果是隨機的,表現出來的症狀和時機也是隨機的,讓BUG的現象和本質看似沒有什麼聯絡,這給BUG的定位帶來極大的困難。
對於記憶體越界,比較保險的方法還是在程式設計時就小心,特別是對於外部傳入的引數要仔細檢查(特別是外來的指標)。