valgrind工具排查内存泄露

valgrind工具排查内存泄露

概述

Valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具。我们可以使用它进行内存、多线程及性能等各种问题的分析。它采用非侵入方式,所谓非侵入方式是指:我们不用在代码中插入分析工具的库。这对于开发者来说是友好的。因为如果要将工具编译到文件中,或者要调用其提供的一些API,才能进行问题分析,无疑增大了用户的学习和使用成本。

Valgrind 发行版目前包括七个生产质量工具:一个内存错误检测器、两个线程错误检测器、一个缓存和分支预测分析器、一个调用图生成缓存和分支预测分析器,以及两个不同的堆分析器。它还包括一个实验性的 SimPoint 基本块矢量生成器。它可在以下平台上运行:X86/Linux、AMD64/Linux、ARM/Linux、ARM64/Linux、PPC32/Linux、PPC64/Linux、PPC64LE/Linux、S390X/Linux、MIPS32/Linux、MIPS64/Linux、X86/Solaris , AMD64/Solaris, ARM/Android (2.3.x 及更高版本), ARM64/Android, X86/Android (4.0 及更高版本), MIPS32/Android, X86/FreeBSD, AMD64/FreeBSD, X86/Darwin 和 AMD64/Darwin (Mac OS X 10.12)

valgrind功能介绍

Valgrind是一个构建动态分析工具的工具框架,其带有一组工具,每个都执行某种调试,分析或类似的任务,可帮助开发者改进程序。 Valgrind的架构是模块化的,因此可以轻松创建新工具,而不会影响现有结构。标准提供了许多有用的工具。

  • Memcheck是一个内存错误检测器。它可以帮助您使您的程序更加正确,特别是使用C和C ++编写的程序。
  • Cachegrind是一个缓存和分支预测分析器。它可以帮助您使程序运行得更快。
  • Callgrind是一个调用图生成缓存分析器。它与Cachegrind有一些重叠,但也收集了一些Cachegrind没有的信息。
  • Helgrind是一个线程错误检测器。它可以帮助您使多线程程序更加正确。
  • DRD也是一个线程错误检测器。它与Helgrind类似,但使用不同的分析技术,因此可能会发现不同的问题。
  • Massif是一个堆分析器。它可以帮助您使您的程序使用更少的内存。
  • DHAT是一种不同类型的堆分析器。它可以帮助您了解块寿命,块利用率和布局低效问题。
  • SGcheck是一个可以检测堆栈和全局数组溢出的实验工具。它的功能与Memcheck的功能是互补的:SGcheck发现了Memcheck无法解决的问题,反之亦然。
  • BBV是一个实验性的SimPoint基本块矢量生成器。对于从事计算机体系结构研究和开发的人员非常有用。

我们主要讨论内存错误检测器(Memcheck),用于检测C/C ++程序中常见问题:

  • 访问不应该使用的内存,例如溢出,堆溢出,溢出栈顶,在访问释放了的内存。
  • 使用未定义的值,即尚未初始化的值或从其他未定义值派生的值。
  • 错误地释放堆内存,如重复释放堆,或者不匹配的使用malloc/new/new []与free/delete/delete []
  • 在memcpy和相关函数中重叠src和dst指针。
  • 在调用内存分配函数时,给size参数传递一个负值。
  • 内存泄漏。

使用Valgrind

Valgrind的使用非常简单,valgrind命令的格式如下:

valgrind [valgrind-options] your-prog [your-prog options] 

当我们需要分析一款软件时,只要采用上面格式的调用。其中your-prog是被分析的程序文件路径,your-prog-options是原本要传递给待分析程序的参数。valgrind-options是valgrind的一些参数,最常用的是–tool=【tool_name】。我们可以使用不同的tool进行不同的分析,比如使用memcheck进行内存问题分析。

