NS编程与调试 - 内存调试工具 valgrind

时间:2022-03-14 10:51:24

转自:http://blog.chinaunix.net/uid-21389973-id-1827286.html

linux下面用c++写代码,在所难免会遇到segmentation fault (段错误)。个人在编写ns扩展模块时候,遇到过很多段错误,虽然运行时刻经常由程序抛出段错误,但是段错误的发生的程序级别的原因多种多样,不过归结到 系统级别上,段错误都是由于内存原因引起的(个人总结)。

会造成内存错误的程序级别的原因,也就是我们程序员所经常犯的错误大致可以分为以下几个方面:
1,使用未初始化的指针 - 这是必然的,指针指空的东西,必然出错。
2,重复删除一个指针 - 必然,再次删除就会删除一个已经分配给别的程序的数据或者其他。
3,内存冲突 - 由于程序声明的两块数据区域重叠,造成混乱。
4,混杂的指针错误 - 只能具体问题具体分析,情况比较复杂。

对于一位刚开始用c++在linux编程的人来说,最常遇到的应该的就是1与2了。当工程规模比较大,程序由小组完成而后整合等的情况下,很容易出现2,3,4的情况。这时候的调试比较麻烦,也需要很多耐心。

我 在做的wimax mesh的项目就是这样。对于一个timer的使用,没有初始化,造成的段错误,一目了然。工程进展非常顺利。当工程做到50%时候(11.08号),遇 到了一个段错误,结果调试到12.02号才调出来!我就来说一下我的调试历程吧!真是一波三折阿!开始的时候以为是1或者2的情况,反复检查,不是这样。 然后怀疑3或者4,结果由于没有使用任何工具,只是在代码中加打印信息,这时候只能把错误定位到transmit(p)这个函数上。但是这个函数我只写了 一行,就是
transmit(p)
{
     downtarget_->recv(p,(Handle*)NULL);
}
程序在这个地方出错实在让人摸不到头绪,因为再往下执行就不是我代码的问题了,而是下层已有代码甚至是系统代码的问题阿!非常困扰!
然后开始用gdb调试,gdb是一个很好的很强大的调试工具,我用的命令行的,所能完成的功能和vc下的调试工具差不多,只是需要看什么变量就是要用print ×××来看罢了,不过功能决不比它差。但是我用了gdb只能把错误定位在:
(gdb) bt
#0 0x082d16ba in CheckChannelErrors ()
#1 0x082d43a5 in Tcl_Write ()
#2 0x081cdb52 in BaseTrace::namdump (this=0x90e09d0) at trace/basetrace.cc:109
#3 0x08137aa4 in CMUTrace::nam_format (this=0x90e10b0, p=0x90fb860, offset=64) at trace/cmu-trace.cc:1123
#4 0x081384db in CMUTrace::format (this=0x90e10b0, p=0x90fb860, why=0x833ec34 "---") at trace/cmu-trace.cc:1137
#5 0x08138751 in CMUTrace::recv (this=0x90e10b0, p=0x90fb860, h=0x0) at trace/cmu-trace.cc:1239
#6 0x0821e9e7 in Mac802_16::mac_log (this=0x90d1f08, p=0x90fb860) at wimax/mac802_16.h:909
#7 0x082268e6 in Mac802_16MSS::receive (this=0x90d1f08) at wimax/mac802_16MSS.cc:660
#8 0x08228b08 in WimaxRxTimer::handle (this=0x90d29a4, e=0x90d29b4) at wimax/mac802_16timer.cc:98
#9 0x08054332 in Scheduler::dispatch (this=0x8eb6a10, p=0x90d29b4, t=10.017116268588) at common/scheduler.cc:150
#10 0x0805457e in Scheduler::run (this=0x8eb6a10) at common/scheduler.cc:129
#11 0x0805485d in Scheduler::command (this=0x8eb6a10, argc=2, argv=0xbfa20730) at common/scheduler.cc:198
#12 0x0828b7ae in TclClass::dispatch_cmd ()
#13 0x082903d0 in OTclDispatch (cd=<value optimized out>, in=0x8e97250, argc=3, argv=0xbfa2077c) at otcl.c:434
#14 0x0829712e in TclInvokeStringCommand ()
#15 0x08298c4b in TclEvalObjvInternal ()
#16 0x082c2ef1 in TclExecuteByteCode ()
#17 0x082c6d1c in TclCompEvalObj ()
#18 0x082c2fca in TclExecuteByteCode ()
#19 0x082c6d1c in TclCompEvalObj ()
#20 0x082eec76 in TclObjInterpProc ()
#21 0x082ef092 in TclProcInterpProc ()
#22 0x08290520 in OTclDispatch (cd=<value optimized out>, in=0x8e97250, argc=2, argv=0xbfa2131c) at otcl.c:477
#23 0x0829712e in TclInvokeStringCommand ()
#24 0x08298c4b in TclEvalObjvInternal ()
#25 0x082c2ef1 in TclExecuteByteCode ()
#26 0x082c6d1c in TclCompEvalObj ()
#27 0x082eec76 in TclObjInterpProc ()
#28 0x082ef092 in TclProcInterpProc ()
#29 0x082903d0 in OTclDispatch (cd=<value optimized out>, in=0x8e97250, argc=2, argv=0xbfa21c2c) at otcl.c:434
#30 0x0829712e in TclInvokeStringCommand ()
#31 0x08298c4b in TclEvalObjvInternal ()
#32 0x08299207 in Tcl_EvalEx ()
#33 0x082ded12 in Tcl_FSEvalFile ()
#34 0x082e2307 in Tcl_Main ()
#35 0x0804cafc in main (argc=2747804, argv=0x178b62) at common/tclAppInit.cc:67

