關於內存管理之泄露與越界

一.
node

在 咱們我的編程的過程中,內存泄露雖然不會像內存溢出那樣形成各類莫名奇妙的問題,可是它的危害也是不可忽視的。一方面,內存的泄露致使咱們的軟件在運行 過程當中佔用了愈來愈多的內存,佔有資源而又得不到及時清理,這會致使咱們程序的效率愈來愈低;另外一方面,它會影響咱們用戶的體驗,失去市場的競爭能力。編程

    常見的內存泄露是這樣的:緩存

  1. void process(int size)  
  2. {  
  3.     char* pData = (char*)malloc(size);  
  4.   
  5.     /* other code  */  
  6.       
  7.     return/* forget to free pData */  
  8. }  
    如上圖所示,咱們在函數process的處理過程當中,每一次都須要對內存進行申請,可是在函數結束的時候卻沒有進行釋放。若是這樣的一段代碼出如今業務 側,那麼後果是不可思議的。舉個例子來講,若是咱們服務器每秒鐘須要接受100個用戶的併發訪問,每一個用戶過來的數據,咱們都須要本地申請內存從新保存一 份。處理結束以後,若是內存沒有獲得很好地釋放,就會致使咱們服務器可用的物理內存愈來愈少。一旦達到某一個臨界點以後,操做系統不得不經過內外存的調度 來知足咱們申請新內存的需求,這在另外一方面來說又會下降服務器服務的質量。

    內存泄露的危害是不言而喻的,可是查找內存泄露倒是一件苦難並且複雜的工做。咱們都知道,解決bug是一件很是簡單的事情,可是尋找bug的出處倒是一 件很是吃力的事情。所以,咱們有必要在本身編寫代碼的時候,就把查找內存泄露的工做放在很重要的位置上面。那麼有沒有什麼辦法來解決這一問題呢?服務器

    我想要作到解決內存泄露,必須作到下面兩個方面:數據結構

    (1)必須記錄內存在哪一個函數申請的,具體文件的行數是多少併發

    (2)內存應該何時被釋放函數

   要完成第1個條件其實並不困難。咱們能夠用節點的方法記錄咱們申請的內存:測試

    a)設置節點的數據結構spa

  1. typedef struct _MEMORY_NODE  
  2. {  
  3.     char functionName[64];  
  4.     int line;  
  5.     void* pAddress;  
  6.     struct _MEMORY_NODE* next;  
  7.   
  8. }MEMORY_NODE;  
    其中 functionName記錄函數名稱,line記錄行數, pAddress記錄分配的地址, next記錄下一個內存節點。

    

    b)修改內存的分配函數操作系統

    對業務側的malloc進行函數修改,添加下面一句宏語句

    #define malloc(param)  MemoryMalloc(__FUNCTION__, __LINE__, param)

    在樁函數側書寫下面的代碼

  1. void* MemoryMalloc(const char* name, int line, int size)  
  2. {  
  3.     void* pData = (void*)malloc(size);  
  4.     MEMORY_NODE* pMemNode = NULL;  
  5.     if(NULL == pData) return NULL;  
  6.     memset((char*)pData, 0, size);  
  7.   
  8.     pMemNode = (MEMORY_NODE*)malloc(sizeof(MEMORY_NODE));  
  9.     if(NULL == pMemNode){  
  10.         free(pData);  
  11.         return NULL;  
  12.     }  
  13.     memset((char*)pMemNode, 0, sizeof(MEMORY_NODE));  
  14.     memmove(pMemNode->functionName, name, strlen(name));  
  15.     pMemNode->line = line;  
  16.     pMemNode->pAddress = pData;  
  17.     pMemNode->next = NULL;  
  18.     add_memory_node(pMemNode);  
  19.   
  20.     return pData;  
  21. }  

    內存的分配過程當中還涉及到了節點的添加,因此咱們還須要添加下面的代碼

  1. static MEMORY_NODE* gMemNode = NULL;  
  2.   
  3. void add_memory_node(MEMORY_NODE* pMemNode)  
  4. {  
  5.     MEMORY_NODE* pNode = gMemNode;  
  6.     if(NULL == pMemNode) return;  
  7.     if(NULL == gMemNode){  
  8.         gMemNode = pMemNode;  
  9.         return;  
  10.     }  
  11.   
  12.     while(NULL != pNode->next){  
  13.         pNode = pNode->next;  
  14.     }  
  15.     pNode->next = pMemNode;  
  16.     return;  
  17. }  
    文中gMemNode表示全部內存節點的根節點,咱們每增長一次malloc過程就會對內存節點進行記錄。在記錄過程當中,咱們還會記錄調用malloc的函數名稱和具體文件行數,這主要是爲了方便咱們在後面進行故障定位的時候更好地查找。

   完成了第一個條件以後,咱們就要對第二個條件進行完成。

   a)內存何時釋放,這取決於咱們在函數中是怎麼實現的,可是咱們在編寫測試用例的時候倒是應該知道內存釋放沒有,好比說若是測試用例所有結束了,咱們有理由相信assert(gMemNode == NULL)這應該是恆等於真的。

    b)內存釋放的時候,咱們應該作些什麼?和節點的添加同樣,咱們在內存釋放的時候須要free指定的內存,free節點,free節點的內存,下面就是在釋放的時候咱們須要進行的操做


    對業務側的free函數進行修改,添加下面一句宏代碼,

    #define free(param)      MemoryFree(param)


    在樁函數側輸入下面的代碼:

  1. void MemoryFree(void* pAddress)  
  2. {  
  3.     if(NULL == pAddress) return;  
  4.     delete_memory_node(pAddress);  
  5.     free(pAddress);  
  6. }  

    在刪除內存的時候,須要刪除節點,刪除節點的內存

  1. void delete_memory_node(void* pAddress)  
  2. {  
  3.     MEMORY_NODE* pHead = gMemNode;  
  4.     MEMORY_NODE* pMemNode = gMemNode;  
  5.     while(NULL != pMemNode){  
  6.         if(pAddress == pMemNode->pAddress)  
  7.             break;  
  8.         pMemNode = pMemNode->next;  
  9.     }  
  10.     if(NULL == pMemNode) {  
  11.         assert(1 == 0);  
  12.         return;  
  13.     }  
  14.   
  15.     while(pMemNode != pHead->next){  
  16.         pHead = pHead->next;  
  17.     }  
  18.   
  19.     if(pMemNode == gMemNode){  
  20.         gMemNode = gMemNode->next;  
  21.     }else{  
  22.         pHead->next = pMemNode->next;  
  23.     }  
  24.     free(pMemNode);  
  25.     return;  
  26. }  

    有了上面一小段代碼的幫助,咱們在編寫測試用例的時候,就能夠在函數執行後,經過判斷內存節點是否爲空的方法判斷內存是否已經釋放。若是內存沒有釋放,咱們還能經過節點的信息幫助咱們是哪裏發生了錯誤,可是這個方法還有兩個缺點:

    (1)沒有考慮緩存的狀況,好多內存分配了以後並不會在函數中立刻釋放,而是放在緩存池中等待下一次調用,這就須要咱們準確把握和判斷了。

    (2)代碼中節點刪除和添加的時候沒有考慮多進程的情形,應該考慮用一個互斥鎖或者是信號量加以保護。