一些常用的选项如下:

  • -h --help 显示帮助信息。

  • –version 显示valgrind内核的版本,每个工具都有各自的版本。

  • -q --quiet 安静地运行,只打印错误信息。

  • -v --verbose 打印更详细的信息。

  • –tool= [default: memcheck] 最常用的选项。运行valgrind中名为toolname的工具。如果省略工具名,默认运行memcheck。

  • –db-attach= [default: no] 绑定到调试器上,便于调试错误。

  • –run-libc-freeres=<yes|no> [default: yes]
    GNU C库(libc.so),所有程序共用的,可能会分配一部分内存自已用。通常在程序退出时释放内存并不麻烦 – 这里没什么问题,因为Linux内核在一个进程退出时会回收进程全部的资源,所以这只是会造成速度慢。
    glibc的作者认识到这样会导致内存检查器,像Valgrind,在退出时检查内存错误的报告glibc的内存泄漏问题,为了避免这个问题,他们提供了
    一个__libc_freeres()例程特别用来让glibc释放分配的所有内存。因此Memcheck在退出时尝试着去运行__libc_freeres()。
    不幸的是,在glibc的一些版本中,__libc_freeres是有bug会导致段错误的。这在Red Hat 7.1上有特别声明。所以,提供这个选项来决定是否运行__libc_freeres。如果你的程序看起来在Valgrind上运行得很好,但是在退出时发生段错误,你可能需要指定–run-libc-freeres=no来修正,这将可能错误的报告libc.so的内存泄漏。

  • –leak-check=<no|summary|yes|full> [default: summary]
    当这个选项打开时,在客户程序结束时查找内存泄漏。内存泄漏意味着有用malloc分配内存块,但是没有用free释放,而且没有指针指向这块内存。这样的内存块永远不能被程序释放,因为没有指针指向它们。如果设置为summary,Valgrind会报告有多少内存泄漏发生了。如果设置为full或yes,Valgrind给出每一个独立的泄漏的详细信息。

  • –show-reachable=<yes|no> [default: no]
    当这个选项关闭时,内存泄漏检测器只显示没有指针指向的内存块,或者只能找到指向块中间的指针。当这个选项打开时,内存泄漏检测器还报告有指针指向的内存块。这些块是最有可能出现内存泄漏的地方。你的程序可能,至少在原则上,应该在退出前释放这些内存块。这些有指针指向的内存块和没有指针指向的内存块,或者只有内部指针指向的块,都可能产生内存泄漏,因为实际上没有一个指向块起始的指针可以拿来释放,即使你想去释放它。

  • –leak-resolution=<low|med|high>[default: low]
    在做内存泄漏检查时,确定memcheck将怎么样考虑不同的栈是否是相同的情况。当设置为low时,只需要前两层栈匹配就认为是相同的情况;当设置为med,必须要四层栈匹配,当设置为high时,所有层次的栈都必须匹配。
    对于hardcore内存泄漏检查,你很可能需要使用–leak-resolution=high和–num-callers=40或者更大的数字。注意这将产生巨量的信息,这就是为什么默认选项是四个调用者匹配和低分辨率的匹配。注意–leak-resolution= 设置并不影响memcheck查找内存泄漏的能力。它只是改变了结果如何输出。

  • –freelist-vol=[default: 5000000]
    当客户程序使用free(C中)或者delete(C++)释放内存时,这些内存并不是马上就可以用来再分配的。这些内存将被标记为不可访问的,并被放到一个已释放内存的队列中。这样做的目的是,使释放的内存再次被利用的点尽可能的晚。这有利于memcheck在内存块释放后这段重要的时间检查对块不合法的访问。
    这个选项指定了队列所能容纳的内存总容量,以字节为单位。默认的值是5000000字节。增大这个数目会增加memcheck使用的内存,但同时也增加了对已释放内存的非法使用的检测概率。

  • –workaround-gcc296-bugs=<yes|no>[default: no]
    当这个选项打开时,假定读写栈指针以下的一小段距离是gcc 2.96的bug,并且不报告为错误。距离默认为256字节。注意gcc 2.96是一些比较老的Linux发行版(RedHat 7.X)的默认编译器,所以你可能需要使用这个选项。如果不是必要请不要使用这个选项,它可能会使一些真正的错误溜掉。一个更好的解决办法是使用较新的,修正了这个bug的gcc/g++版本。

  • –partial-loads-ok=<yes|no>[default: no]
    控制memcheck如何处理从地址读取时字长度,字对齐,因此哪些字节是可以寻址的,哪些是不可以寻址的。当设置为yes是,这样的读取并不抛出一个寻址错误。而是从非法地址读取的V字节显示为未定义,访问合法地址仍然是像平常一样映射到内存。
    设置为no时,从部分错误的地址读取与从完全错误的地址读取同样处理:抛出一个非法地址错误,结果的V字节显示为合法数据。注意这种代码行为是违背ISO C/C++标准,应该被认为是有问题的。如果可能,这种代码应该修正。这个选项应该只是做为一个最后考虑的方法。

  • –undef-value-errors=<yes|no> [default: yes]
    控制memcheck是否检查未定义值的危险使用。当设为yes时,Memcheck的行为像Addrcheck(一个轻量级的内存检查工具,是Valgrind的一个部分),它并不检查未定义值的错误。如果你不希望看到未定义值错误,可以使用这个选项。