这让我更加不解,为什么会在调用ns原有代码的地方出问题呢???后来决定再换一个工具。用valgrind

在这里我先贴一下valgrind的用法,我自己搜到的,很详细的。姑且就不自己写了。
============================================

Valgrind

Valgrind 已经在 Linux 应用程序开发社区中广泛用来调试应用程序。它尤其擅长发现内存管理的问题。它可以检查程序运行时的内存泄漏问题。这个工具目前正由 Julian Seward 进行开发,并由 Paul Mackerras 移植到了 Power 架构上。

要安装 Valgrind,请从 Valgrind 的 Web 站点上下载源代码(参阅 参考资料)。切换到 Valgrind 目录,并执行下面的命令:

# make
# make check
# make install

堆栈分析器,它能测量程序在堆栈中使用了多少内存,告诉我们堆块,堆管理块和栈的大小。Massif能帮助我们减少内存的使用,在带有虚拟内存的现代系统中,它还能够加速我们程序的运行,减少程序停留在交换区中的几率。

Valgrind 安装

1、 到www.valgrind.org下载最新版valgrind-3.2.3.tar.bz2
2、 解压安装包:tar –jxvf valgrind-3.2.3.tar.bz2
3、 解压后生成目录valgrind-3.2.3
4、 cd valgrind-3.2.3
5、 ./configure
6、 Make;make install

Valgrind 使用

用法: valgrind [options] prog-and-args [options]: 常用选项,适用于所有Valgrind工具

  1. -tool=<name> 最常用的选项。运行 valgrind中名为toolname的工具。默认memcheck。
  2. h –help 显示帮助信息。
  3. -version 显示valgrind内核的版本,每个工具都有各自的版本。
  4. q –quiet 安静地运行,只打印错误信息。
  5. v –verbose 更详细的信息, 增加错误数统计。
  6. -trace-children=no|yes 跟踪子线程? [no]
  7. -track-fds=no|yes 跟踪打开的文件描述?[no]
  8. -time-stamp=no|yes 增加时间戳到LOG信息? [no]
  9. -log-fd=<number> 输出LOG到描述符文件 [2=stderr]
  10. -log-file=<file> 将输出的信息写入到filename.PID的文件里,PID是运行程序的进行ID
  11. -log-file-exactly=<file> 输出LOG信息到 file
  12. -log-file-qualifier=<VAR> 取得环境变量的值来做为输出信息的文件名。 [none]
  13. -log-socket=ipaddr:port 输出LOG到socket ,ipaddr:port

