代碼測試之內存越界

 

代碼測試之內存越界

    內存越界是我們軟件開發中經常遇到的一個問題。不經意間的複製常常導致很嚴重的後果。經常使用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)在函數的開頭和結尾位置添加宏處理。函數運行返回前進行節點清除。

發佈了19 篇原創文章 · 獲贊 12 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章