Memcheck是默认工具。–leak-check 选项打开详细的内存泄漏检测器。程序运行速度会比正常情况慢很多(例如20到30倍),并且会占用更多内存。Memcheck将发出有关内存错误和检测到的泄漏的消息。

这些问题有时候很难通过其他方式发现,常常长时间未被发现,然后导致偶然的,难以诊断的崩溃。Memcheck还使用命令行选项–xtree-memory和monitor命令xtmemory,对执行树提供内存分析

示例

valgrind --leak-check=yes ./valgrind_test
一开始是valgrind信息“==62414==”表示进程号
==62414== Memcheck, a memory error detector
==62414== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==62414== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==62414== Command: ./valgrind_test
程序访问非法地址的内存,无效写入
==62414== Invalid write of size 4
==62414==    at 0x40054B: fun (valgrind_test.c:21)
==62414==    by 0x400566: main (valgrind_test.c:28)
==62414==  Address 0x5201068 is 0 bytes after a block of size 40 alloc'd
==62414==    at 0x4C2AEC3: malloc (vg_replace_malloc.c:309)
==62414==    by 0x40053E: fun (valgrind_test.c:20)
==62414==    by 0x400566: main (valgrind_test.c:28)
==62414==

堆区情况:

==62414== HEAP SUMMARY:
==62414==     in use at exit: 40 bytes in 1 blocks
==62414==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
内存泄漏消息如下所示:

==62414== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==62414==    at 0x4C2AEC3: malloc (vg_replace_malloc.c:309)
==62414==    by 0x40053E: fun (valgrind_test.c:20)
==62414==    by 0x400566: main (valgrind_test.c:28)

有几种泄漏; 两个最重要的类别是,肯定泄露(definitely lost),可能已经泄露(possibly lost)

==62414== LEAK SUMMARY:
==62414==    definitely lost: 40 bytes in 1 blocks
==62414==    indirectly lost: 0 bytes in 0 blocks
==62414==      possibly lost: 0 bytes in 0 blocks
==62414==    still reachable: 0 bytes in 0 blocks
==62414==         suppressed: 0 bytes in 0 blocks
==62414==
==62414== For lists of detected and suppressed errors, rerun with: -s
==62414== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

报告的基本格式是:

{问题描述}   
at {地址、函数名、模块或代码行} 
by {地址、函数名、代码行}
by ...{逐层依次显示调用堆栈}
Address 0x???????? {描述地址的相对关系}

报告的输出文档整体格式则可以总结为:

1. copyright 版权声明
2. 异常读写报告
2.1 主线程异常读写
2.2 线程A异常读写报告
2.3 线程B异常读写报告
2... 其他线程
3. 堆内存泄露报告
3.1 堆内存使用情况概述(HEAP SUMMARY)
3.2 确信的内存泄露报告(definitely lost)
3.3 可疑内存操作报告 (show-reachable=no关闭)
3.4 泄露情况概述(LEAK SUMMARY)

官方解释及分析:
摘自http://valgrind.org/docs/manual/faq.html#faq.deflost

