在 visual studio 单元测试中检查内存泄漏

Checking for memory leaks in a visual studio unit test

我主要是一名 Linux 开发人员,但是,我继承了一个存在内存泄漏的 windows dll。 我知道原因并相信我已经解决了它。 我想在单元测试中检查一下。 单元测试使用内置的 cppunit 测试框架,这与我通常在 Linux 上使用的 cppunit 框架无关。 即

#include "CppUnitTest.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;

我想做的是测量代码块前后的内存使用情况,并检查它是否没有改变——这表明内存泄漏。或者类似地,检查分配器类型函数是否准确分配了后续析构函数类型函数释放的内存量。

是否有合适的API我可以用来可靠地获取当前内存使用情况?

我天真地尝试了以下方法:

size_t getMemoryUsage()
{
    PROCESS_MEMORY_COUNTERS pmc;
    auto processHandle = GetCurrentProcess();
    if (GetProcessMemoryInfo(processHandle, &pmc, sizeof(pmc)))
    {
        return pmc.WorkingSetSize;
    }
    else 
    {
        Assert::Fail(L"Unable to get memory usage for current process");
    }
    return 0;
}

这给了我当前进程的内存使用情况。不幸的是,这并不能准确反映正在进行的分配和释放。我认为,如果我释放内存,OS 可能仍保留它以供应用程序稍后使用。工作集是 OS 分配给进程的,而不是它在内部实际使用的内存。

我尝试通过 What is private bytes, virtual bytes, working set? 将其更改为 PrivateUsage,但这似乎并不总是在 malloc 之后改变。

有合适的 API 可以帮我做这件事吗? 也许有一个库可以像您在 Linux 上使用 LD_PRELOAD 那样替代经过检测的 malloc? 参见 ld-preload-equivalent-for-windows-to-preload-shared-libraries

这里有几个类似的问题-例如memory leak unit test c++

此问题特定于 visual studio 中使用 cppunit 对 DLL 进行单元测试的情况。

DLL 不公开可重写的分配器接口。我想我的 目前最好的选择可能是增加一个。 如果有更简单的方法,我宁愿避免进行大量更改。 确认这是唯一方法的答案将被接受。

我不觉得尝试使用 OS API 是可靠的,好像我这样做 p = new char[1024]; delete[] p; 没有承诺内存返回给 OS,而且在很多情况下不会。例如假设最小的页面大小是 4KB,很明显,为小对象分配 4KB 会很浪费,所以进程 中的分配器 会将这个较大的块拆分,因此 OS 看不到这样的碎片有没有被释放

这也适用于其他 OS/compilers。如果您不断重复相同的测试循环 "it keeps using more memory",您可以确定随时间推移的趋势,但随后您必须继续搜索它,并且负载不太一致,很难判断几 KB 的差异是泄漏还是没有。


Visual Studio 有许多更集成的工具可以提供帮助。这些通常假设您正在使用 new/deletemalloc/free 或 IDE 可能知道的其他此类内容。如果不是,您可能需要稍微调整 DLL,以便 IDE 能够以最准确的方式知道发生了什么。

例如,如果您使用内部内存 "pool",系统只能知道池分配的内存,而不是它的用途或是否返回到该池。


要查找执行中的内存泄漏(比如 运行 测试用例),您可以使用 memory leak detection 功能。

#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
int main()
{
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    char *leak = new char[1024];
}
The thread 0x3280 has exited with code 0 (0x0).
Detected memory leaks!
Dumping objects ->
{94} normal block at 0x0000021EF2EA1FD0, 1024 bytes long.
 Data:  CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.
The program '[0xF64] test.exe' has exited with code 0 (0x0).

然后您可以设置一个断点以在下次找到它时停止在该分配上。您需要程序 运行 完全相同才能正常工作,但对于一段通常可行的可疑代码进行单元测试。

您可以通过在 VS 中一开始就暂停您的程序,然后在手表 window 中将 {,,ucrtbased.dll}_crtBreakAlloc 设置为所需的分配编号来执行此操作,例如 94。 运行在你的程序中它将停止在有问题的分配中,让你看到堆栈跟踪等。

默认情况下,这会转到调试输出,这不容易从自动化中捕获,但您可以将其重定向到 stderr,然后检查测试输出中是否有任何 "Detected memory leaks!"(以及测试用例 success/fail/etc.).

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
_CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR);
_CrtSetReportMode(_CRT_ERROR, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ERROR, _CRTDBG_FILE_STDERR);

您还可以包含文件和行号(请参阅 Microsoft 文档),但不幸的是,这通常更加困难,因为宏可能会破坏例如新的放置,并且在您没有直接分配的情况下不起作用在您正在编译的源代码中(现有库等)。


