一
使用方法
使用方法非常簡單,首先定義宏_CRTDBG_MAP_ALLOC,然后包含頭文件crtdbg.h,最后在main函數結尾調用_CrtDumpMemoryLeaks統計內存申請和釋放的情況。相關例子如下,編譯的時候需要在Debug模式:
#define _CRTDBG_MAP_ALLOC
#include 
#include 
#include 
int main()
{
    std::cout << "Hello World!";
    int* x = (int*)malloc(sizeof(int));
    *x = 7;
    printf("%d", *x);
    x = (int*)calloc(3, sizeof(int));
    x[0] = 7;
    x[1] = 77;
    x[2] = 777;
    printf("%d %d %d", x[0], x[1], x[2]);
    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); 
    _CrtDumpMemoryLeaks();
}

運行結果如下:

Detected memory leaks!
Dumping objects ->
main.cpp(16) : {163} normal block at 0x000002882AE17740, 12 bytes long.
 Data: <    M       > 07 00 00 00 4D 00 00 00 09 03 00 00 
main.cpp(10) : {162} normal block at 0x000002882AE148C0, 4 bytes long.
 Data: <    > 07 00 00 00 
Object dump complete.
// main.cpp(x) 表示在main.cpp的x行申請的內存沒有被釋放


二
CRT 檢測的原理
在安裝Visual Studio之后,Windows CRT的源碼已經被存放在C:\Program Files (x86)\Windows Kits\10\Source\,這個目錄下面有多個sdk的版本,我選擇的是19041

內存的申請

在C++編程語言中,內存申請對應的關鍵字是newmalloc,其實new最后調用的也是malloc函數,對應源代碼文件是debug_heap.cpp。在包含相關頭文件之后,malloc函數的調用棧為:malloc -> _malloc_dbg -> heap_alloc_dbg -> heap_alloc_dbg_internal。heap_alloc_dbg_internal函數分析如下:

1.獲取臨界區,保證當前只有一個線程進入。

__acrt_lock(__acrt_heap_lock);
extern "C" void __cdecl __acrt_lock(_In_ __acrt_lock_id _Lock)
{
    EnterCriticalSection(&__acrt_lock_table[_Lock]);
}

2.執行自定義malloc的回調函數。

if (_crtBreakAlloc != -1 && request_number == _crtBreakAlloc)
{
    _CrtDbgBreak();
}
if (_pfnAllocHook && !_pfnAllocHook(
    _HOOK_ALLOC,
    nullptr,
    size,
    block_use,
    request_number,
    reinterpret_cast(file_name),
    line_number))
{
    if (file_name)
        _RPTN(_CRT_WARN, "Client hook allocation failure at file %hs line %d.", file_name, line_number);
    else
        _RPT0(_CRT_WARN, "Client hook allocation failure.");
    __leave;
}

_pfnAllocHook有一個默認的回調函數,也允許程序員自己定義,回調函數原型如下:

typedef int (__cdecl * _CRT_ALLOC_HOOK)(
    int  const allocation_type,
    void*                const data,
    size_t               const size,
    int                  const block_use,
    long                 const request,
    unsigned char const* const file_name,
    int                  const line_number
);

設置回調函數的接口為_CrtSetAllocHook

3.調用Windows API分配內存,不過需要多分配一些冗余內存,記錄一些信息,用于管理malloc分配的內存。

管理的數據結構如下:

struct _CrtMemBlockHeader
{
    _CrtMemBlockHeader* _block_header_next;     // 
    _CrtMemBlockHeader* _block_header_prev;     // 雙向鏈表,訪問該雙向鏈表的全局變量為__acrt_first_block
    char const*         _file_name;             // 調用malloc的文件名
    int                 _line_number;           // 調用malloc的行數
    int                 _block_use;             // 內存類型,所有內存類型如下
    /*
     #define _FREE_BLOCK      0     // 內存釋放
     #define _NORMAL_BLOCK    1     // 內存申請
     #define _CRT_BLOCK       2     // 標注CRT庫申請的內存
     #define _IGNORE_BLOCK    3     // 此類內存不進行管理
     #define _CLIENT_BLOCK    4     // 暫時沒找到用法
     #define _MAX_BLOCKS      5     // 暫時沒找到用法
     */
    size_t              _data_size;             // malloc分配的大小
    long                _request_number;        // 記錄分配內存的序號,每次分配內存自增1
    unsigned char       _gap[no_mans_land_size]; // 標記
    // Followed by:
    // unsigned char    _data[_data_size];          // malloc返回的內存
    // unsigned char    _another_gap[no_mans_land_size];    // 標記
};