5.2.	With Memcheck's memory leak detector, what's the difference between "definitely lost", "indirectly lost", "possibly lost", "still reachable", and "suppressed"?
The details are in the Memcheck section of the user manual.

In short:

"definitely lost" means your program is leaking memory -- fix those leaks!

"indirectly lost" means your program is leaking memory in a pointer-based structure. (E.g. if the root node of a binary tree is "definitely lost", all the children will be "indirectly lost".) If you fix the "definitely lost" leaks, the "indirectly lost" leaks should go away.

"possibly lost" means your program is leaking memory, unless you're doing unusual things with pointers that could cause them to point into the middle of an allocated block; see the user manual for some possible causes. Use --show-possibly-lost=no if you don't want to see these reports.

"still reachable" means your program is probably ok -- it didn't free some memory it could have. This is quite common and often reasonable. Don't use --show-reachable=yes if you don't want to see these reports.

"suppressed" means that a leak error has been suppressed. There are some suppressions in the default suppression files. You can ignore suppressed errors.
  • definitely lost
    确认丢失。程序中存在内存泄露,应尽快修复。当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存则会报这个错误。报告给出的堆栈是内存被分配时的调用堆栈,它可以基本明确内存是由什么业务逻辑创建的。
char *Fun1()  //definitely lost  
{
    
      
    char *pcTemp;  
  
    pcTemp=(char*)malloc(10);  
    return pcTemp;  
}
  • indirectly lost
    间接丢失。当使用了含有指针成员的类或结构时可能会报这个错误。这类错误无需直接修复,他们总是与"definitely lost"一起出现,只要修复"definitely lost"即可。
int Fun4()//definitely and indirectly lost  
{
    
    
    c1 *pobjTest;

    pobjTest=new c1();

    return 0;
}

possibly lost
是说可能有泄漏,大多数情况下应视为与"definitely lost"一样需要尽快修复,除非你的程序让一个指针指向一块动态分配的内存(但不是这块内存起始地址),然后通过运算得到这块内存起始地址,再释放它。
当程序结束时如果一块动态分配的内存没有被释放且通过程序内的指针变量均无法访问这块内存的起始地址,但可以访问其中的某一部分数据,则会报这个错误。一般是有二级指针(指针的指针)等复杂情况不易于追踪时出现。

char *Fun3()//possibly lost  
{
    
    
    static char *s_pcTemp;
    char *pcData;

    pcData=(char*)malloc(10);
    s_pcTemp=pcData+1;

    return NULL;
}

still reachable
可以访问,未丢失但也未释放。是说内存没有被释放,尽管如此仍有指针指向,内存仍在使用中,这可以不算泄露。(程序退出时仍在工作的异步系统调用?).如果程序是正常结束的,那么它可能不会造成程序崩溃,但长时间运行有可能耗尽系统资源,因此笔者建议修复它。如果程序是崩溃(如访问非法的地址而崩溃)而非正常结束的,则应当暂时忽略它,先修复导致程序崩溃的错误,然后重新检测。

char *Fun2()//still reachable  
{
    
      
    static char *s_pcTemp=NULL;  
    if(s_pcTemp==NULL) s_pcTemp=(char*)malloc(10);
    return NULL;
}

suppressed
已被解决。出现了内存泄露但系统自动处理了。可以无视这类错误。这类错误我没能用例程触发,看官方的解释也不太清楚是操作系统处理的还是valgrind,也没有遇到过。所以无视他。

其它内存使用错误

写违例

#include <stdlib.h>
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    p[array_count] = 0; // Illegal read 
    free(p);
    return 0;
}

上述代码只分配了4个int型大小的空间,但是第6行要往该空间之后的空间写入数据,这就造成了写违例。使用valgrind分析会显示

==18100== Invalid write of size 4
==18100==    at 0x400658: main (mem_error.c:6)
==18100==  Address 0x51e0050 is 0 bytes after a block of size 16 alloc'd
==18100==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==18100==    by 0x40063F: main (mem_error.c:5)

第一行显示有4个字节被违例写入,第三行显示写入的位置在分配的16个字节之后。

