背景:我們以VS2005作為環境,安裝目錄是C:/Program Files/Microsoft Visual Studio 8 ,我們稱C:/Program Files/Microsoft Visual Studio 8/VC/ 目錄為VC目錄,稱C:/Program Files/Microsoft Visual Studio 8/VC/crt 目錄為運行時目錄,在這個目錄下面有C執行階段程式庫的源碼C:/Program Files/Microsoft Visual Studio 8/VC/crt/src。
(一下代碼基於VS2005的C執行階段程式庫,其他版本略有不同)
假設這樣的語句:char* p = (char*)malloc(4); malloc最終調用dbgheap.c中的heap_alloc_dbg.
size_t blockSize;<br />int fIgnore = FALSE;<br />_CrtMemBlockHeader * pHead;
我們將重點關注這三個變數。
if ((_pfnAllocHook) && !(*_pfnAllocHook)(_HOOK_ALLOC, NULL, nSize, nBlockUse, lRequest, (const unsigned char *)szFileName, nLine))<br />
在dbghook.c中定義_pfnAllocHook, 這是申請記憶體的一個鉤子函數指標,預設指向_CrtDefaultAllocHook。預設函數什麼也不幹,直接返回1。我們可以通過調用_CrtSetAllocHook函數設定我們自己的函數,這樣的話,在每次分配記憶體之前都會回調一次我們的函數。上面是分配介面中調用這個函數的地方。
/* cannot ignore CRT allocations */<br />if (_BLOCK_TYPE(nBlockUse) != _CRT_BLOCK &&<br /> !(_crtDbgFlag & _CRTDBG_ALLOC_MEM_DF))<br /> fIgnore = TRUE;
這句是說,如果沒有設定_crtDbgFlag的 _CRTDBG_ALLOC_MEM_DF位,則忽略調試資訊。
blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize;<br />pHead = (_CrtMemBlockHeader *)_heap_alloc_base(blockSize);
正是在_heap_alloc_base中調用了HeapAlloc申請的記憶體。申請的大小並不是我們所請求的nSize,而是nSize + sizeof(_CrtMemBlockHeader) + nNoMansLandSize。也就是說,我們每次申請一塊記憶體,都會有額外的記憶體被分配,其中_CrtMemBlockHeader是用來維護一個鏈表,鏈表的每一個節點就是一次申請的情況,nNoMansLandSize一般是四位元組的地區。
if (_pFirstBlock)<br /> _pFirstBlock->pBlockHeaderPrev = pHead;<br /> else<br /> _pLastBlock = pHead;</p><p> pHead->pBlockHeaderNext = _pFirstBlock;<br /> pHead->pBlockHeaderPrev = NULL;<br /> pHead->szFileName = (char *)szFileName;<br /> pHead->nLine = nLine;<br /> pHead->nDataSize = nSize;<br /> pHead->nBlockUse = nBlockUse;<br /> pHead->lRequest = lRequest;</p><p> /* link blocks together */<br /> _pFirstBlock = pHead;
很顯然,把新的節點插入鏈表的表頭,_pFirstBlock指向表頭。pFirstBlock全域變數在dbgheap.c中定義。
再讓我們看看這個關係圖
由於_CrtMemBlockHeader的最後,也就是DATA的頭,也有一個nNoMansLandSize大小的地區gap(從_CrtMemBlockHeader的定義可以看到)。函數最後返回的是pbData(pHead),也就是DATA的首地址,這就是我們實際可以操作的記憶體,而這塊記憶體的前後被nNoMansLandSize大小的gap包裹著。
/* fill in gap before and after real block */<br />memset((void *)pHead->gap, _bNoMansLandFill, nNoMansLandSize);<br />memset((void *)(pbData(pHead) + nSize), _bNoMansLandFill, nNoMansLandSize);
這兩個位置被初始化為_bNoMansLandFill,這是0xFD.作用是用來判斷使用者操作地址的時候,是否越界。
memset((void *)pbData(pHead), _bCleanLandFill, nSize);
而實際使用者空間DATA初始化為_bCleanLandFill,這是0xCD.(可以測試一下,申請一個空間,看看是不是全部是0xCD.)
_pFirstBlock表頭並沒有向使用者開放,要想獲得這個表頭指標從而遍曆整個表,可以這樣操作:申請一個任意大小的記憶體(通常為1),這塊記憶體被放到表頭,把獲得的地址向前位移一個_CrtMemBlockHeader,就可以得到當前表頭指標,也就可以得到第二個節點的地址,記錄下第二個節點的地址,然後釋放剛才分配的記憶體,這樣本來是第二個節點,就變成了表頭。
void* p = malloc(1);
_CrtMemBlockHeader* pHeader = (((_CrtMemBlockHeader*)p) - 1)-> pBlockHeaderNext;
delete p;