二.

 內存越界是咱們軟件開發中常常遇到的一個問題。不經意間的複製經常致使很嚴重的後果。常用memset、memmove、strcpy、 strncpy、strcat、sprintf的朋友確定對此印象深入,下面就是我我的在開發中實際遇到的一個開發問題,頗具典型。

  1. #define MAX_SET_STR_LENGTH  50  
  2. #define MAX_GET_STR_LENGTH 100  
  3.   
  4. int* process(char* pMem, int size)  
  5. {  
  6.     char localMemory[MAX_SET_STR_LENGTH] = {0};  
  7.     int* pData = NULL;  
  8.   
  9.     /*  code process */  
  10.     memset(localMemory, 1, MAX_GET_STR_LENGTH);  
  11.     memmove(pMem, localMemory, MAX_GET_STR_LENGTH);  
  12.     return pData;  
  13. }  

    這段代碼看上去沒有什麼問題。咱們本意是對localMemory進行賦值,而後拷貝到pMem指向的內存中去。其實問題就出在這一句memset的大 小。根據localMemory初始化定義語句,咱們能夠看出localMemory其實最初的申明大小隻有MAX_SET_STR_LENGTH,可是 咱們賦值的時候,卻設置成了MAX_GET_STR_LENGTH。之因此會犯這樣的錯誤,主要是由於MAX_GET_STR_LENGTH和 MAX_SET_STR_LENGTH極其類似。這段代碼編譯後,產生的後果是很是嚴重的,不斷沖垮了堆棧信息,還把返回的int*設置成了非法值。

    那麼有沒有什麼好的辦法來處理這樣一個問題?咱們能夠換一個方向來看。首先咱們查看,在軟件中存在的數據類型主要有哪些?無非就是全局數據、堆數據、棧 臨時數據。搞清楚了須要控制的數據以後,咱們應該怎麼對這些數據進行監控呢,一個簡單有效的辦法就是把memset這些函數替換成咱們本身的函數,在這些 函數中咱們嚴格對指針的複製、拷貝進行判斷和監督。

    (1)事實上,通常來講malloc的數據是不須要咱們監督的,由於內存分配的時候,一般庫函數會比咱們要求的size多分配幾個字節,這樣在free的時候就能夠判斷內存的開頭和結尾處有沒有指針溢出。朋友們能夠試一下下面這段代碼。

  1. void heap_memory_leak()  
  2. {  
  3.     char* pMem = (char*)malloc(100);  
  4.     pMem[-1] = 100;  
  5.     pMem[100] = 100;  
  6.     free(pMem);  
  7. }  
    pMem[-1] = 100是堆左溢出, pMem[100]是堆右溢出。

    

    (2)堆全局數據和棧臨時數據進行處理時,咱們利用memset初始化記錄全局指針或者是堆棧臨時指針

    a) 首先對memset處理,添加下面一句宏語句

    #define memset(param, value, size)      MEMORY_SET_PROCESS(__FUNCTION__, __LINE__, param, value, size)

 

    b) 定義內存節點結構

  1. typedef struct _MEMORY_NODE  
  2. {  
  3.     char functionName[64];  
  4.     int line;  
  5.     void* pAddress;  
  6.     int size;  
  7.     struct _MEMORY_NODE* next;  
  8.   
  9. }MEMORY_NODE;  

    其中functionName記錄了函數名稱,line記錄文件行數, pAddress記錄了指針地址, size指向了pAddress指向的內存大小,next指向下一個結構節點。

 

     c)記錄內存節點屬性

    在MEMORY_SET_PROCESS處理過程當中,不只須要調用memset函數,還須要對當前內存節點進行記錄和保存。能夠經過使用單鏈表節點的方 法進行記錄。可是若是發現pAddress指向的內存是malloc時候分配過的,此時就不須要記錄了,由於堆內存指針溢出的問題lib庫已經幫咱們解決 了。

 

    d)改造原有內存指針操做函數

    好比對memmove等函數進行改造,不失去通常性,咱們就以memmove做爲範例。

    添加宏語句 #define memmove(dst, src, size)        MEMMOVE_PROCESS(dst, src, size)

  1. void MEMMOVE_PROCESS(void* dst, const void* src, int size)  
  2. {  
  3.     MEMORY_NODE* pMemNode = check_node_exist(dst);  
  4.     if(NULL == pMemNode) return;  
  5.   
  6.     assert(dst >= (pMemNode->pAddress));  
  7.     assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));  
  8.         memmove(dst, src, size);  
  9.     return;  
  10. }  

 

    e)下面就是內存節點的刪除工做。

    咱們知道函數是須要反覆使用堆棧的。不一樣時間相同的堆棧地址對應的是徹底不一樣的指針內容,這就要求咱們在函數返回的時候對內存地址進行清理,把內存節點從對應的鏈表刪除。

    咱們知道在函數運行後,ebp和esp之間的內存就是一般意義上臨時變量的生存空間,因此下面的一段宏就能夠記錄函數的內存空間。

  1. #ifdef MEMORY_LEAK_TEST  
  2. #define FUNCTION_LOCAL_SPACE_RECORD()\  
  3. {\  
  4.     int* functionBpRecord = 0;\  
  5.     int*  functionSpRecord = 0;\  
  6. }  
  7. #else  
  8. #define FUNCTION_LOCAL_SPACE_RECORD()  
  9. #endif  
  10.   
  11. #ifdef MEMORY_LEAK_TEST  
  12. #define FUNCTION_LEAVE_PROCESS()\  
  13. {\  
  14. __asm { mov functionBpRecord, bp\  
  15.     mov functionSpRecord, sp}\  
  16.     FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\  
  17. }  
  18. #else  
  19. #define FUNCTION_LEAVE_PROCESS()  
  20. #endif  

    這兩段宏代碼,須要插在函數的起始位置和結束的位置,這樣在函數結束的時候就能夠根據ebp和esp刪除堆棧空間中的全部內存,方便了堆棧的重複使用。若是是全局內存,由於函數的變化不會致使地址的變化,因此沒有必要進行全局內存節點的處理。

內存溢出檢查流程總結:

    (1)對memset進行從新設計,記錄除了malloc指針外的一切內存;

    (2)對memmove, strcpy, strncpy,strcat,sprintf等所有函數進行從新設計,由於咱們須要對他們的指針運行範圍進行判斷;

    (3)在函數的開頭和結尾位置添加宏處理。函數運行返回前進行節點清除。

相關文章
相關標籤/搜索