我有一个用 C++ 编写的简单程序,其构建配置如下:

  1. 使用/链接libstdc++
  2. 使用/链接libc++

我使用valgrind运行这两个版本,如下所示:

valgrind –leak-check=full –show-reachable=yes –track-origins=yes –log-file=test_program.log -v./test_program

libstdc++版本运行后没有出现内存泄漏:

==
== HEAP SUMMARY:
==     in use at exit: 0 bytes in 0 blocks
==   total heap usage: 24,813,106 allocs, 24,813,106 frees, 51,325,970,073 bytes allocated
==
== All heap blocks were freed -- no leaks are possible
==
== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

然而libc++运行时它显示内存泄漏:

==434036== HEAP SUMMARY:
==434036==     in use at exit: 16 bytes in 1 blocks
==434036==   total heap usage: 317,709,577 allocs, 317,709,576 frees, 645,827,127,171 bytes allocated
==434036==
==434036== Searching for pointers to 1 not-freed blocks
==434036== Checked 401,408 bytes
==434036==
==434036== 16 bytes in 1 blocks are still reachable in loss record 1 of 1
==434036==    at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==434036==    by 0x49A365F: ??? (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036==    by 0x49A24E9: __cxa_get_globals (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036==    by 0x49A53F6: __cxa_throw (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==434036==    by 0x2EE7B3: goal::details::special_node<double>::value() const (in workspace/goal/goal_test)
==434036==    by 0x2DC349: goal::details::caller_node<double>::value() const (in workspace/goal/goal_test)
==434036==    by 0x50AC40: double goal::details::arg_node<double>::process<main_node<double> > const&) (in workspace/goal/goal_test)
==434036==    by 0x45A79C: bool execute_test_base<double>() (in workspace/goal/goal_test)
==434036==    by 0x2322A0: main (in workspace/goal/goal_test)
==434036==
==434036== LEAK SUMMARY:
==434036==    definitely lost: 0 bytes in 0 blocks
==434036==    indirectly lost: 0 bytes in 0 blocks
==434036==      possibly lost: 0 bytes in 0 blocks
==434036==    still reachable: 16 bytes in 1 blocks
==434036==         suppressed: 0 bytes in 0 blocks
==434036==
==434036== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

我还对该程序进行了另外两个构建:

  1. 使用/libstdc++链接ASAN/LSAN/UBSAN/TSAN
  2. 使用/libc++链接ASAN/LSAN/UBSAN/TSAN

运行它们时,它们都不会触发清理错误或警告。

使用的编译器:

  1. g++-13 (Ubuntu 13.1.0-8ubuntu1~22.04) 13.1.0
  2. clang 版本 20.0.0git

仅当与 libc++ 链接时,两个编译器上才会观察到 valgrind 泄漏。


问题: valgrind 的泄漏可能是误报吗?还可以做些什么来验证它是合法的?

12

  • 2
    这本身并不是泄漏。它只是说在程序结束时仍有可访问的内存尚未释放。但退出时不释放内存通常没有问题(除非有析构函数要运行并进行相关清理)。如果存在实际泄漏(即没有指向剩余内存的指针),则 valgrind 会在“ (肯定|间接|可能)丢失main”下报告它


    – 


  • ASAN/LSAN 泄漏检测的工作方式比 valgrind 的检测可靠性低得多。


    – 

  • 1
    @user17732522 所以两个标准库之间的内存状态差异是实现差异,而不是实际泄漏?


    – 

  • 1
    取决于你所说的“误报”是什么意思。由于(根据提供的信息)你的程序正在退出,所有未释放的内存都将被回收(假设是现代主机操作系统,如 windows 或 unix)。如果你将代码用作不会终止的大型程序的一部分(例如,一个程序执行了你的测试程序所做的所有事情,但在一个无限运行的循环中),或者如果你的程序(为某个平台构建并)在某个平台上执行(例如,在金属上)程序终止时不会恢复内存,则会发生泄漏(内存使用量不断增加)。


    – 

  • 2
    从技术上讲(除了 sanitiser 错误之外),这不是误报 – valgrind 检测到了在关键检测点未释放分配内存的情况。这是否重要取决于您的用例,您尚未指定。如果您将正在测试的代码重新用作更大、持续运行的程序的一部分,那么它将很重要


    – 


最佳答案
2

简而言之:您上面描述的情况不是内存泄漏,因为一块内存被分配(例如通过 malloc)给一个指针,后来该指针被覆盖或以其他方式丢失(范围),导致无法显式释放分配的内存。

那么valgrind给出的泄漏报告是什么?

标准库实现在如何实现指定的 C++ 标准功能方面具有很大的自由度。

在您的情况下,您看到的是libc++,与不同libstdc++当可执行文件第一次尝试在给定线程内引发异常时(main 也被视为线程),会创建一个线程本地存储TLS)内存实例(通过) __calloc_with_fallback(aka process)

__cxa_eh_globals * __cxa_get_globals () {
//  Try to get the globals for this thread
    __cxa_eh_globals* retVal = __cxa_get_globals_fast ();

//  If this is the first time we've been asked for these globals, create them
    if ( NULL == retVal ) {
        retVal = static_cast<__cxa_eh_globals*>
                    (__calloc_with_fallback (1, sizeof (__cxa_eh_globals)));
        if ( NULL == retVal )
            abort_message("cannot allocate __cxa_eh_globals");
        if ( 0 != std::__libcpp_tls_set ( key_, retVal ) )
           abort_message("std::__libcpp_tls_set failure in __cxa_get_globals()");
       }
    return retVal;
    }

代码可以在这里找到:

实例化内存的代码libc++立即通过调用将关联指针注册到关闭的内存管理器__libcpp_tls_set

这里的想法是,作为进程关闭过程的一部分:一旦进程的所有状态都被释放/销毁/清理,注册的TLS相关分配(因为进程中可能有多个线程)__libcpp_tls_set最终会被释放(或释放)。

Valgrind在这里提出了一个问题,因为它无法跟踪分配的真实位置并将其与相关的销毁对齐,尽管自进程运行开始以来分配的所有内存最终都会被明确释放 – 而不是通过操作系统清理隐式释放。

使用以下代码可以轻松复制您所看到的泄漏:

int main()
{
    try
    {
        throw 1;
    }
    catch (int)
    {} 

    return 0;
}

建造:

c++ -pedantic-errors -Wall -Wextra -Werror -O2 -o exceptiontest exceptiontest.cpp -L/usr/lib -lc++

运行 valgrind:

valgrind –leak-check=full –show-reachable=yes –track-origins=yes –log-file=exceptiontest.log -v./exceptiontest

Valgrind 输出:

==551373== HEAP SUMMARY:
==551373==     in use at exit: 16 bytes in 1 blocks
==551373==   total heap usage: 2 allocs, 1 frees, 160 bytes allocated
==551373==
==551373== Searching for pointers to 1 not-freed blocks
==551373== Checked 135,200 bytes
==551373==
==551373== 16 bytes in 1 blocks are still reachable in loss record 1 of 1
==551373==    at 0x484DA83: calloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==551373==    by 0x48AE65F: ??? (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373==    by 0x48AD4E9: __cxa_get_globals (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373==    by 0x48B03F6: __cxa_throw (in /usr/lib/llvm-14/lib/libc++abi.so.1.0)
==551373==    by 0x109105: main (in temp/exception_test/exceptiontest)
==551373==
==551373== LEAK SUMMARY:
==551373==    definitely lost: 0 bytes in 0 blocks
==551373==    indirectly lost: 0 bytes in 0 blocks
==551373==      possibly lost: 0 bytes in 0 blocks
==551373==    still reachable: 16 bytes in 1 blocks
==551373==         suppressed: 0 bytes in 0 blocks
==551373==
==551373== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

如您所见,编写的程序非常简单且完全符合标准,并且不会显式或隐式泄漏内存,但根据所使用的 c++ 标准库实现,valgrind 会引发与未释放块相关的问题。


当使用构建程序时libstdc++,正如预期的那样,没有产生泄漏或可到达的块:

==551500== HEAP SUMMARY:
==551500==     in use at exit: 0 bytes in 0 blocks
==551500==   total heap usage: 2 allocs, 2 frees, 73,860 bytes allocated
==551500==
==551500== All heap blocks were freed -- no leaks are possible
==551500==
==551500== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

2

  • 我认为这是 libc++ 的缺陷。libstdc++ 有一个 freeres 函数,可以在 Valgrind 下运行时释放这样的内存。


    – 

  • 1
    这绝对是一个极好的答案!它解释了一切,谢谢@Patrick!


    – 

简单的规则。不要自欺欺人地认为可能会有假阳性。这几乎总是一厢情愿的想法和确认偏见。

Valgrind memcheck 确实会产生一些误报,但这种情况很少见。泄漏检测是一项“简单”的工作,因此误报率基本为零。

Memcheck 可以比清理程序更好地检测泄漏,因为它可以完整查看客户机 exe 的执行情况,从第一条指令到最后一条指令。我不是清理程序专家,但我认为清理程序代码只能在第一个全局构造函数执行时启动。

正如评论中所说,如果您的 exe 抛出了以 std::terminate 结尾的异常,那么像 atexit 清理这样的操作就不会执行。只有当 exe 执行干净退出时,泄漏检测才有意义。

2

  • 您所说的一切都没有意义,没有调用 std::terminate,运行时两个构建都正常结束而没有失败。


    – 

  • 在这种情况下,这是 libc++ 问题。他们应该释放所有内存,或者提供 freeres 函数。


    –