-
-
[原创]Windows 之 CRT的检测内存泄露
-
2023-7-15 15:42
14882
-
Windows 之 CRT的检测内存泄露
使用方法
使用方法非常简单,首先定义宏_CRTDBG_MAP_ALLOC
,然后包含头文件crtdbg.h
,最后在main
函数结尾调用_CrtDumpMemoryLeaks
统计内存申请和释放的情况。相关例子如下,编译的时候需要在Debug模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>
int main()
{
std::cout << "Hello World!\n" ;
int * x = ( int *) malloc ( sizeof ( int ));
*x = 7;
printf ( "%d\n" , *x);
x = ( int *) calloc (3, sizeof ( int ));
x[0] = 7;
x[1] = 77;
x[2] = 777;
printf ( "%d %d %d\n" , x[0], x[1], x[2]);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG);
_CrtDumpMemoryLeaks();
}
|
运行结果如下:
1 2 3 4 5 6 7 8 | 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.
|
CRT 检测的原理
在安装Visual Studio
之后,Windows CRT的源码已经被存放在C:\Program Files (x86)\Windows Kits\10\Source\
,这个目录下面有多个sdk的版本,我选择的是19041
。
内存的申请
在C++编程语言中,内存申请对应的关键字是new
或malloc
,其实new最后调用的也是malloc函数,对应源代码文件是debug_heap.cpp
。在包含相关头文件之后,malloc函数的调用栈为:malloc -> _malloc_dbg -> heap_alloc_dbg -> heap_alloc_dbg_internal。heap_alloc_dbg_internal函数分析如下:
- 获取临界区,保证当前只有一个线程进入.
1 2 3 4 5 6 | __acrt_lock(__acrt_heap_lock);
extern "C" void __cdecl __acrt_lock(_In_ __acrt_lock_id _Lock)
{
EnterCriticalSection(&__acrt_lock_table[_Lock]);
}
|
- 执行自定义malloc的回调函数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | if (_crtBreakAlloc != -1 && request_number == _crtBreakAlloc)
{
_CrtDbgBreak();
}
if (_pfnAllocHook && !_pfnAllocHook(
_HOOK_ALLOC,
nullptr,
size,
block_use,
request_number,
reinterpret_cast <unsigned char const *>(file_name),
line_number))
{
if (file_name)
_RPTN(_CRT_WARN, "Client hook allocation failure at file %hs line %d.\n" , file_name, line_number);
else
_RPT0(_CRT_WARN, "Client hook allocation failure.\n" );
__leave;
}
|
_pfnAllocHook有一个默认的回调函数,也允许程序员自己定义,回调函数原型如下:
1 2 3 4 5 6 7 8 9 | 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分配的内存。
管理的数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | struct _CrtMemBlockHeader
{
_CrtMemBlockHeader* _block_header_next;
_CrtMemBlockHeader* _block_header_prev;
char const * _file_name;
int _line_number;
int _block_use;
size_t _data_size;
long _request_number;
unsigned char _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。该函数的函数原型如下:
1 2 3 4 5 6 7 8 | 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 ()
|
- 检查block和new_size的情况.
1 2 3 4 5 6 7 8 9 | if (!block)
{
return _malloc_dbg(*new_size, block_use, file_name, line_number);
}
if (reallocation_is_allowed && *new_size == 0)
{
_free_dbg(block, block_use);
return nullptr;
}
|
- 调用_pfnAllocHook回调函数,参数allocation_type为_HOOK_REALLOC。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | if (_pfnAllocHook && !_pfnAllocHook(
_HOOK_REALLOC,
block,
*new_size,
block_use,
request_number,
reinterpret_cast <unsigned char const *>(file_name),
line_number))
{
if (file_name)
_RPTN(_CRT_WARN, "Client hook re-allocation failure at file %hs line %d.\n" , file_name, line_number);
else
_RPT0(_CRT_WARN, "Client hook re-allocation failure.\n" );
return nullptr;
}
|
- 对block进行一系列检查
1 2 3 4 5 6 7 8 9 10 11 | is_block_an_aligned_allocation(block)
_ASSERTE(_CrtIsValidHeapPointer(block));
检查 -> 堆的类型是否是_IGNORE_BLOCK
检查 -> block的header的_data_size是否被破坏
检查 -> *new_size 是否过大
if (*new_size > static_cast < size_t >(_HEAP_MAXREQ - no_mans_land_size - sizeof (_CrtMemBlockHeader)))
{
errno = ENOMEM;
return nullptr;
}
|
- 分配一个新的_CrtMemBlockHeader结构
1 2 3 4 | 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));
|
- 对新分配的内存初始化
1 2 3 4 5 6 7 | if (*new_size > new_head->_data_size)
{
memset (new_block + new_head->_data_size, clean_land_fill, *new_size - new_head->_data_size);
}
memset (new_block + *new_size, no_mans_land_fill, no_mans_land_size);
|
- 将新的header链接到双向链表中
1 2 3 4 5 6 7 8 9 | 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->_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++编程语言中,内存释放对应的关键字是delete
或free
,delete操作符最后调用到free函数,对应的源文件是debug_heap.cpp
。
free函数的调用栈为:free -> _free_dbg -> free_dbg_nolock,_free_dbg函数会获取临界区然后调用free_dbg_nolock。free_dbg_nolock函数分析过程如下:
- 释放内存时block_use为_FREE_BLOCK,若此时block_use为_NORMAL_BLOCK,且block由_aligned_malloc分配,不进行内存释放。
1 2 3 4 5 6 7 8 | if (block_use == _NORMAL_BLOCK && is_block_an_aligned_allocation(block))
{
_RPTN(_CRT_ERROR, "The Block at 0x%p was allocated by aligned routines, use _aligned_free()" , block);
errno = EINVAL;
return ;
}
|
- 调用_pfnAllocHook,只不过allocation_type换成了_HOOK_FREE。
1 2 3 4 5 6 | if (_pfnAllocHook && !_pfnAllocHook(_HOOK_FREE, block, 0, block_use, 0, nullptr, 0))
{
_RPT0(_CRT_WARN, "Client hook free failure.\n" );
return ;
}
|
- 进行一系列检查
1 2 3 4 5 6 | _ASSERTE(_CrtIsValidHeapPointer(block));
_ASSERTE(is_block_type_valid(header->_block_use));
_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)
|
- 在双向链表中,删除block元素,并释放内存.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | header->_block_header_next->_block_header_prev = header->_block_header_prev;
header->_block_header_prev->_block_header_next = header->_block_header_next;
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类型之外的其他内存,结果的数据结构如下:
1 2 3 4 5 6 7 8 | typedef struct _CrtMemState
{
struct _CrtMemBlockHeader * pBlockHeader;
size_t lCounts[_MAX_BLOCKS];
size_t lSizes[_MAX_BLOCKS];
size_t lHighWaterCount;
size_t lTotalCount;
} _CrtMemState;
|
- _CrtMemDumpAllObjectsSince主要显示_NORMAL_BLOCK、_CRT_BLOCK和_CLIENT_BLOCK类型的内存,显示的回调函数可以自行设置,函数原型如下:
1 | typedef void (__cdecl * _CRT_DUMP_CLIENT)( void *, size_t );
|
CRT库检测内存泄露的优缺点
优点
- Windows SDK自带的内存泄露检测工具,使用简单方便。
缺点
- 无法检测使用Windows APi来分配内存的情况,如: HeapAlloc或VirtualAlloc.
- 仅使用源码模式下的检测,对于已编译成功的二进制文件无能为力.
- 若程序依赖于其他的库文件,库文件出现的内存泄露无法被检测。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法