內存越界是我們軟件開發中經常遇到的一個問題。不經意間的複製常常導致很嚴重的後果。經常使用memset、memmove、strcpy、strncpy、strcat、sprintf的朋友肯定對此印象深刻,下面就是我個人在開發中實際遇到的一個開發問題,頗具典型。
- #define MAX_SET_STR_LENGTH 50
- #define MAX_GET_STR_LENGTH 100
- int* process(char* pMem, int size)
- {
- char localMemory[MAX_SET_STR_LENGTH] = {0};
- int* pData = NULL;
- /* code process */
- memset(localMemory, 1, MAX_GET_STR_LENGTH);
- memmove(pMem, localMemory, MAX_GET_STR_LENGTH);
- return pData;
- }
這段代碼看上去沒有什麼問題。我們本意是對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的時候就可以判斷內存的開頭和結尾處有沒有指針溢出。朋友們可以試一下下面這段代碼。
- void heap_memory_leak()
- {
- char* pMem = (char*)malloc(100);
- pMem[-1] = 100;
- pMem[100] = 100;
- free(pMem);
- }
(2)堆全局數據和棧臨時數據進行處理時,我們利用memset初始化記錄全局指針或者是堆棧臨時指針
a) 首先對memset處理,添加下面一句宏語句
#define memset(param, value, size) MEMORY_SET_PROCESS(__FUNCTION__, __LINE__, param, value, size)
b) 定義內存節點結構
- typedef struct _MEMORY_NODE
- {
- char functionName[64];
- int line;
- void* pAddress;
- int size;
- struct _MEMORY_NODE* next;
- }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)
- void MEMMOVE_PROCESS(void* dst, const void* src, int size)
- {
- MEMORY_NODE* pMemNode = check_node_exist(dst);
- if(NULL == pMemNode) return;
- assert(dst >= (pMemNode->pAddress));
- assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));
- memmove(dst, src, size);
- return;
- }
e)下面就是內存節點的刪除工作。
我們知道函數是需要反覆使用堆棧的。不同時間相同的堆棧地址對應的是完全不同的指針內容,這就要求我們在函數返回的時候對內存地址進行清理,把內存節點從對應的鏈表刪除。
我們知道在函數運行後,ebp和esp之間的內存就是通常意義上臨時變量的生存空間,所以下面的一段宏就可以記錄函數的內存空間。
- #ifdef MEMORY_LEAK_TEST
- #define FUNCTION_LOCAL_SPACE_RECORD()\
- {\
- int* functionBpRecord = 0;\
- int* functionSpRecord = 0;\
- }
- #else
- #define FUNCTION_LOCAL_SPACE_RECORD()
- #endif
- #ifdef MEMORY_LEAK_TEST
- #define FUNCTION_LEAVE_PROCESS()\
- {\
- __asm { mov functionBpRecord, bp\
- mov functionSpRecord, sp}\
- FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\
- }
- #else
- #define FUNCTION_LEAVE_PROCESS()
- #endif
這兩段宏代碼,需要插在函數的起始位置和結束的位置,這樣在函數結束的時候就可以根據ebp和esp刪除堆棧空間中的所有內存,方便了堆棧的重複使用。如果是全局內存,因爲函數的變化不會導致地址的變化,所以沒有必要進行全局內存節點的處理。
內存溢出檢查流程總結:
(1)對memset進行重新設計,記錄除了malloc指針外的一切內存;
(2)對memmove, strcpy, strncpy,strcat,sprintf等全部函數進行重新設計,因爲我們需要對他們的指針運行範圍進行判斷;
(3)在函數的開頭和結尾位置添加宏處理。函數運行返回前進行節點清除。