LOG信息输出

  1. -xml=yes 将信息以xml格式输出,只有memcheck可用
  2. -num-callers=<number> show <number> callers in stack traces [12]
  3. -error-limit=no|yes 如果太多错误,则停止显示新错误? [yes]
  4. -error-exitcode=<number> 如果发现错误则返回错误代码 [0=disable]
  5. -db-attach=no|yes 当出现错误,valgrind会自动启动调试器gdb。[no]
  6. -db-command=<command> 启动调试器的命令行选项[gdb -nw %f %p]

适用于Memcheck工具的相关选项:

  1. -leak-check=no|summary|full 要求对leak给出详细信息? [summary]
  2. -leak-resolution=low|med|high how much bt merging in leak check [low]
  3. -show-reachable=no|yes show reachable blocks in leak check? [no]

Valgrind 的错误报告

Valgrind 的输出格式如下:


清单 1. Valgrind 的输出消息


# valgrind du –x –s
.
.
==29404== Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd
==29404== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==29404== by 0xFEE1AD0: strdup (in /lib/tls/libc.so.6)
==29404== by 0xFE94D30: setlocale (in /lib/tls/libc.so.6)
==29404== by 0x10001414: main (in /usr/bin/du)

==29404== 是进程的 ID。消息 Address 0x1189AD84 is 0 bytes after a block of size 12 alloc'd 说明在这个 12 字节的数组后面没有存储空间了。第二行以及后续几行说明内存是在 130 行(vg_replace_malloc.c)的 strdup() 程序中进行分配的。strdup() 是在 libc.so.6 库的 setlocale() 中调用的;main() 调用了 setlocale()

未初始化的内存

最为常见的一个 bug 是程序使用了未初始化的内存。未初始化的数据可能来源于:

  • 未经初始化的变量
  • malloc 函数所分配的数据,在写入值之前使用了

下面这个例子使用了一个未初始化的数组:


清单 2. 使用未初始化的内存


2 {
3 int i[5];
4
5 if (i[0] == 0)
6 i[1]=1;
7 return 0;
8 }

在这个例子中,整数数组 i[5] 没有进行初始化;因此,i[0] 包含的是一个随机数。因此使用 i[0] 的值来判断一个条件分支就会导致不可预期的问题。Valgrind 可以很容易捕获这种错误条件。当您使用 Valgrind 运行这个程序时,就会接收到下面的消息:


清单 3. Valgrind 的输出消息


# gcc –g –o test1 test1.c
# valgrind ./test1
.
.
==31363==
==31363== Conditional jump or move depends on uninitialised value(s)
==31363== at 0x1000041C: main (test1.c:5)
==31363==
==31363== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
==31363== malloc/free: in use at exit: 0 bytes in 0 blocks.
==31363== malloc/free: 0 allocs, 0 frees, 0 bytes allocated.
==31363== For counts of detected errors, rerun with: -v
==31363== No malloc'd blocks -- no leaks are possible.

Valgrind 的输出说明,有一个条件分支依赖于文件 test1.c 中第 5 行中的一个未初始化的变量。

内存泄漏

内 存泄漏是另外一个常见的问题,也是很多程序中最难判断的问题。内存泄漏的主要表现为:当程序连续运行时,与程序相关的内存(或堆)变得越来越大。结果是, 当这个程序所消耗的内存达到系统的上限时,就会自己崩溃;或者会出现更严重的情况:挂起或导致系统崩溃。下面是一个有内存泄漏 bug 的示例程序:


清单 4. 内存泄漏示例


1 int main(void)
2 {
3 char *p1;
4 char *p2;
5
6 p1 = (char *) malloc(512);
7 p2 = (char *) malloc(512);
8
9 p1=p2;
10
11 free(p1);
12 free(p2);
13 }

上面的代码分别给字符指针 p1 和 p2 分配了两个 512 字节的内存块,然后将指向第一个内存块的指针设置为指向第二个内存块。结果是,第二个内存块的地址丢失了,并导致内存泄漏。在使用 Valgrind 运行这个程序时,会返回如下的消息:


清单 5. Valgrind 的输出消息


