1、简介调试程序有很多方法,例如向屏幕上打印消息,使用调试器,或者只需仔细考虑程序如何运行,并对问题进行有根有据的猜测。在修复 bug 之前,首先要确定在源程序中的位置。例如,当一个程序产生崩溃或生成核心转储(core dump)时,您就需要了解是哪行代码发生了崩溃。在找到有问题的代码行之后,就可以确定这个函数中变量的值,函数是如何调用的,更具体点说,为什么会发 生这种错误。使用调试器查找这些信息非常简单。本文将简要介绍几种用于修复一些很难通过可视化地检查代码而发现的 bug 的技术,并阐述了如何使用在 Linux on Power 架构上可用的工具。回页首调试内存问题的工具和技术动态内存分配看起
2、来似乎非常简单:您可以根据需要分配内存 使用 malloc() 或其变种 并在不需要时释放这些内存。实际上,内存管理的问题是软件中最为常见的 bug,因为通常在程序启动时这些问题并不明显。例如,程序中的内存泄漏可能开始并不为人注意,直到经过多天甚至几个月的运行才会被发现。接下来的几节将 简要介绍如何使用流行的调试器 Valgrind 来发现并调试这些最常见的内存 bug。在开始使用任何调试工具之前,请考虑这个工具是否对重新编译应用程序有益,是否可以支持具有调试信息的库(-g 选项)。如果没有启用调试信息,调试工具可以做的最好的事情也不过是猜测一段特定的代码是属于哪个函数的。这使得错误消息和概要
3、分析输出几乎没有什么用处。使用 -g 选项,您就有可能获得一些信息来直接指出相关的代码行。ValgrindValgrind 已经在 Linux 应用程序开发社区中广泛用来调试应用程序。它尤其擅长发现内存管理的问题。它可以检查程序运行时的内存泄漏问题。这个工具目前正由 Julian Seward 进行开发,并由 Paul Mackerras 移植到了 Power 架构上。要安装 Valgrind,请从 Valgrind 的 Web 站点上下载源代码(参阅 参考资料)。切换到 Valgrind 目录,并执行下面的命令:# make# make check# make installValgrind
4、 的错误报告Valgrind 的输出格式如下:清单 1. Valgrind 的输出消息# valgrind du x s.=29404= Address 0x1189AD84 is 0 bytes after a block of size 12 allocd=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= b
5、y 0x10001414: main (in /usr/bin/du)=29404= 是进程的 ID。消息 Address 0x1189AD84 is 0 bytes after a block of size 12 allocd 说明在这个 12 字节的数组后面没有存储空间了。第二行以及后续几行说明内存是在 130 行(vg_replace_malloc.c)的 strdup() 程序中进行分配的。strdup() 是在 libc.so.6 库的 setlocale() 中调用的;main() 调用了 setlocale()。未初始化的内存最为常见的一个 bug 是程序使用了未初始化的内存。
6、未初始化的数据可能来源于: 未经初始化的变量 malloc 函数所分配的数据,在写入值之前使用了下面这个例子使用了一个未初始化的数组:清单 2. 使用未初始化的内存2 3 int i5;4 5 if (i0 = 0)6 i1=1;7 return 0;8 在这个例子中,整数数组 i5 没有进行初始化;因此,i0 包含的是一个随机数。因此使用 i0 的值来判断一个条件分支就会导致不可预期的问题。Valgrind 可以很容易捕获这种错误条件。当您使用 Valgrind 运行这个程序时,就会接收到下面的消息:清单 3. Valgrind 的输出消息# gcc g o test1 test1.c# v
7、algrind ./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 fre
8、es, 0 bytes allocated.=31363= For counts of detected errors, rerun with: -v=31363= No mallocd blocks - no leaks are possible.Valgrind 的输出说明,有一个条件分支依赖于文件 test1.c 中第 5 行中的一个未初始化的变量。内存泄漏内存泄漏是另外一个常见的问题,也是很多程序中最难判断的问题。内存泄漏的主要表现为:当程序连续运行时,与程序相关的内 存(或堆)变得越来越大。结果是,当这个程序所消耗的内存达到系统的上限时,就会自己崩溃;或者会出现更严重的情况:挂起或导
9、致系统崩溃。下面是一个有内 存泄漏 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 运行这个程序时,会返回如下的消息:清单
10、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 freed=31468= at 0xFFB9FF0: free (vg_replace_
11、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
12、: -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= Us
13、e -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(1
14、0*sizeof(int);6 7 8 for (i=0; i - 附加到进程 process_id 上。process_id 可以使用 ps 命令找到info thread 显示当前正在运行的线程 ? thread apply threadno command 对一个线程运行 gdb 命令 thread apply 3 where - 对线程 3 运行 where 命令Thread threadno 选择一个线程作为当前线程 ? 如果一个程序崩溃了,并生成了一个 core 文件,您可以查看 core 文件来判断进程结束时的状态。使用下面的命令启动 gdb:# gdb programname
15、corefilename 要调试一个 core 文件,您需要可执行程序、源代码文件以及 core 文件。要对一个 core 文件启动 gdb,请使用 -c 选项:# gdb -c core programname gdb 会显示是哪行代码导致这个程序产生了核心转储。默认情况下,核心转储在 Novell 的 SUSE LINUX Enterprise Server 9(SLES 9)和 Red Hat? Enterprise Linux Advanced Server(RHEL AS 4)上都是禁用的。要启用核心转储,请以 root 用户的身份在命令行中执行 ulimit c unlimited
16、。清单 8 中的例子阐述了如何使用 gdb 来定位程序中的 bug。 清单 8 是一段包含 bug 的 C+ 代码。清单 8 中的 C+ 程序试图构建 10 个链接在一起的数字框(number box),例如:图 1. 一个包含 10 个链接在一起的数字框的列表然后试图从这个列表中逐个删除数字框。编译并运行这个程序,如下所示:清单 9. 编译并运行这个程序# g+ -g -o gdbtest1 gdbtest1.cpp# ./gdbtest1Number Box “0“ createdNumber Box “1“ createdNumber Box “2“ createdNumber Box
17、“3“ createdNumber Box “4“ createdNumber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdNumber Box “9“ deletedSegmentation fault正如您可以看到的一样,这个程序会导致段错误。调用 gdb 来看一下这个问题,如下所示:清单 10. 调用 gdb# gdb ./gdbtest1GNU gdb 6.2.1Copyright 2004 Free
18、Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.Type “show copying“ to see the conditions.There is absolutely no warranty for GDB. Type “show warranty“ for details.This
19、GDB was configured as “ppc-suse-linux“.Using host libthread_db library “/lib/tls/libthread_db.so.1“.(gdb)您知道段错误是在数字框 “9“ 被删除之后发生的。执行 run 和 where 命令来精确定位段错误发生在程序中的什么位置。清单 11. 执行 run 和 where 命令(gdb) runStarting program: /root/test/gdbtest1 Number Box “0“ createdNumber Box “1“ createdNumber Box “2“ cre
20、atedNumber Box “3“ createdNumber Box “4“ createdNumber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdNumber Box “9“ deletedProgram received signal SIGSEGV, Segmentation fault.0x10000f74 in NumBox:GetNext (this=0x0) at gdbtest1.cpp:
21、1414 NumBox*GetNext() const return Next; (gdb) where#0 0x10000f74 in NumBox:GetNext (this=0x0) at gdbtest1.cpp:14#1 0x10000d10 in NumChain:RemoveBox (this=0x10012008, item_to_remove=0xffffe200) at gdbtest1.cpp:63#2 0x10000978 in main (argc=1, argv=0xffffe554) at gdbtest1.cpp:94(gdb)跟踪信息显示这个程序在第 14 行
22、 NumBox:GetNext (this=0x0) 接收到一个段错误。这个数字框上 Next 指针的地址是 0x0,这对于一个数字框来说是一个无效的地址。从上面的跟踪信息可以看出,GetNext 函数是由 63 行调用的。看一下在 gdbtest1.cpp 的 63 行附近发生了什么:清单 12. gdbtest1.cpp54 else 55 temp-SetNext (current-GetNext();56 delete temp;57 temp = 0;58 return 0;59 60 61 current = 0;62 temp = current;63 current = cur
23、rent-GetNext();64 65 66 return -1;第 61 行 current=0 将这个指针设置为一个无效的地址,这正是产生段错误的根源。注释掉第 61 行,将其保存为 gdbtest2.cpp,然后编译并重新运行。清单 13. 再次运行程序(gdbtest2.cpp)# g+ -g -o gdbtest2 gdbtest2.cpp# ./gdbtest2Number Box “0“ createdNumber Box “1“ createdNumber Box “2“ createdNumber Box “3“ createdNumber Box “4“ createdN
24、umber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdNumber Box “9“ deletedNumber Box “0“ deleted这个程序现在可以成功完成而不会出现段错误了。然而,结果并不像我们预期的一样:程序在删除 Number Box “9“之后删除了 Number Box “0“,而不像我们期望的一样删除 Number Box “8,“。使用 gdb 再次来看一下。清单 14. 再次使用 g
25、db 进行查看# gdb ./gdbtest2GNU gdb 6.2.1Copyright 2004 Free Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.Type “show copying“ to see the conditions.There is absolutely no
26、warranty for GDB. Type “show warranty“ for details.This GDB was configured as “ppc-suse-linux“.Using host libthread_db library “/lib/tls/libthread_db.so.1“.(gdb) break 94 if i=8Breakpoint 1 at 0x10000968: file gdbtest2.cpp, line 94.(gdb) runStarting program: /root/test/gdbtest2 Number Box “0“ create
27、dNumber Box “1“ createdNumber Box “2“ createdNumber Box “3“ createdNumber Box “4“ createdNumber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdNumber Box “9“ deletedBreakpoint 1, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:9494 l
28、ist -RemoveBox(i);您可能希望找出为什么这个程序删除的是 Number Box 0,而不是 Number Box 8,因此需要在您认为程序会删除 Number Box 8 的地方停止程序。设置这个断点:break 94 if i=8,可以在 i 等于 8 时在第 94 行处停止程序。然后单步跟踪到 RemoveBox() 函数中。清单 15. 单步跟踪到 RemoveBox() 函数中(gdb) s38 NumBox *temp = 0; (gdb) s40 while (current != 0) (gdb) print pointer$1 = (NumBox *) 0x10
29、0120a8(gdb) print *pointer$2 = Num = 0, Next = 0x0(gdb)指针早已指向了 Number Box “0“,因此这个 bug 可能就存在于程序删除 Number Box “9“ 的地方。要在 gdb 中重新启动这个程序,请使用 kill 删除原来的断点,然后添加一个 i 等于 9 时的新断点,然后再次运行这个程序。清单 16. 在 gdb 中重新启动程序(gdb) killKill the program being debugged? (y or n) y(gdb) info breakNum Type Disp Enb Address Wha
30、t1 breakpoint keep y 0x10000968 in main at gdbtest2.cpp:94stop only if i = 8breakpoint already hit 1 time(gdb) delete 1(gdb) break 94 if i=9Breakpoint 2 at 0x10000968: file gdbtest2.cpp, line 94.(gdb) runStarting program: /root/test/gdbtest2 Number Box “0“ createdNumber Box “1“ createdNumber Box “2“
31、 createdNumber Box “3“ createdNumber Box “4“ createdNumber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdBreakpoint 2, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:9494 list -RemoveBox(i);(gdb)当这一次单步跟踪 RemoveBox() 函数时,要特别注意 list-
32、pointer 正在指向哪一个数字框,因为 bug 可能就在于 list-pointer 开始指向 Number Box “0“ 的地方。请使用 display *pointer 命令来查看,这会自动显示这个函数。清单 17. 使用 display *pointer 命令进行监视Breakpoint 2, main (argc=1, argv=0xffffe554) at gdbtest2.cpp:9494 list -RemoveBox(i);(gdb) sNumChain:RemoveBox (this=0x10012008, item_to_remove=0xffffe200) at g
33、dbtest2.cpp:3737 NumBox *current = pointer;(gdb) display *pointer1: *this-pointer = Num = 9, Next = 0x10012098(gdb) s38 NumBox *temp = 0; 1: *this-pointer = Num = 9, Next = 0x10012098(gdb) s40 while (current != 0) 1: *this-pointer = Num = 9, Next = 0x10012098(gdb) s41 if (current-GetValue() = item_t
34、o_remove) 1: *this-pointer = Num = 9, Next = 0x10012098(gdb) sNumBox:GetValue (this=0x100120a8) at gdbtest2.cpp:1616 const T (gdb) sNumChain:RemoveBox (this=0x10012008, item_to_remove=0xffffe200) at gdbtest2.cpp:4242 if (temp = 0) 1: *this-pointer = Num = 9, Next = 0x10012098(gdb) s44 if (current-Ge
35、tNext() = 0) 1: *this-pointer = Num = 9, Next = 0x10012098(gdb) sNumBox:GetNext (this=0x100120a8) at gdbtest2.cpp:1414 NumBox*GetNext() const return Next; (gdb) sNumChain:RemoveBox (this=0x10012008, item_to_remove=0xffffe200) at gdbtest2.cpp:5050 delete current;1: *this-pointer = Num = 9, Next = 0x1
36、0012098(gdb) sNumBox (this=0x100120a8) at gdbtest2.cpp:1010 std:cout :GetValue (this=0x100120a8) at gdbtest2.cpp:1616 const T (gdb) sNumber Box “9“ deletedNumBox (this=0x100120a8) at gdbtest2.cpp:1111 Next = 0;(gdb) sNumChain:RemoveBox (this=0x10012008, item_to_remove=0xffffe200) at gdbtest2.cpp:515
37、1 current = 0;1: *this-pointer = Num = 0, Next = 0x0(gdb) s53 return 0;1: *this-pointer = Num = 0, Next = 0x0(gdb) s0x10000d1c 66 return -1;1: *this-pointer = Num = 0, Next = 0x0从上面的跟踪过程中,您可以看到 list-pointer 在删除 Number Box “9“ 之后指向了 Number Box “0“。这个逻辑并不正确,因为在删除 Number Box “9“ 之后,list-pointer 应该指向的是
38、Number Box “8“。现在非常显然我们应该在第 50 行之前添加一条语句 pointer = pointer-GetNext();,如下所示:清单 18. 在第 50 行之前添加一条 pointer = pointer-GetNext(); 语句49 else 50 pointer = pointer-GetNext();51 delete current;52 current = 0;53 54 return 0;将新修改之后的程序保存为 gdbtest3.cpp,然后编译并再次运行。清单 19. 再次运行程序(gdbtest3.cpp)# g+ -g -o gdbtest3 gdb
39、test3.cpp# ./gdbtest3Number Box “0“ createdNumber Box “1“ createdNumber Box “2“ createdNumber Box “3“ createdNumber Box “4“ createdNumber Box “5“ createdNumber Box “6“ createdNumber Box “7“ createdNumber Box “8“ createdNumber Box “9“ createdlist createdNumber Box “9“ deletedNumber Box “8“ deletedNum
40、ber Box “7“ deletedNumber Box “6“ deletedNumber Box “5“ deletedNumber Box “4“ deletedNumber Box “3“ deletedNumber Box “2“ deletedNumber Box “1“ deletedNumber Box “0“ deleted这才是我们期望的结果。多线程环境在 GDB 中有一些特殊的命令可以用于多线程应用程序的调试。下面这个例子给出了一个死锁情况,并介绍了如何使用这些命令来检查多线程应用程序中的问题:清单 20. 多线程的例子#include #include “pthrea
41、d.hpthread_mutex_t AccountA_mutex;pthread_mutex_t AccountB_mutex;struct BankAccount char account_name1;int balance;struct BankAccount accountA = “A“, 10000 ;struct BankAccount accountB = “B“, 20000 ;void * transferAB (void* amount_ptr) int amount = *(int*)amount_ptr);pthread_mutex_lock(if (accountA.
42、balance amount) printf(“There is not enough memory in Account A!n“);pthread_mutex_unlock(pthread_exit(void *)1);accountA.balance -=amount;sleep(1);pthread_mutex_lock(accountB.balance +=amount;pthread_mutex_unlock( pthread_mutex_unlock(void * transferBA (void* amount_ptr) int amount = *(int*)amount_p
43、tr);pthread_mutex_lock(if (accountB.balance amount) printf(“There is not enough memory in Account B!n“);pthread_mutex_unlock(pthread_exit(void *)1);accountB.balance -=amount;sleep(1);pthread_mutex_lock(accountA.balance +=amount;pthread_mutex_unlock(pthread_mutex_unlock(int main(int argc, char* argv)
44、 int threadid4;pthread_t pthread4;int transfer_amount4 = 100, 200, 300, 400;int final_balanceA, final_balanceB;final_balanceA=accountA.balance-transfer_amount0-transfer_amount1+transfer_amount2+transfer_amount3;final_balanceB=accountB.balance+transfer_amount0+transfer_amount1-transfer_amount2-transf
45、er_amount3;if (threadid0 = pthread_create(exit (1);if (threadid1 = pthread_create(exit (1);if (threadid2 = pthread_create(exit (1);if (threadid3 = pthread_create(exit (1);printf(“Transitions are in progress“);while (accountA.balance != final_balanceA) printf(“nAll the money is transferred !n“);使用 gc
46、c 来编译这个程序,如下所示: # gcc -g -o gdbtest2 gdbtest2.c -L/lib/tls -lpthread程序 gdbtest2 会挂起,不会返回一条 All the money is transferred ! 消息。将 gdb 附加到正在运行的进程上,从而了解这个进程内部正在发生什么。清单 21. 将 gdb 附加到正在运行的进程上# ps -ef |grep gdbtest2root 9510 8065 1 06:30 pts/1 00:00:00 ./gdbtest2root 9516 9400 0 06:30 pts/4 00:00:00 grep gd
47、btest2# gdb -pid 9510GNU gdb 6.2.1Copyright 2004 Free Software Foundation, Inc.GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.Type “show copying“ to see the conditions.There is absolutely no warranty for GDB. Type “show warranty“ for details.This GDB was configured as “ppc-suse-linux“.Attaching to process 9510Reading symbo