一个非常有用的 IDE 工具是诊断工具 window 中的 Memory Usage

举个例子:

#include <stdio.h>
int main()
{
    for (int i = 0; i < 1000; ++i)
    {
        char *more_leaks = new char[100];
        sprintf(more_leaks, "Leaking %d memory", i);
        if (i % 55) delete[] more_leaks;
    }
}

在 VS 中,转到 "Debug" -> "Windows" -> "Diagnostic Tools"。

运行 你的程序到某个断点,然后单击新 window 中的 "Memory Usage" 选项卡。选项卡左侧是一个 "Take Snapshot" 按钮。然后 运行 你的程序在你认为泄漏的函数之后,再拍一张快照。

然后您可以查看分配是否存在差异、它们是什么、它们保存的数据是什么,并大致探索您程序中的内存。

这可能有点矫枉过正,但我​​最终选择了使用 malloc 和 free 的本地实现并允许它们被覆盖的路线:

foobar_alloc.h:

///
/// @brief
/// This module provides routines to control the memory allocation and free functions 
/// used by the library.
///
/// Altering these functions is intended for use in testing only.
///

#ifdef __cplusplus
extern "C"
{
#endif

///
/// @brief
/// A function to be used as allocate memory
/// This should have semantics equivalent to the malloc() system call.
typedef void* (*AllocFunc)(size_t);

///
/// @brief
/// A function to be used to delete memory
/// This should have semantics equivalent to the free() system call.
typedef void (*FreeFunc)(void*);

///
/// @brief
/// Tells the library to use the given allocator function instead of the current one.
///
/// The default value is to use malloc()
/// 
/// @return
/// returns the currently used allocator function allowing it to be restored
///
__declspec(dllexport) AllocFunc set_alloc_func(AllocFunc func);

///
/// @brief
/// Tells the library to use the given deallocator function instead of the current one.
///
/// The default value is to use free()
/// 
/// @return
/// returns the currently used deallocator function allowing it to be restored
///
__declspec(dllexport) FreeFunc set_free_func(FreeFunc func);

///
/// @brief
/// Allocate memory using the currently set allocation funtion - default malloc()
///
__declspec(dllexport) void* foobar_malloc(size_t size);

///
/// @brief
/// Allocate memory using the currently set allocation funtion - default malloc()
///
__declspec(dllexport) void foobar_free(void* ptr);

#ifdef __cplusplus
}
#endif

foobar_alloc.cpp:

#include "foobar_alloc.h"
#include <malloc.h>

static AllocFunc allocator = malloc;
static FreeFunc deallocator = free;

AllocFunc set_alloc_func(AllocFunc newFunc)
{
    AllocFunc old = allocator;
    allocator = newFunc;
    return old;
}

FreeFunc set_free_func(FreeFunc newFunc)
{
    FreeFunc old = deallocator;
    deallocator = newFunc;
    return old;     
}

void* foobar_malloc(size_t size)
{
    return allocator(size);
}

void foobar_free(void* ptr)
{
    return deallocator(ptr);
}

// EOF

为了允许 C++ 代码以及我添加的 C,

foobar_new.h:

#pragma once
///
/// @brief
/// This is a private header file to override the global new and delete operators
/// to use foobar_malloc and foobar_free to allow for testing.
/// It is not intended for use outside of the dll.
/// 

#include <new>
#include "foobar_alloc.h"

void* operator new(std::size_t sz) {
    return foobar_malloc(sz);
}

void operator delete(void* ptr) noexcept {
    foobar_free(ptr);
}

void* operator new[](std::size_t sz) {
    return foobar_malloc(sz);
}

void operator delete[](void* ptr) noexcept {
    foobar_free(ptr);
}      

在测试代码中添加自己实现的malloc和free,例如:

Test_foobar.cpp:

namespace
{
    size_t totalMemoryUsage = 0;
}

// override the global operator new 
void* leaktest_malloc(std::size_t n) throw(std::bad_alloc)
{
    void* res = malloc(n);
    if (res == nullptr) throw std::bad_alloc();
    totalMemoryUsage += _msize(res);
    return res;
}

// override the global delete operator 
void leaktest_free(void* p) throw()
{
    totalMemoryUsage -= _msize(p);
    free(p);
}

记得在测试中的某处启用它:

// tell the library to use our wrappers to free & malloc
set_alloc_func(leaktest_malloc);
set_free_func(leaktest_free);

您需要在分配的 DLL 中的所有 C++ 代码中#include foobar_new 并将 malloc 和 free 替换为 foobar_malloc 和 foobar_free.

根据需要将 foobar 替换为您自己的库名称,以避免任何其他库出现命名空间问题。