Linux 内存检查工具 Valgrind 介绍

2019-10-12 language c/cpp

Valgrind 可以用于构建动态分析工具,包括一个工具集,每个工具可以进行一类的调试、分析、测试,以帮助完善程序。

该工具采用的是模块化方式,可以很容易创建新的工具而又不会扰乱现有工具。

简介

  • memcheck 内存异常检测,包括未释放、重复释放等异常;
  • sgcheck 用来检测堆和全局数组的溢出,可以与 memcheck 互补;
  • cachegrind 缓存和分支预测分析器,可以用来提高程序的性能;
  • callgrind 函数调用图缓存生成分析器,与上一工具功能略有重叠,可以收集一些其它信息;
  • helgrind 多线程的异常检测,可以发现一些多线程导致的问题;
  • DRD 与 helgrind 类似,采用了不同的实现方式,可以找到不同的问题;
  • massif 堆分析器,对堆内存的使用进行分析;
  • DHAT 另一种不同的堆分析器,有助于理解块的生命周期、块的使用和布局的低效等问题;

memcheck

一个内存使用情况的检查工具,可以处理如下的几类错误:

  • 使用未初始化的内存,主要是一些变量。
  • 内存多次释放、释放后访问、未释放等。
  • 对堆栈内存空间的非法访问,包括了写入和读取。

内存操作

包括了内存未释放、多次释放等操作,其中前者默认只打印内存泄漏信息,如果要查看详细的地址需要添加 --leak-check=full 参数。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (void)
{
    char *buff = NULL;

    buff = (char *)malloc(5);
    if (buff == NULL)
        return -1;

    free(buff);
    free(buff);

    return 0;
}

上面代码出现了两次的释放操作,会有如下的报错信息。

==43613== Invalid free() / delete / delete[] / realloc()
==43613==    at 0x4C2ACBD: free (vg_replace_malloc.c:530)
==43613==    by 0x400722: main (in /tmp/test/race)
==43613==  Address 0x541f040 is 0 bytes inside a block of size 5 free'd
==43613==    at 0x4C2ACBD: free (vg_replace_malloc.c:530)
==43613==    by 0x40071A: main (in /tmp/test/race)
==43613==  Block was alloc'd at
==43613==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==43613==    by 0x40070A: main (in /tmp/test/race)

如果把上面的两个 free() 操作注释掉,同时添加 --leak-check=full 参数,那么会报如下的错误。

==35580== 5 bytes in 1 blocks are definitely lost in loss record 1 of 1
==35580==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==35580==    by 0x400714: main (in /tmp/test/race)

非法访问

包括了非法读取以及非法写入,例如如下的示例。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char *buff;

    buff = (char *)malloc(5);
    if (buff == NULL)
        return -1;
    strcpy(buff, "01234");

    printf("buffer is '%s'\n", buff);
    free(buff);

    return 0;
}

其中的缓存只申请了 5 个字节,但是包括字符串结束符 \0 总共写入了 6 个字符,之所以报两个,估计是因为 strcpy() 正常只允许写入 4 个字节。

另外,在打印的时候,会多访问一个字符串的结束符。

==23247== Invalid write of size 2
==23247==    at 0x400771: main (in /tmp/test/race)
==23247==  Address 0x541f044 is 4 bytes inside a block of size 5 alloc'd
==23247==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==23247==    by 0x40075A: main (in /tmp/test/race)
==23247== 
==23247== Invalid read of size 1
==23247==    at 0x509EEF9: vfprintf (vfprintf.c:1635)
==23247==    by 0x50A5328: printf (printf.c:34)
==23247==    by 0x400780: main (in /tmp/test/race)
==23247==  Address 0x541f045 is 0 bytes after a block of size 5 alloc'd
==23247==    at 0x4C29BC3: malloc (vg_replace_malloc.c:299)
==23247==    by 0x40075A: main (in /tmp/test/race)
==23247== 

其它

另外几个常用的参数示例如下。

--show-reachable=yes
--trace-children=yes

--leak-check=<no|summary|yes|full>[default:summary]
  在退出时检查是否有泄漏,其中summary只显示有多少泄漏,而yes或full会打印每次泄漏的详细信息。
