【问题标题】:Checking for memory leaks in a visual studio unit test在 Visual Studio 单元测试中检查内存泄漏
【发布时间】:2020-04-11 21:57:09
【问题描述】:

我主要是一名 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;
}

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

我尝试通过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 不会为可能被覆盖的分配器公开接口。我认为我的 目前最好的选择可能是添加一个。 如果有更简单的方法,我宁愿避免进行大量更改。 确认这是唯一方法的答案将被接受。

【问题讨论】:

    标签: c++ visual-studio memory-leaks


    【解决方案1】:

    我不认为尝试使用操作系统 API 是可靠的,就好像我这样做 p = new char[1024]; delete[] p; 没有保证将内存返回给操作系统,而且在很多情况下也不会。例如假设最小的页面大小是 4KB,显然为小对象分配 4KB 会很浪费,所以分配器在您的进程中会将这些较大的块拆分,因此操作系统无法查看是否这样碎片是否被释放。

    这也适用于其他操作系统/编译器。如果您不断重复相同的测试循环“它一直在使用更多内存”,您可以确定随时间推移的趋势,但是您必须去搜索它,并且负载不一致,很难判断几个 KB 的差异是否是泄漏与否。


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

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


    要查找执行中的内存泄漏(例如运行测试用例),您可以使用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];
    }
    
    线程 0x3280 以代码 0 (0x0) 退出。 检测到内存泄漏! 倾倒对象 -> {94} 0x0000021EF2EA1FD0 处的普通块,1024 字节长。 资料: CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 对象转储完成。 程序“[0xF64] test.exe”已退出,代码为 0 (0x0)。

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

    您可以通过在 VS 中从一开始就暂停程序,然后在监视窗口中将{,,ucrtbased.dll}_crtBreakAlloc 设置为所需的分配编号,例如94。运行您的程序,它将在相关分配中停止,让您看到堆栈跟踪等。

    默认情况下,这会转到调试输出,这不容易从自动化中捕获,但您可以将其重定向到 stderr,然后检查是否有任何“检测到内存泄漏!”在您的测试输出中(以及测试用例成功/失败/等)。

    _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 工具是诊断工具窗口中的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 中,转到“调试”->“Windows”->“诊断工具”。

    将程序运行到某个断点,然后单击新窗口中的“内存使用”选项卡。选项卡的左侧是“拍摄快照”按钮。然后在您认为泄漏的函数之后运行您的程序,并拍摄另一个快照。

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

    【讨论】:

    • 我对操作系统 API 有同样的感觉,因此提出了这个问题。虽然您提到的工具对调试很有用,但我在这里寻找测试自动化,因此它们并不能真正解决问题。
    • 内存泄漏检测转储信息,您可以使用_CRTDBG_FILE_STDERR 对其进行配置,然后我只需检查进程输出中的自动化中没有泄漏以及测试失败(“如果输出包含“检测到内存泄漏!”然后失败“)。然后使用其他工具查找单元测试输出实际抱怨的内容。
    • 我尝试了 _CrtSetDbgFlag 和 _CrtSetReportMode,但即使我引入了泄漏,我似乎也无法从测试代码中获得任何输出。
    • 确保使用了调试 CRT,并且您在检查 C 分配的每个源文件中都有所需的 _CRTDBG_MAP_ALLOC#include &lt;crtdbg.h&gt;malloc 等)。 new 我认为是全球性的。请记住_CRTDBG_LEAK_CHECK_DF 表示仅在进程退出时报告,因为无论如何都很难自动区分现有进程的泄漏与非泄漏。 _CRTDBG_ALLOC_MEM_DF 需要在你分配东西之前,小心静态/全局初始化器,它们可能不会被检测到。
    • 哦,如果您想在 stdout/stderr 而不是 VS 调试“输出”窗口上使用 _CrtSetReportFile,请不要忘记。
    【解决方案2】:

    这可能有点矫枉过正,但我​​最终选择了使用 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 替换为您自己的库名称,以避免与任何其他库出现命名空间问题。

    【讨论】:

      猜你喜欢
      • 2011-09-28
      • 1970-01-01
      • 2016-01-05
      • 1970-01-01
      • 2011-02-18
      • 2010-09-15
      • 1970-01-01
      • 2018-02-23
      相关资源
      最近更新 更多