读违例

#include <stdlib.h>
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    int error_num = p[array_count]; // Illegal read
    free(p);
    return 0;
}

错误的位置和上例一样,区别在于这次是读取不合法的地址的数据。使用valgrind分析显示

==31461== Invalid read of size 4
==31461==    at 0x400658: main (mem_error.c:6)
==31461==  Address 0x51e0050 is 0 bytes after a block of size 16 alloc'd
==31461==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==31461==    by 0x40063F: main (mem_error.c:5)

第一行显示有4个字节被违例读取,第三行显示读取的位置在分配的16个字节之后。

使用未初始化变量

这是初学C/C++编程的人非常容易犯的错误。

#include <stdlib.h>
#include <stdio.h>
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    printf("%d",  p[array_count - 1]);
    free(p);
 
    int undefine_num;
    printf("%d", undefine_num);
    return 0;
}

第7行和第11行分别访问了堆上、栈上未初始化的变量。valgrind分析显示

==24104== Conditional jump or move depends on uninitialised value(s)
==24104==    at 0x4E79F7F: vfprintf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==24104==    by 0x4E837A8: printf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==24104==    by 0x4006BA: main (mem_error.c:7)
==24104== 
==24104== Conditional jump or move depends on uninitialised value(s)
==24104==    at 0x4E79E37: vfprintf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==24104==    by 0x4E837A8: printf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==24104==    by 0x4006DA: main (mem_error.c:11)
==24104== 

虽然这个报告已经非常详细,但是我们还可以给valgrind增加–track-origins=yes,以打印问题出现在哪个结构上。当然这也会导致valgrind分析的比较慢

==29911== Conditional jump or move depends on uninitialised value(s)
==29911==    at 0x4E79F7F: vfprintf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==29911==    by 0x4E837A8: printf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==29911==    by 0x4006BA: main (mem_error.c:7)
==29911==  Uninitialised value was created by a heap allocation
==29911==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==29911==    by 0x40068F: main (mem_error.c:6)
==29911== 
==29911== Conditional jump or move depends on uninitialised value(s)
==29911==    at 0x4E79E37: vfprintf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==29911==    by 0x4E837A8: printf (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==29911==    by 0x4006DA: main (mem_error.c:11)
==29911==  Uninitialised value was created by a stack allocation
==29911==    at 0x400670: main (mem_error.c:4)

在系统函数中使用未初始化变量

我们看一个稍微复杂点的例子。下例中,test函数操作的是一个未初始化的变量,所以其结果是不可预知的。

#include <stdlib.h>
#include <stdio.h>
 
void test(int n) {
    
    
    n = n + 1;
}
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    test(p[array_count - 1]);
    free(p);
    return 0;
}

valgrind并不知道上述代码的作者想表达什么,所以它并没有报错

==28259== Command: ./mem_error
==28259== 
==28259== 
==28259== HEAP SUMMARY:
==28259==     in use at exit: 0 bytes in 0 blocks
==28259==   total heap usage: 1 allocs, 1 frees, 16 bytes allocated
==28259== 
==28259== All heap blocks were freed -- no leaks are possible
==28259== 
==28259== For counts of detected and suppressed errors, rerun with: -v
==28259== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

但是如果错误调用是针对系统函数。valgrind是知道系统函数的输入要求的,于是就可以判定这种行为违例。我们稍微改下代码

#include <stdlib.h>
#include <stdio.h>
 
void test(int n) {
    
    
    n = n + 1;
    write(stdout, "xxx", n); 
}
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    test(p[array_count - 1]);
    free(p);
    return 0;
}

valgrind就会分析出第6行系统方法write的第三个参数未初始化。

==4344== Syscall param write(count) contains uninitialised byte(s)
==4344==    at 0x4F0BED0: __write_nocancel (in /home/opt/gcc-4.8.2.bpkg-r4/gcc-4.8.2.bpkg-r4/lib64/libc-2.18.so)
==4344==    by 0x4006CA: test (mem_error.c:6)
==4344==    by 0x40070D: main (mem_error.c:12)

释放空间出错