# gcc –g –o test2 test2.c
# valgrind ./test2
.
.
==31468== Invalid free() / delete / delete[]
==31468== at 0xFFB9FF0: free (vg_replace_malloc.c:152)
==31468== by 0x100004B0: main (test2.c:12)
==31468== Address 0x11899258 is 0 bytes inside a block of size 512 free'd
==31468== at 0xFFB9FF0: free (vg_replace_malloc.c:152)
==31468== by 0x100004A4: main (test2.c:11)
==31468==
==31468== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 7 from 1)
==31468== malloc/free: in use at exit: 512 bytes in 1 blocks.
==31468== malloc/free: 2 allocs, 2 frees, 1024 bytes allocated.
==31468== For counts of detected errors, rerun with: -v
==31468== searching for pointers to 1 not-freed blocks.
==31468== checked 167936 bytes.
==31468==
==31468== LEAK SUMMARY:
==31468== definitely lost: 512 bytes in 1 blocks.
==31468== possibly lost: 0 bytes in 0 blocks.
==31468== still reachable: 0 bytes in 0 blocks.
==31468== suppressed: 0 bytes in 0 blocks.
==31468== Use --leak-check=full to see details of leaked memory.

正如您可以看到的一样,Valgrind 报告说这个程序中有 512 字节的内存丢失了。

非法写/读

这种情况发生在程序试图对一个不属于程序本身的内存地址进行读写时。在有些系统上,在发生这种错误时,程序会异常结束,并产生一个段错误。下面这个例子就是一个常见的 bug,它试图读写一个超出数组边界的元素。


清单 6. 非法读写


1 int main() {
2 int i, *iw, *ir;
3
4 iw = (int *)malloc(10*sizeof(int));
5 ir = (int *)malloc(10*sizeof(int));
6
7
8 for (i=0; i<11; i++)
9 iw[i] = i;
10
11 for (i=0; i<11; i++)
12 ir[i] = iw[i];
13
14 free(iw);
15 free(ir);
16 }

从这个程序中我们可以看出,对于 iw[10] 和 ir[10] 的访问都是非法的,因为 iw 和 ir 都只有 10 个元素,分别是从 0 到 9。请注意 int iw[10 ] 和 iw = (int *)malloc(10*sizeof(int)) 是等效的 —— 它们都是用来给一个整数数组 iw 分配 10 个元素。

当您使用 Valgrind 运行这个程序时,会返回如下的消息:


清单 7. Valgrind 的输出消息


# gcc –g –o test3 test3.c
# valgrind ./test3
.
.
==31522== Invalid write of size 4
==31522== at 0x100004C0: main (test3.c:9)
==31522== Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
==31522== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==31522== by 0x10000474: main (test10.c:4)
==31522==
==31522== Invalid read of size 4
==31522== at 0x1000050C: main (test3.c:12)
==31522== Address 0x11899050 is 0 bytes after a block of size 40 alloc'd
==31522== at 0xFFB9964: malloc (vg_replace_malloc.c:130)
==31522== by 0x10000474: main (test10.c:4)
==31522==
==31522== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 7 from 1)
==31522== malloc/free: in use at exit: 0 bytes in 0 blocks.
==31522== malloc/free: 2 allocs, 2 frees, 84 bytes allocated.
==31522== For counts of detected errors, rerun with: -v
==31522== No malloc'd blocks -- no leaks are possible.

在 test3.c 的第 9 行发现一个非法的 4 字节写操作,在第 12 行发现一个非法的 4 字节读操作。

Valgrind 也可以帮助判断内存误用的问题,例如:

  • 读/写已经释放的内存
  • C++ 环境中错误地使用 malloc/new 与 free/delete 的配对

下面这个列表介绍了 POWER 架构上 Valgrind 的状态:

  • memcheck 和 addrcheck 工具都可以很好地工作。然而,其他工具还没有进行大量的测试。另外,Helgrind (一个数据竞争的检测程序)在 POWER 上尚不能使用。
  • 所有的 32 位 PowerPC? 用户模式的指令都可以支持,除了两条非常少用的指令:lswx 和 stswx。具体来说,所有的浮点和 Altivec(VMX)指令都可以支持。
  • Valgrind 可以在 32 位或 64 位 PowerPC/Linux 内核上工作,但是只能用于 32 位的可执行程序。