結構中成員_gap填充了no_mans_land_size(4)個0xFD,在釋放內存時檢測寫內存時是否出現溢出(上溢)。該結構后續的內容是malloc返回的內存,內存中被填充了0xCD。最后內存_another_gap也是填充了no_mans_land_size(4)個0xFD,在釋放內存時檢測寫內存時是否出現溢出(下溢)。

內存的擴容

在C++編程語言中,內存擴容的關鍵字為realloc,對應的源文件是realloc.cpp,realloc函數的調用棧為:realloc -> _realloc_dbg -> realloc_dbg_nolock。該函數的函數原型如下:
static void * __cdecl realloc_dbg_nolock(
    void*       const block,
    size_t*     const new_size,
    int         const block_use,
    char const* const file_name,
    int         const line_number,
    bool        const reallocation_is_allowed
    ) throw()

1.檢查block和new_size的情況。

if (!block)         // block為nullptr,蛻變為malloc(size)
{
    return _malloc_dbg(*new_size, block_use, file_name, line_number);
}
if (reallocation_is_allowed && *new_size == 0)  // *new_size為0,則蛻變為free(block)
{
    _free_dbg(block, block_use);
    return nullptr;
}

2.調用_pfnAllocHook回調函數,參數allocation_type為_HOOK_REALLOC。

if (_pfnAllocHook && !_pfnAllocHook(
    _HOOK_REALLOC,
    block,
    *new_size,
    block_use,
    request_number,
    reinterpret_cast(file_name),
    line_number))
{
    if (file_name)
        _RPTN(_CRT_WARN, "Client hook re-allocation failure at file %hs line %d.", file_name, line_number);
    else
        _RPT0(_CRT_WARN, "Client hook re-allocation failure.");
    return nullptr;
}

3.對block進行一系列檢查。

is_block_an_aligned_allocation(block)       // 檢查block是否被_aligned_malloc分配的,若是,則返回nullptr.
_ASSERTE(_CrtIsValidHeapPointer(block));    // 保證block內存屬于進程堆
檢查 -> 堆的類型是否是_IGNORE_BLOCK
檢查 -> block的header的_data_size是否被破壞
檢查 -> *new_size 是否過大
// Ensure the new requested size is not too large:
if (*new_size > static_cast(_HEAP_MAXREQ - no_mans_land_size - sizeof(_CrtMemBlockHeader)))
{
    errno = ENOMEM;
    return nullptr;
}

4.分配一個新的_CrtMemBlockHeader結構。

size_t const new_internal_size{sizeof(_CrtMemBlockHeader) + *new_size + no_mans_land_size};
_CrtMemBlockHeader* new_head{nullptr};
new_head = static_cast<_CrtMemBlockHeader*>(_realloc_base(old_head, new_internal_size));
// _realloc_base中調用HeapReAlloc函數重新分配

5.對新分配的內存初始化。

// If the block grew, fill the "extension" with the land fill value:
if (*new_size > new_head->_data_size)       // *new_size 新的內存大于原來的
{
    memset(new_block + new_head->_data_size, clean_land_fill, *new_size - new_head->_data_size);
}
// Fill in the gap after the client block:
memset(new_block + *new_size, no_mans_land_fill, no_mans_land_size);    // 填充向下溢出的標記

6.將新的header鏈接到雙向鏈表中。

