1. 程式人生 > >程式碼測試之記憶體越界

程式碼測試之記憶體越界

【 宣告:版權所有,歡迎轉載,請勿用於商業用途。  聯絡信箱:feixiaoxing @163.com】

    記憶體越界是我們軟體開發中經常遇到的一個問題。不經意間的複製常常導致很嚴重的後果。經常使用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);
}
    pMem[-1] = 100是堆左溢位, pMem[100]是堆右溢位。

    (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)在函式的開頭和結尾位置新增巨集處理。函式執行返回前進行節點清除。

(全文完)