valgrind工具使用

1. 关于编译选项:

针对memcheck工具,需要注意以下几点:

a. 强烈推荐被调试的目标程序在编译时加入-g参数,这样再运行valgrind memcheck时,可以拿到更为丰富的调试信息,比如行号,调用栈等。

b. 当使用-O0编译目标程序时,valgrind可以保证输出的所有警告、错误提示信息都是准确的,副作用是程序运行会非常慢。

c. 当使用-O1编译目标程序时,valgrind可以保证程序运行速度相对较快,副作用是无法保证错误提示信息100%精确,比如,行号会不精确。(这点也很好理解,因为-O1下编译器会对指令进行重排)

d. valgrind不推荐使用-O2和-O3编译目标程序,此时错误提示信息可能会存在误报,比如,valgrind会报大量uninitialised-value错误,实际这些错误并不真实存在。

e. 在编译目标程序时添加-fno-inline,有利于valgrind生成更为精确的堆栈信息。(非必需)

    注意:如果不添加-fno-inline选项,也可以在运行valgrind时添加--read-inline-info=yes,这样valgrind会读取目标程序调试信息,使函数调用链能够正确显示。

f. 在编译目标程序时推荐使用-Wall,打印所有编译器告警信息。因为在较高的编译优化级别下,valgrind可能无法检测出全部编译期告警信息。

其他用于profile相关的工具,比如Cachegrind,通常不受编译优化选项的影响。在测试前,往往需要让程序在正常的编译优化选项下进行编译(比如-O2/-O3),之后再运行valgrind。

2. 关于输出信息:

为了说明valgrind的基本使用,以下是一个简单的示例(来自valgrind官网)。

 #include <stdlib.h>
 
 void f(void)
 {
     int* x = malloc(10 * sizeof(int));
     x[10] = 0;        // problem 1: heap block overrun
 }                    // problem 2: memory leak -- x not freed
 
 int main(void)
 {
     f();
     return 0;
 }
当运行valgrind时,程序会输出以下信息:

[adam040606@localhost test01]$ valgrind demo 
==17084== Memcheck, a memory error detector
==17084== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==17084== Using Valgrind-3.12.0 and LibVEX; rerun with -h for copyright info
==17084== Command: demo
==17084== 
==17084== Invalid write of size 4
==17084==    at 0x40054E: f (in /home/adam040606/Projects/valgrindTests/test01/demo)
==17084==    by 0x40055E: main (in /home/adam040606/Projects/valgrindTests/test01/demo)
==17084==  Address 0x51f7068 is 0 bytes after a block of size 40 alloc'd
==17084==    at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
==17084==    by 0x400541: f (in /home/adam040606/Projects/valgrindTests/test01/demo)
==17084==    by 0x40055E: main (in /home/adam040606/Projects/valgrindTests/test01/demo)
==17084== 
==17084== 
==17084== HEAP SUMMARY:
==17084==     in use at exit: 40 bytes in 1 blocks
==17084==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==17084== 
==17084== LEAK SUMMARY:
==17084==    definitely lost: 40 bytes in 1 blocks
==17084==    indirectly lost: 0 bytes in 0 blocks
==17084==      possibly lost: 0 bytes in 0 blocks
==17084==    still reachable: 0 bytes in 0 blocks
==17084==         suppressed: 0 bytes in 0 blocks
==17084== Rerun with --leak-check=full to see details of leaked memory
==17084== 
==17084== For counts of detected and suppressed errors, rerun with: -v
==17084== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
关于上面这个例子,有几点是值得关注的:

a) valgrind实际上提供了一组默认工具集。如果不指定具体运行哪个工具集,则默认运行memcheck。

    在上面的示例中,valgrind demo 实际等价于 valgrind --tool=memcheck demo。

b) 最左侧的数字 ——17084,实是PID。

c) Invalid write of size 4 

    说明了具体的错误类型,说明发生了写越界。

    由于目标程序采用-O0进行编译,可以看到出现错误时完整的堆栈信息。

d) definitely lost: 40 bytes in 1 blocks

    上述信息说明代码中存在明确的内存泄漏。

    如果在运行时添加了--leak-check=full,则会输出以下信息:

==17543== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==17543==    at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
==17543==    by 0x400541: f (in /home/adam040606/Projects/valgrindTests/test01/demo)
==17543==    by 0x40055E: main (in /home/adam040606/Projects/valgrindTests/test01/demo)
二、命令参数详解

一、输出信息重定向:

默认情况下,valgrind输出的信息会重定向到标准错误输出流(stderr,fd=2)。但有时我们往往需要将输出信息重定向到指定文件,有以下几种方式:

1. --log-fd=N:

    通过这种方式直接将输出信息重定向到fd=N的文件中。

2. --log-file=filename:

    将输出重定向到filename指向的文件。