有关 Valgrind 内存调试的更多信息,请访问 Valgrind HOW TO 站点。还可以参阅 Steve Best 的“Debugging Memory Problems”(Linux Magazine,2003 年 5 月)。参考资料 中有它们的链接

除了 Valgrind 之外,还可以使用其他几个内存调试工具;例如,Memwatch 和 Electric Fence。

=============================================

上述的例子足以应付我们日常遇到的内存错误。那么我来说说我是怎么用valgrind调试我的程序的。

用命令:
[tengda@localhost tcl]$ valgrind --tool=memcheck --leak-check=full --db-attach=yes ns sss.tcl 
==26507== Memcheck, a memory error detector.
==26507== Copyright (C) 2002-2007, and GNU GPL'd, by Julian Seward et al.
==26507== Using LibVEX rev 1732, a library for dynamic binary translation.
==26507== Copyright (C) 2004-2007, and GNU GPL'd, by OpenWorks LLP.
==26507== Using valgrind-3.2.3, a dynamic binary instrumentation framework.
==26507== Copyright (C) 2000-2007, and GNU GPL'd, by Julian Seward et al.
==26507== For more details, rerun with: -v
==26507==

然后出现调试信息:
Mac 2 has transmit a packet at 10.015894
==26507== 
==26507== Invalid read of size 4
==26507==    at 0x82D439B: Tcl_Write (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x81CDB51: BaseTrace::namdump() (basetrace.cc:109)
==26507==    by 0x813768E: CMUTrace::nam_format(Packet*, int) (cmu-trace.cc:1034)
==26507==    by 0x81384DA: CMUTrace::format(Packet*, char const*) (cmu-trace.cc:1137)
==26507==    by 0x8138750: CMUTrace::recv(Packet*, Handler*) (cmu-trace.cc:1239)
==26507==    by 0x822513C: Mac802_16MSS::transmit(Packet*) (mac802_16MSS.cc:569)
==26507==    by 0x82149B2: Mac802_16::send_ncfg() (mac802_16.cc:1086)
==26507==    by 0x8228EFB: NcfgTimer::expire(Event*) (mac802_16timer.cc:418)
==26507==    by 0x8052A2C: TimerHandler::handle(Event*) (timer-handler.cc:99)
==26507==    by 0x8054331: Scheduler::dispatch(Event*, double) (scheduler.cc:150)
==26507==    by 0x805457D: Scheduler::run() (scheduler.cc:129)
==26507==    by 0x805485C: Scheduler::command(int, char const* const*) (scheduler.cc:198)
==26507== Address 0x54D4540 is 0 bytes inside a block of size 28 free'd
==26507==    at 0x40050FF: free (vg_replace_malloc.c:233)
==26507==    by 0x83125AC: TclpFree (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x829DBDC: Tcl_Free (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82EDFB4: Tcl_EventuallyFree (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82D4094: FlushChannel (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82D3B9C: Tcl_Close (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82D57D3: Tcl_UnregisterChannel (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82D8C90: Tcl_CloseObjCmd (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x8298C4A: TclEvalObjvInternal (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82C2EF0: TclExecuteByteCode (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82C6D1B: TclCompEvalObj (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507==    by 0x82EEC75: TclObjInterpProc (in /home/tengda/ns-allinone-2.31/ns-2.31/ns)
==26507== 
==26507== ---- Attach to debugger ? --- [Return/N/n/Y/y/C/c] ----

程 序一直在10s时候出现非法读4字节,只到最后偏移出缓冲区,彻底down掉。对于非法读,无非就是非法读缓冲,读共享变量,读文件。前两种我排除了。应 该是非法读文件,于是我检查nam文件。发现在10s时候写入一条以w开头的行,ns中nam文件的开始和结束是以w开始的行标记的,关于传输数据的行是 以+,-,h开头的,那么就是在10s时候关闭了nam文件。于是我开始检查tcl测试脚本,果然,我的测试时间是200s,我在10s关闭了nam文 件,当我把10s改为200s时候,程序正常了!!!

gdb与valgrind配合使用效果非常好阿!