我们可能重复释放同一段空间,或者给释放函数传入不是堆上的地址,或者使用了不对称的方法申请释放函数。这类错误发生在free,delete,delete[]和realloc上。

反复free同一段空间
#include <stdlib.h>
 
int main() {
    
    
    const int array_count = 4;
    int* p = malloc(array_count * sizeof(int));
    free(p);
    free(p);
    return 0;
}

使用valgrind分析,报告显示第7行释放了第6行已经释放了的空间,这个空间是在第5行申请的。

==6537== Invalid free() / delete / delete[] / realloc()
==6537==    at 0x4C28CBD: free (vg_replace_malloc.c:530)
==6537==    by 0x40065B: main (mem_error.c:7)
==6537==  Address 0x51e0040 is 0 bytes inside a block of size 16 free'd
==6537==    at 0x4C28CBD: free (vg_replace_malloc.c:530)
==6537==    by 0x40064F: main (mem_error.c:6)
==6537==  Block was alloc'd at
==6537==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==6537==    by 0x40063F: main (mem_error.c:5)
释放一个不是堆的空间
#include <stdlib.h>
 
int main() {
    
    
    int n = 1;
    int* p = &n;
    free(p);
    return 0;
}

valgrind会报告错误的释放栈上空间

==32411== Invalid free() / delete / delete[] / realloc()
==32411==    at 0x4C28CBD: free (vg_replace_malloc.c:530)
==32411==    by 0x4005F2: main (mem_error.c:6)
==32411==  Address 0x1fff000234 is on thread 1's stack
==32411==  in frame #1, created by main (mem_error.c:3)
申请释放方法不对称

对称的方法是指:

  • new使用delete释放
  • new[]使用delete[]释放
  • alloc类函数,如malloc,realloc等使用free释放
#include <stdlib.h>
 
int main() {
    
    
    int* p = new int(1);
    free(p);
    return 0;
}

valgrind可以分析出这种不对称调用——new申请空间,free释放空间。

==5666== Mismatched free() / delete / delete []
==5666==    at 0x4C28CBD: free (vg_replace_malloc.c:530)
==5666==    by 0x400737: main (mem_error.c:5)
==5666==  Address 0x59fc040 is 0 bytes inside a block of size 4 alloc'd
==5666==    at 0x4C281E3: operator new(unsigned long) (vg_replace_malloc.c:334)
==5666==    by 0x400721: main (mem_error.c:4)

空间覆盖

当我们操作内存时,可能会发生内存覆盖。

#include <stdlib.h>
#include <string.h>                                                
int main() {
    
    
    const int array_size = 8;
    char p[array_size] = {
    
    0};
    memcpy(p + 1, p, sizeof(char) * array_size);
    return 0;
}

这段代码的目的空间覆盖了源空间
valgrind分析的报告也说明了这个错误

==25991== Source and destination overlap in memcpy(0x1fff000231, 0x1fff000230, 8)
==25991==    at 0x4C2BFEC: memcpy@@GLIBC_2.14 (vg_replace_strmem.c:1022)
==25991==    by 0x4006E2: main (mem_error.c:7)

可疑的参数

在C/C++中,有符号数的负数的二进制最高位是0x1。如果把一个负数看成一个无符号类型的数,则可以表达出极大数,比如0xFFFFFFFF(无符号值4294967295,有符号值-1),因为它们的底层二进制值是一致的。

有时我们在调用内存分配时,不小心将空间大小设置为一个负数,就要求申请一个极大的空间,这明显是有问题的。

#include <stdlib.h>
 
int main() {
    
    
    const int array_size = -1;
    void* p = malloc(array_size);
    free(p);
    return 0;
}

这个时候valgrind就会检测出参数可疑

==3364== Argument 'size' of function malloc has a fishy (possibly negative) value: -1
==3364==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==3364==    by 0x40070A: main (mem_error.c:5)

内存泄露

内存泄露是比较常见的问题,往往也是非常难以排查的问题。

#include <stdlib.h>
 
int main() {
    
    
    const int array_size = 32; 
    void* p = malloc(array_size);
    return 0;
}