3. --log-socket=host_ip:port:

    举例:--log-socket=192.168.0.1:18888 或者 --log-socket=192.168.0.1

    通过这种方式可以将输出重定向到一台远程主机。如果端口号未填写,则默认1500。对于这种方式,valgrind不支持使用host_name,只能使用host_ip。

    valgrind还支持添加listener,可以同时处理50个valgrind进程的输出。

二、屏蔽错误信息:

我们的程序一般都会依赖一些系统库或者C运行时库。在使用valgrind进行memcheck时,会产生大量关于这些运行时库的错误信息,而这些信息通常是我们无法处理的。

valgrind会在启动时,读取一个默认的suppresion file,屏蔽里面添加的错误。suppression file是在运行configure脚本时产生的。你可以根据自身项目需要,修改suppression file。

详情可以查阅官方文档2.5节Suppressing Error。

三、关键命令行选项:

1. Tool-selection Option:

    --tool=<toolname> [default: memcheck]

    目前valgrind可以使用的工具包括:memcheck、cachegrind,callgrind,helgrind,drd,massif,lackey,none,exp-sgcheck,exp-bbv,exp-dhat等。

2. Basic Options:

    以下只列出一些相关关键的命令行选项。

    a. -v, --verbose:

    输出更为详尽的提示信息,比如,所加载的共享对象的信息(the shared objects loaded)、输出屏蔽信息(the suppressions used)、注入(instrumentation)和执行(execution)引擎的工作过程,以及其他无用的告警。

    ===== 以下开关跟子进程相关 =====

    b. --trace-children=<yes|no> [default:no]:

    如果这个开关打开,valgrind将会跟踪当前进程创建的子进程。子进程必须通过exec()系统调用创建,目前valgrind无法对通过fork()创建出来的子进程进行跟踪。

    c. --trade-children-skip=patt1,patt2,... 

    当--trace-children=yes时,这个开关允许valgrind不对满足条件(patt1,patt2,....)的当前进程的子进程进行跟踪。这个也很好理解,因为被调试的进程可能会创建若干子进程,而我们并非关心全部子进程。

    补充说明:patt1,patt2实际上是进程的名字,支持?和*通配符。

    d. --child-silent-after-fork=<yes|no> [default:no]

    如果开启这个开关,valgrind不会输出任何由fork()创建出来的子进程。这个开关通常可以避免子进程输出信息混淆父进程输出信息。

     ===== 以下开关跟GDB相关 =====

    e. --vgdb=<no|yes|full> [default:yes]

    当--vgdb=yes或full时,valgrind将提供gdbserver的功能,允许GDB调试运行在valgrind之上的目标进程。(后面的章节会详细说明)

    f. --vgdb-error=<number> [default:999999999]

    当--vgdb=yes或full时,这个开关才能生效。valgrind检测到的error个数达到指定number时,将挂起目标程序,并允许用户通过GDB调试目标程序。

    如果number指定为0,在valgrind运行目标程序前,会先启动gdbserver。这个很有用,可以允许用户在目标程序启动前,向目标程序插入断点(breakpoints)。

    g. --vgdb-stop-at=<set> [default:none]

    当--vgdb=yes或full时,这个开关才能生效。通常,当valgrind检测到的错误数量达到--vgdb-error指定的错误数时,之后每触发一个错误,gdbserver都将会被调用(invoked)。

    通常,valgrind还允许用户在指定的事件发生时,触发针对gdbserver的调用。如下所示:

    *** 在目标程序启动(startup)、目标程序退出(exit),以及valgrind异常终止(valgrindabexit)时触发。

          注意:startup和--vgdb-error=0都会导致gdbserver在目标程序启动之前被调用。二者之间的不同点在于,--vgdb-error=0还会导致目标程序在启动之后每一个错误发生时,都触发gdbserver的调用。

    *** --vgdb-stop-at=all 等价于 --vgdb-stop-at=startup,exit,valgrindabexit

    *** --vgdb-stop-at=none (默认情况)

