1. 程式人生 > >C語言之程式在記憶體中的分佈以及記憶體越界問題

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的定位帶來極大的困難。

對於記憶體越界,比較保險的方法還是在程式設計時就小心,特別是對於外部傳入的引數要仔細檢查(特別是外來的指標)。