这次我们给valgrind增加选项–leak-check=full以显示出详细信息

valgrind --tool=memcheck --leak-check=full ./mem_error

valgrind分析出第5行分配的空间没有释放。其中definitely lost是指“确认泄露”,

==17393== HEAP SUMMARY:
==17393==     in use at exit: 32 bytes in 1 blocks
==17393==   total heap usage: 1 allocs, 0 frees, 32 bytes allocated
==17393== 
==17393== 32 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17393==    at 0x4C27BC3: malloc (vg_replace_malloc.c:299)
==17393==    by 0x4006B8: main (mem_error.c:5)
==17393== 
==17393== LEAK SUMMARY:
==17393==    definitely lost: 32 bytes in 1 blocks
==17393==    indirectly lost: 0 bytes in 0 blocks
==17393==      possibly lost: 0 bytes in 0 blocks
==17393==    still reachable: 0 bytes in 0 blocks
==17393==         suppressed: 0 bytes in 0 blocks

valgrind(memcheck)包含7类错误

1,illegal read/illegal write errors  
 提示信息:[invalid read of size 4]
2,use of uninitialised values 
  提示信息:[Conditional jump or move depends on uninitialised value]
3,use of uninitialised or unaddressable values in system calls  
  提示信息:[syscall param write(buf) points to uninitilaised bytes]
4,illegal frees  
  提示信息:[invalid free()]
5,when a heap block is freed with an inappropriate deallocation function  
  提示信息:[Mismatched free()/delete/delete[]]
6,overlapping source and destination blocks
  提示信息:[source and destination overlap in memcpy(,)]
7,memory leak detection
 1),still reachable 
    内存指针还在还有机会使用或释放,指针指向的动态内存还没有被释放就退出了
 2),definitely lost 
    确定的内存泄露,已经不能访问这块内存
 3),indirectly lost 
    指向该内存的指针都位于内存泄露处
 4),possibly lost 
    可能的内存泄露,仍然存在某个指针能够快速访问某块内存,但该指针指向的已经不是内存首位置

原理

当待分析程序片段第一次被执行时,valgrind会将代码片段交给工具——比如内存调试时使用的memcheck处理,工具会在代码中插入一些辅助分析的代码片段。新的代码会在valgrind模拟出的CPU上执行。然后valgrind会结合之前读取到的待执行程序和其所关联的库文件的调试信息,输出分析结果。

因为有新插入的代码逻辑,valgrind运行下的程序都比其独立运行时要慢。视选择的工具不同,其效率可能是正常值的1/4~1/50。所以使用valgrind做性能分析时,一般不使用绝对数据,而使用相同环境下的相对数据进行对比。

为了让valgrind读取出准确的调试信息,待分析程序最好使用-O0禁止编译器优化,以及使用-g让编译器把行号信息编入到文件中。

Memcheck 能够检测出内存问题,关键在于其建立了两个全局表。

Valid-Value 表
对于进程的整个地址空间中的每一个字节(byte),都有与之对应的 8 个 bits;对于CPU的每个寄存器,也有一个与之对应的bit向量。这些bits负责记录该字节或者寄存器值是否具有有效的、已初始化的值。

Valid-Address 表
对于进程整个地址空间中的每一个字节(byte),还有与之对应的1个bit,负责记录该地址是否能够被读写。

检测原理:

当要读写内存中某个字节时,首先检查这个字节对应的 A bit。如果该A bit显示该位置是无效位置,memcheck则报告读写错误。 内核(core)类似于一个虚拟的 CPU 环境,这样当内存中的某个字节被加载到真实的 CPU 中时,该字节对应的 V bit 也被加载到虚拟的 CPU 环境中。一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则 memcheck 会检查对应的V bits,如果该值尚未初始化,则会报告使用未初始化内存错误。

参考地址:

https://valgrind.org/docs/manual/manual.html
http://www.cppblog.com/Wealth/archive/2008/06/04/52118.html
https://blog.csdn.net/breaksoftware/article/details/79445591

猜你喜欢

转载自blog.csdn.net/u010523811/article/details/129141596