// 刪除原來的元素
new_head->_block_header_prev->_block_header_next = new_head->_block_header_next;
new_head->_block_header_prev->_block_header_next = new_head->_block_header_next;
// 替換__acrt_first_block指向的元素
__acrt_first_block->_block_header_prev = new_head;
new_head->_block_header_next = __acrt_first_block;
new_head->_block_header_prev = nullptr;
__acrt_first_block = new_head;

內存的釋放

在C++編程語言中,內存釋放對應的關鍵字是deletefree,delete操作符最后調用到free函數,對應的源文件是debug_heap.cpp
free函數的調用棧為:free -> _free_dbg -> free_dbg_nolock,_free_dbg函數會獲取臨界區然后調用free_dbg_nolock。free_dbg_nolock函數分析過程如下:

1.釋放內存時block_use為_FREE_BLOCK,若此時block_use為_NORMAL_BLOCK,且block由_aligned_malloc分配,不進行內存釋放。

// Check to ensure that the block was not allocated by _aligned routines
if (block_use == _NORMAL_BLOCK && is_block_an_aligned_allocation(block))
{
    // We don't know (yet) where (file, linenum) block 

2.調用_pfnAllocHook,只不過allocation_type換成了_HOOK_FREE。

// Forced failure handling
if (_pfnAllocHook && !_pfnAllocHook(_HOOK_FREE, block, 0, block_use, 0, nullptr, 0))
{
    _RPT0(_CRT_WARN, "Client hook free failure.");
    return;
}

3.進行一系列檢查。

_ASSERTE(_CrtIsValidHeapPointer(block));    // 保證block是從進程堆申請的
_ASSERTE(is_block_type_valid(header->_block_use)); // 保證block的堆類型是正常的
_ASSERTE(header->_block_use == block_use || header->_block_use == _CRT_BLOCK && block_use == _NORMAL_BLOCK);
// 檢查之前的標記是否被破壞,被破壞意味著存在內存溢出
check_bytes(header->_gap, no_mans_land_fill, no_mans_land_size)
check_bytes(block_from_header(header) + header->_data_size, no_mans_land_fill, no_mans_land_size)

4.在雙向鏈表中,刪除block元素,并釋放內存。

// 刪除雙向鏈表中的元素
header->_block_header_next->_block_header_prev = header->_block_header_prev;
header->_block_header_prev->_block_header_next = header->_block_header_next;
// 調用Windows api釋放內存
extern "C" void __declspec(noinline) __cdecl _free_base(void* const block)
{
    if (block == nullptr)
    {
        return;
    }
    if (!HeapFree(select_heap(block), 0, block))
    {
        errno = __acrt_errno_from_os_error(GetLastError());
    }
}

內存統計

調用_CrtDumpMemoryLeaks進行內存統計,主要是兩個函數:_CrtMemCheckpoint(統計)和_CrtMemDumpAllObjectsSince(顯示)

◆_CrtMemCheckpoint主要統計除了_FREE_BLOCK類型之外的其他內存,結果的數據結構如下:

typedef struct _CrtMemState
{
    struct _CrtMemBlockHeader * pBlockHeader;   // __acrt_first_block
    size_t lCounts[_MAX_BLOCKS];    // 統計各類型的數據
    size_t lSizes[_MAX_BLOCKS];     // 統計各類型內存的大小
    size_t lHighWaterCount;         // 
    size_t lTotalCount;             // 所有的內存
} _CrtMemState;

◆_CrtMemDumpAllObjectsSince主要顯示_NORMAL_BLOCK、_CRT_BLOCK和_CLIENT_BLOCK類型的內存,顯示的回調函數可以自行設置,函數原型如下:

typedef void (__cdecl * _CRT_DUMP_CLIENT)(void *, size_t);


三
CRT庫檢測內存泄露的優缺點

優點

◆Windows SDK自帶的內存泄露檢測工具,使用簡單方便。

缺點

◆無法檢測使用Windows APi來分配內存的情況,如: HeapAlloc或VirtualAlloc。

◆僅使用源碼模式下的檢測,對于已編譯成功的二進制文件無能為力。

◆若程序依賴于其他的庫文件,庫文件出現的內存泄露無法被檢測。