--error-exitcode=1
  当出现错误时,指定返回的错误码,对于一些根据退出码判断是否正常的工具有效。

helgrind

数据竞争 (Data Race) 是指在非线程安全的情况下,多线程对同一个地址空间进行写操作,一般会使用互斥锁或者读写锁进行保护,但是有可能会因为笔误或者设计缺陷仍然存在数据竞争的可能性。

如下的代码中,正常来说应该使用写锁,但是误写成了读锁。

#include <unistd.h>
#include <pthread.h>

static int racy;
static pthread_rwlock_t rwlock;

static void *thread_func(void *arg)
{
    (void) arg;
    pthread_rwlock_rdlock(&rwlock);
    racy++;
    pthread_rwlock_unlock(&rwlock);
    usleep(100 * 1000);

    return 0;
}

int main(void)
{
    pthread_t thread1;
    pthread_t thread2;

    pthread_rwlock_init(&rwlock, 0);
    pthread_create(&thread1, 0, thread_func, 0);
    pthread_create(&thread2, 0, thread_func, 0);

    pthread_join(thread1, 0);
    pthread_join(thread2, 0);
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

然后执行如下操作,其中输出是简化之后的结果。

$ gcc -o race race.c -O2 -rdynamic -lpthread
$ valgrind --tool=helgrind ./race
... ...
==11510== Possible data race during write of size 4 at 0x6010D8 by thread #3
==11510== Locks held: none
==11510==    at 0x400AD3: thread_func (in /tmp/test/race)
==11510==    by 0x4C3081E: mythread_wrapper (hg_intercepts.c:389)
==11510==    by 0x4E42DD4: start_thread (pthread_create.c:307)
==11510==    by 0x5154EAC: clone (clone.S:111)
==11510== 
==11510== This conflicts with a previous write of size 4 by thread #2
==11510== Locks held: none
==11510==    at 0x400AD3: thread_func (in /tmp/test/race)
==11510==    by 0x4C3081E: mythread_wrapper (hg_intercepts.c:389)
==11510==    by 0x4E42DD4: start_thread (pthread_create.c:307)
==11510==    by 0x5154EAC: clone (clone.S:111)
==11510==  Address 0x6010d8 is 0 bytes inside data symbol "racy"

如上标示了在地址 0x6010D8 处有 4 个字节存在数据竞争,分别有两个线程在尝试访问这一地址,而且没有持有锁;最后一行标示了这个变量是 racy 。

Suppression

可以通过 --gen-suppressions=all 参数生成 suppression 示例,然后略微编辑即可生成所需的 suppression 文件。

文件中的每个 suppression{} 开始结束,并由如下的行组成。

  • 第一行,对应的名称,可以通过这个名称对应 suppression
  • 第二行,所使用的工具,以及异常类型,例如 Memcheck:Leak
  • 第三行,如果第二行的工具需要参数,则在这行指定;
  • 剩余行,代表了上下文,类似于调用栈。

其中上下文可以指定函数 fun 或者动态库 obj ,可以通过 * 或者 ? 匹配任意字符,也可以使用 ... 匹配上下文中的一条。

{
   foobar
   Memcheck:Leak
   match-leak-kinds: possible
   ...
   fun:foobar
   ...
}
{
   g_type_register_static
   Memcheck:Cond
   obj:*
   fun:g_type_register_static
   ...
}

对于 Memcheck 来说,常用的关键字如下。

  • Value1, Value2, Value4, Value8, Value16:代表1-16字节的未初始化变量的使用
  • Cond (or its old name, Value0):表示未初始化的cpu条件变量
  • Addr1, Addr2, Addr4, Addr8, Addr16:表示1-16字节的不可addressable的内存访问
  • Jump:表示跳转到一个不可addressable的地方
  • Param:表示系统调用syscall的参数错误,这个类型需要另外一行指定syscall的那个参数
  • Free:表示不匹配的内存释放
  • Overlap:表示在memcpy时source和destination有重叠
  • Leak:表示内存泄漏