3. Erro-related Options:

    a. --demangle=<yes|no> [default:yes]

    C++中默认会对符号进行名称修饰(name mangling)。默认情况下--demangle=yes,valgrind会将符号还原为未经过名称修饰前的样子。

    b. --num-callers=<number> [default:12]

    这个参数主要指定调用链(call chain)嵌套的深度。默认情况下,valgrind能够识别的调用链嵌套深度为12,能够支持的最大值为500。当--num-caller=500时,valgrind运行速度会下降,内存占用也会上升。对于调用链嵌套很深的程序,这个选项比较有用。

    c. --sigill-diagnostics=<yes|no> [default:yes]

    这个参数主要用于在发生SIGILL信号时,valgrind是否输出必要的诊断信息。通常,使用valgrind调试目标程序时,当valgrind无法decode或translate一条特定的指令时,将产生SIGILL信号。产生SIGILL的根源有两种情况,第一,目标程序本身存在bug;第二,目标程序使用了valgrind无法识别的特殊指令。

    d. --show-below-main=<yes|no> [default:no]

    这个参数如果设置为yes,将会输出C/C++运行时库中检测到的错误。通常,我们并不需要关心运行时库中的错误。

    e. --fullpath-after=<string> [default:don't show source paths]

    默认情况下,valgrind在输出堆栈信息时(statck traceback)仅输出文件名。对于大型工程,不同模块的源代码处于不同目录,可能会带来不便。通过这个参数,用户可以指定源代码所在路径,这样在valgrind输出调用栈信息时,会输出文件所在路径信息。

    f. --extra-debuginfo-path=<path> [default:undefined and unused]

    在Linux发布版本中,系统库一般是不带调试信息的。确切说,二进制文件和调试信息本身是分离的。默认情况下,valgrind会扫描/usr/lib/debug目录,以查找系统库对应的默认调试信息。但是,有些情况下,用户依然系统自己指定二进制文件对应的调试信息目录,此时,这个命令行参数就能派上用场。

    g. --max-stackframe=<number> [default:2000000]

    指定堆栈上每一个栈帧(stack frame)大小上限。如果堆栈指针(stack pointer, esp/rsp)移动超过这个阈值,valgrind将假设程序将切换到另一个栈帧。

    一般情况下,如果一个程序会在堆栈上分配一个很大的数组,则在使用valgrind调试时,可能需要设置这个参数。当然,在栈上分配很大的数组通常不是一个好的做法,因为很容易将堆栈空间耗尽。另外,valgrind memcheck工具针对堆的内存检测,要比栈空间更高效。

    h. --main-stacksize=<number> [default:use current 'ulimit' value]

    指定主线程的堆栈大小。为了简化内存管理,valgrind在启动时,为主线程调用栈预留全部内存空间。这使得在启动时必须确定需要多大的栈空间。

    默认情况下,valgrind将使用ulimit中的stack size,或者16MB,作为默认值。

    注意:--main-stacksize和--main-stackframe时有差别的。前者代表当前线程整个堆栈需要多大的内存空间。后者代表堆栈上某一栈帧最大允许字节。

    i. --max-threads=<number> [default:500]

    默认情况下,valgrind可以同时处理最多500个线程。

4. Malloc-related Options:

    对于Valgrind中的Memcheck、Massif、Helgrind、DRD等工具,往往需要使用自己定义的malloc、realloc版本。因此,可能会涉及以下选项:

    a. --alignment=<number> [default:8 or 16,depends on the platform]

    对于Valgrind中使用的malloc、realloc,默认情况下,其分配的内存的起始地址为8字节或16字节对齐的。具体时8字节对齐,还是16字节对齐,跟具体的平台相关。该命令行选项允许用户指定一个不同的对齐方式,其中number必须必默认的对齐方式(即8字节或16字节)要大,并且必须小于等于4096。此外,还必须是2的倍数。

    b. --redzone-size=<number> [default:depends on tool]

    Valgrind中使用的malloc和realloc,往往会在每一个从堆(Heap)上分配的bock之前及之后,插入padding block。这些padding block也被称为red zone。Red zone的大小默认16字节。可见,默认情况下,valgrind能够检测出当前block向前及向后16字节内内存读写错误。

    本选项可以设置padding block的大小。将该配置设置得过大,会消耗更多的内存。

四、关于多线程:

    对于多线程程序,valgrind实际上会将所有线程的执行串行化。也就是说,即使运行环境拥有多CPU或者多核,对于运行在valgrind之上的(多线程)程序,同时只会有一个线程运行,即同时只会占据CPU的一个核。这样做主要是为了简化valgrind设计,避免因考虑多线程而引入不必要的设计上的复杂度。

    Valgrind实际上并不直接调度线程。仅仅是通过一种简单的锁机制,来确保同时只有一个线程运行。真正的线程调度还是由操作系统内核控制。因此,目标程序运行在Valgrind下,同直接运行在操作系统之上相比,将会有截然不同的行为。因为Valgrind将目标程序串行化,因此目标程序运行于Valgrind之上时,将会比正常运行慢很多。

    关于Valgrind线程调度及多线程性能

    线程占据CPU执行之前,首先必须获取锁。执行完一定数量指令之后,运行线程释放锁。其他就绪线程将继续争抢锁。

    命令行选项 --fair-sched 用来设置valgrind使用的锁机制,用来串行化目标程序中所有线程。

    默认情况下,--fair-sched=no,此时valgrind采用基于Pipe的锁机制,适用于所有平台。这种机制不保证线程之间公平性,极有可能出现一个运行线程释放锁后立马又获取锁,即使此刻其他线程已经就绪。使用这种调度策略,目标程序每次运行都将产生截然不同的行为。

    当 --fair-sched=yes 或 --fair-sched=try时,valgrind将采用基于mutex的锁机制,仅适用于部分平台。基于mutex的锁机制可以确保线程间公平调度(Round-Robin,轮询),如果多个线程同时就绪,则锁将分配给第一个就绪的线程。注意,阻塞在系统调用的线程不能请求占用锁,只有从系统调用唤醒后,才能请求占用锁
 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值