用GDB调试程序
GDB概述
————
GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。
一般来说,GDB主要帮忙你完成下面四个方面的功能:
从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。
一个调试示例
——————
源程序:tst.c
编译生成执行文件:(Linux下)
使用GDB调试:
hchen/test> gdbtst
GNU gdb 5.1.1
Copyright 2002 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 certainconditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.
This GDB was configured as "i386-suse-linux"...
(gdb)l
1
2
3
4
5
6
7
8
9
10
(gdb)
11
12
13
14
15
16
17
18
19
20
(gdb) break16
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break
NumType
1
2
(gdb)r
Starting program: /home/hchen/test/tst
Breakpoint 1, main () attst.c:17
17
(gdb)n
18
(gdb) n
20
(gdb) n
18
(gdb) n
20
(gdb)c
Continuing.
result[1-100] =5050
Breakpoint 2, func (n=250) attst.c:5
5
(gdb) n
6
(gdb) pi
$1 = 134513808
(gdb) n
8
(gdb) n
6
(gdb) p sum
$2 = 1
(gdb) n
8
(gdb) p i
$3 = 2
(gdb) n
6
(gdb) p sum
$4 = 3
(gdb)bt
#0
#1
#2
(gdb) finish
Run till exit from #0
0x080484e4 in main () at tst.c:24
24
Value returned is $6 = 31375
(gdb)c
Continuing.
result[1-250] =31375
Program exited with code 027.<--------程序退出,调试结束。
(gdb)q
hchen/test>
好了,有了以上的感性认识,还是让我们来系统地认识一下gdb吧。
使用GDB
————
一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的-g 参数可以做到这一点。如:
如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试他。
启动GDB的方法有以下几种:
GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb-help查看。我在下面只例举一些比较常用的参数:
GDB的命令概貌
———————
启动gdb后,就你被带入gdb的调试环境中,就可以使用gdb的命令开始调试程序了,gdb的命令可以使用help命令来查看,如下所示:
gdb的命令很多,gdb把之分成许多个种类。help命令只是例出gdb的命令种类,如果要看种类中的命令,可以使用help <class> 命令,如:helpbreakpoints,查看设置断点的所有命令。也可以直接help<command>来查看命令的帮助。
gdb中,输入命令时,可以不用打全命令,只用打命令的前几个字符就可以了,当然,命令的前几个字符应该要标志着一个唯一的命令,在Linux下,你可以敲击两次TAB键来补齐命令的全称,如果有重复的,那么gdb会把其例出来。
要退出gdb时,只用发quit或命令简称q就行了。
GDB中运行UNIX的shell程序
————————————
在gdb环境中,你可以执行UNIX的shell的命令,使用gdb的shell命令来完成:
还有一个gdb命令是make:
在GDB中运行程序
————————
当以gdb<program>方式启动gdb后,gdb会在PATH路径和当前目录中搜索<program>的源文件。如要确认gdb是否读到源文件,可使用l或list命令,看看gdb是否能列出源代码。
在gdb中,运行程序使用r或是run命令。程序的运行,你有可能需要设置下面四方面的事。
1、程序运行参数。
2、运行环境。
3、工作目录。
4、程序的输入输出。
调试已运行的程序
————————
两种方法:
1、在UNIX下用ps查看正在运行的程序的PID(进程ID),然后用gdb<program> PID格式挂接正在运行的程序。
2、先用gdb<program>关联上源代码,并进行gdb,在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。
暂停 / 恢复程序运行
—————————
调试程序中,暂停程序运行是必须的,GDB可以方便地暂停程序的运行。你可以设置程序的在哪行停住,在什么条件下停住,在收到什么信号时停往等等。以便于你查看运行时的变量,以及运行时的流程。
当进程被gdb停住时,你可以使用info program来查看程序的是否在运行,进程号,被暂停的原因。
在gdb中,我们可以有以下几种暂停方式:断点(BreakPoint)、观察点(WatchPoint)、捕捉点(CatchPoint)、信号(Signals)、线程停止(ThreadStops)。如果要恢复程序运行,可以使用c或是continue命令。
一、设置断点(BreakPoint)
二、设置观察点(WatchPoint)
三、设置捕捉点(CatchPoint)
使用gdb调试
我们将会使用GNU调试器,gdb,来调试这个程序。这是一个可以免费得到并且可以用于多个Unix平台的功能强大的调试器。他也是Linux系统上的默认调试器。gdb已经被移植到许多其他平台上,并且可以用于调试嵌入式实时系统。
启动gdb
让我们重新编译我们的程序用于调试并且启动gdb。
$ cc -g -o debug3 debug3.c
$ gdb debug3
GNU gdb 5.2.1
Copyright 2002 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 certainconditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” fordetails.
This GDB was configured as “i586-suse-linux”...
(gdb)
gdb具有丰富的在线帮助,以及可以使用info程序进行查看或是在Emacs中进行查看的完整手册。
(gdb) help
List of classes of commands:
aliases — Aliases of other commands
breakpoints — Making program stop at certain points
data — Examining data
files — Specifying and examining files
internals — Maintenance commands
obscure — Obscure features
running — Running the program
stack — Examining the stack
status — Status inquiries
support — Support facilities
tracepoints — Tracing of program execution without stopping theprogram
user-defined — User-defined commands
Type “help” followed by a class name for a list of commands in thatclass.
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)
gdb本身是一个基于文本的程序,但是他确实了一些有助于重复任务的简化操作。许多版本具有一个命令行编辑历史,从而我们可以在命令历史中进行滚动并且再次执行相同的命令。所有的版本都支持一个"空白命令",敲击Enter会再次执行上一条命令。当我们使用step或是next命令在一个程序中分步执行特殊有用。
运行一个程序
我们可以使用run命令执行这个程序。我们为run命令所指定的所有命令都作为参数传递给程序。在这个例子中,我们并不需要任何参数。
我们在这里假设我们的系统与作者的类似,也产生了内存错误的错误信息。如果不是,请继续阅读。我们就会发现当我们自己的程序生成一个内存错误时应怎么办。如果我们并不没有得到内存错误信息,但是我们在阅读本书时希望运行这个例子,我们可以拾起第一个内存访问问题已经被修复的debug4.c。
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug3
Program received signal SIGSEGV, Segmentation fault.
0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
23
(gdb)
如前面一样,我们程序并没有正确运行。当程序失败时,gdb会向我们显示原因以及位置。现在我们可以检测问题背后的原因。
依据于我们的内核,C库,以及编译器选项,我们所看到的程序错误也许有所不同,例如,也许当数组元素交换时是在25行,而不是数组元素比较时的23行。如果是这种情况,我们也许会看到如下的输出:
Program received signal SIGSEGV, Segmentation fault.
0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
25
我们仍然可以遵循如下的gdb例子会话。
栈追踪
程序已经在源文件debug3.c的第23行处的sort函数停止。如果我们并没有使用额外的调试信息来编译这个程序,我们就不能看到程序在哪里失败,也不能使用变量名来检测数据。
我们可以通过使用backstrace命令来查看我们是如何到达这个位置的。
(gdb) backtrace
#0 0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
#1 0x0804849b in main () at debug3.c:37
#2 0x400414f2 in __libc_start_main () from /lib/libc.so.6
(gdb)
这个是一个非常简单的程序,而且追踪信息很短小,因为我们并没有在其他的函数内部来调用许多函数。我们可以看到sort是由同一个文件debug3.c中37行处的main来调用的。通常,问题会更为复杂,而我们可以使用backtrace来发现我们到达错误位置的路径。
backtrace命令可以简写为bt,而且为了与其他调试器兼容,where命令也具有相同的功能。
检测变量
当程序停止时由gdb所输出的信息以及在栈追踪中的信息向我们显示了函数能数的值。
sort函数是使用一个参数a来调用的,而其值为0x8049580。这是数组的地址。依据于所使用的编译器以及操作系统,这个值在不同的操作系统也会不同。
所影响的行号23,是一个数组元素与另一个数组元素进行比较的地方。
if(a[j].key > a[j+1].key) {
我们可以使用调试器来检测函数参数,局部变量以及全局数据的内容。print命令可以向我们显示变量以及其他表达式的内容。
(gdb) print j
$1 = 4
在这里我们可以看到局部变量j的值为4。类似这样由gdb命令所报告的所有值都会保存在伪变量中以备将来使用。在这里变量$1赋值为4以防止我们在以后使用。以后的命令将他们的结果存储为$2,$3,依次类推。
j的值为4的事实意味着程序试着执行语句
if(a[4].key > a[4+1].key)
我们传递给sort的数组,array,只有5个元素,由0到4进行索引。所以这条语句读取并不存在的array[5]。循环变量j已经读取一个错误的值。
如果我们尝试这个例子,而我们程序在25行发生错误,我们系统只有在交互元素时才会检测到一个超过数组边界的读取,执行
a[j] = a[j+1];
此时将j设置为4,结果为
a[4] = a[4+1];
我们可以使用print通过表达式来查看所传递的数组元素。使用gdb,我们几乎可以使用任何合法的C表达式来输出变量,数组元素,以及指针的值。
(gdb) print a[3]
$2 = {data = “alex”, ‘\000’ <repeats 4091times>, key = 1}
(gdb)
gdb将命令的结果保存在一个伪变量中,$<number>。上一个结果总是为$,而之前的一个为$$。这可以使得在一个结果可以用在另一个命令中。例如,
(gdb) print j
$3 = 4
(gdb) print a[$-1].key
$4 = 1
列出程序
我们可以使用list命令在gdb内查看程序源代码。这会打印出当前位置周围的部分代码。持续的使用list会输出更多的代码。我们也可以为list指定一个行号或是函数名作为一个参数,而gdb就会显示那个位置的代码。
(gdb) list
18
19
20
21
22
23
24
25
26
27
(gdb)
我们可以看到在22行循环设置为当变量j小于n时才会执行。在这个例子中,n为5,所以j的最终值为4,总是小1。4会使得a[4]与a[5]进行比较并且有可能进行交换。这个问题的解决方法就是修正循环的结束条件为j< n-1。
让我们做出修改,将这个新程序称之为debug4.c,重新编译,并再次运行。
for(j = 0; j < n-1; j++) {
$ cc -g -o debug4 debug4.c
$ ./debug4
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
程序仍不能正常工作,因为他输出了一个不正确的排序列表。下面我们使用gdb在程序运行时分步执行。
设置断点
查找出程序在哪里失败,我们需要能够查看程序运行他都做了什么。我们可以通过设置断点在任何位置停止程序。这会使得程序停止并将控制权返回调试器。我们将能够监视变量并且允许程序继续执行。
在sort函数中有两个循环。外层循环,使用循环变时i,对于数组中的每一个元素运行一次。内层循环将其与列表中的下一个元素进行交换。这具有将最小的元素交换到最上面的效果。在外层循环的每一次执行之后,最大的元素应位置底部。我们可通过在外层循环停止程序进行验证并且检测数组状态。
有许多命令可以用于设置断点。通过gdb的help breakpoint命令可以列表这些命令:
(gdb) help breakpoint
Making program stop at certain points.
List of commands:
awatch — Set a watchpoint for an expression
break — Set breakpoint at specified line or function
catch — Set catchpoints to catch events
clear — Clear breakpoint at specified line or function
commands — Set commands to be executed when a breakpoint ishit
condition — Specify breakpoint number N to break only if COND istrue
delete — Delete some breakpoints or auto-display expressions
disable — Disable some breakpoints
enable — Enable some breakpoints
hbreak — Set a hardware assisted breakpoint
ignore — Set ignore-count of breakpoint number N to COUNT
rbreak — Set a breakpoint for all functions matching REGEXP
rwatch — Set a read watchpoint for an expression
tbreak — Set a temporary breakpoint
tcatch — Set temporary catchpoints to catch events
thbreak — Set a temporary hardware assisted breakpoint
watch — Set a watchpoint for an expression
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
让我们在20行设置一个断点并且运行这个程序:
$ gdb debug4
(gdb) break 20
Breakpoint 1 at 0x804835d: file debug4.c, line 20.
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 1, sort (a=0x8049580, n=5) at debug4.c:20
20
我们可以输出数组值并且使用cont可以使得程序继续执行。这个会使得程序继续运行直到遇到下一个断点,在这个例子中,直到他再次执行到20行。在任何时候我们都可以有多个活动断点。
(gdb) print array[0]
$1 = {data = “bill”, ‘\000’ <repeats 4091times>, key = 3}
要输出多个连续的项目,我们可以使用@<number>结构使得gdb输出多个数组元素。要输出array的所有五个元素,我们可以使用
(gdb) print array[0]@5
$2 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
注意,输出已经进行简单的处理从而使其更易于阅读。因为这是第一次循环,数组并没有发生变量。当我们允许程序继续执行,我们可以看到当处理执行时array的成功修改:
(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:20
20
(gdb) print array[0]@5
$3 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
(gdb)
我们可以使用display命令来设置gdb当程序在断点处停止时自动显示数组:
(gdb) display array[0]@5
1: array[0] @ 5 = {{data = “bill”, ‘\000’ <repeats4091 times>, key = 3}, {
而且我们可以修改断点,从而他只是简单的显示我们所请求的数据并且继续执行,而不是停止程序。在这样做,我们可以使用commands命令。这会允许我们指定当遇到一个断点时执行哪些调试器命令。因为我们已经指定了一个display命令,我们只需要设置断点命令继续执行。
(gdb) commands
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just “end”.
> cont
> end
现在我们允许程序继续,他会运行完成,在每次运行到外层循环时输出数组的值。
(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049684, n=3) at debug4.c:20
20
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
Breakpoint 1, sort (a=0x8049684, n=2) at debug4.c:20
20
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)
gdb报告程序并没有以通常的退出代码退出。这是因为程序本身并没有调用exit也没有由main返回一个值。在这种情况下,这个退出代码是无意义的,而一个有意义的退出代码应由调用exit来提供。
这个程序看起来似乎外层循环次数并不是我们所期望的。我们可以看到循环结束条件所使用的参数值n在每个断点处减小。这意味着循环并没有执行足够的次数。问题就在于30行处n的减小。
n--;
这是一个利用在每一次外层循环结束时array的最大元素都会位于底部的事实来优化程序的尝试,所以就会有更少的排序。但是,正如我们所看到的,这是与外层循环的接口,并且造成了问题。最简单的修正方法就是删除引起问题的行。让我们通过使用调试器来应用补丁测试这个修正是否有效。
使用调试进行补丁
我们已经看到了我们可以使用调试器来设置断点与检测变量的值。通过使用带动作的断点,我们可以在修改源代码与重新编译之前试验一个修正,称之为补丁。在这个例子中,我们需要在30行设置断点,并且增加变量n。然后,当30行执行,这个值将不会发生变化。
让我们从头启动程序。首先,我们必须删除我们的断点与显示。我们可以使用info命令来查看我们设置了哪些断点与显示:
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:
(gdb) info break
NumType
1
我们可以禁止这些或是完全删除他们。如果我们禁止他们,那么我们可以在以后需要他们时重新允许这些设置:
(gdb) disable break 1
(gdb) disable display 1
(gdb) break 30
Breakpoint 2 at 0x8048462: file debug4.c, line 30.
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just “end”.
>set variable n = n+1
>cont
>end
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)
这个程序运行结束并且会输出正确的结果。现在我们可以进行修正并且继续使用更多的数据进行测试。
了解更多有关gdb的内容
GNU调试器是一个强大的工具,可以提供大量的有关运行程序内部状态的信 息。在支持一个名叫硬件断点(hardwarebreakpoint)的实用程序的系统上,我们可以使用gdb来实时的查看变量的改变。硬件断点是某些CPU的一个特性;如果出现特定的条件,通常是在指定区域的内存访问,这些处理器能够自动停止。相对应的,gdb可以使用watch表达式。这就意味着,出于性能考虑,当一个表达式具有一个特定的值时,gdb可以停止这个程序,而不论计算发生在程序中的哪个位置。
断点可以使用计数以及条件进行设置,从而他们只在一定的次数之后或是当满足一个条件时才会被引发。
gdb也能够将其本身附在已经运行的程序中。这对于我们调试客户端/服务器系统时是非常有用的,因为我们可以调试一个正在运行的行为不当的服务器进程,而不需要停止与重启服务器。例如,我们可以使用gcc -O-g选项来编译我们的程序,从而得到优化与调试的好处。不足之处就是优化也许会重新组织代码,所以当我们分步执行时,我们也许会发出我们自身的跳转来达到与原始源代码相同的效果。
我们也可以使用gdb来调试已经崩溃的程序。Linux与Unix经常会在一个程序失败时在一个名为core的文件中生成一个核心转储信息。这是一个程序内存的图象并且会包含失败时全局变量的值。我们可以使用gdb来查看当程序崩溃时程序运行到哪里。查看gdb手册页我们可以得到更为详细的信息。
gdb以GPL许可证发布并且绝大多数的Unix系统都会支持他。我们强烈建议了解gdb。
更多的调试工具
除了强大的调试工具,例如gdb,Linux系统通常还会提供一些其他们的我们可以用于诊治调试进程的工具。其中的一些会提供关于一个程序的静态信息;其他的会提供动态分析。
静态分析只由程序源代码提供信息。例如ctags,cxref,与cflow这样的程序与源代码一同工作,并且会提供有关函数调用与位置的有用信息。
动态会析会提供有关一个程序在执行过程中如何动作的信息。例如prof与gprof这样的程序会提供有关执行了哪个函数并且执行多长时间的信息。
下面我们来看一下其中的一些工具及其输出。并不是所有的这些工具都可以在所有的系统上得到,尽管其中的一些都有自由版本。
Lint:由我们的程序中移除Fluff
原始的Unix系统提供了一个名为lint的实用程序。他实质是一个C编译的前端,带有一个测试设计来适应一些常识并且生成警告。他会检测变量在设计之前在哪里使用以及哪里函数参数没有被使用,以及其他的一些情况。
更为现代的C编译器可以编译性能为代价提供类似的警告。lint本身已经被C标准所超越。因为这个工具是基于早期的C编译器,他并不能处理所有的ANSI语法。有一些商业版本的lint可以用于Unix,而且在网络上至少有一个名为splint可以用于Linux。这就是过去所知的LClint,他是MIT一个工程的一部分,来生成用于通常规范的工具。一个类似于lint的工具splint可以提供查看注释的有用代码。splint可以在htt://www.splin.org处得到。
下面是一个编辑过的splint例子输出,这是运行我们在前面调试的例子程序的早期版本中所产生的输出:
neil@beast:~/BLP3/chapter10> splint -strictdebug0.c
Splint 3.0.1.6 --- 27 Mar 2002
debug0.c:14:22: Old style function declaration
debug0.c: (in function sort)
debug0.c:20:31: Variable s used before definition
debug0.c:20:23: Left operand of & is not unsignedvalue (boolean):
debug0.c:20:23: Test expression for for not boolean, type unsignedint:
debug0.c:20:23: Operands of & are non-integer(boolean) (in post loop test):
debug0.c:32:14: Path with no return in function declared to returnint
debug0.c:34:13: Function main declared without parameter list
debug0.c: (in function main)
debug0.c:36:17: Return value (type int) ignored: sort(array,5)
debug0.c:37:14: Path with no return in function declared to returnint
debug0.c:14:13: Function exported but not used outside debug0:sort
Finished checking --- 22 code warnings
$
这个程序报告旧风格的函数定义以及函数返回类型与他们实际返回类型之间的不一致。这些并不会影响程序的操作,但是应该注意。
他还在下面的代码片段中检测到两个实在的bug:
int s;
for(; i < n & s != 0; i++) {
splint已经确定在20行使用了变量s,但是并没有进行初始化,而且操作符&已经被更为通常的&&所替代。在这个例子中,操作符优先级修改了测试的意义并且是程序的一个问题。
所有这些错误都在调试开始之前在代码查看中被修正。尽管这个例子有一个故意演示的目的,但是这些错误真实世界的程序中经常会出现的。
函数调用工具
三个实用程序-ctags,cxref与cflow-形成了X/Open规范的部分,所以必须在具有软件开发功能的Unix分枝系统上提供。
ctags
ctags程序创建函数索引。对于每一个函数,我们都会得到一个他在何处使用的列表,与书的索引类似。
ctags [-a] [-f filename] sourcefile sourcefile ...
ctags -x sourcefile sourcefile ...
默认情况下,ctags在当前目录下创建一个名为tags的目录,其中包括在输入源文件码中所声明的每一个函数,如下面的格式
announce app_ui.c /^static void announce(void) /
文件中的每一行由一个函数名,其声明所在的文件,以及一个可以用在文件中查找到函数定义所用的正则表达式所组成。一些编辑器,例如Emacs可以使用这种类型的文件在源码中遍历。
相对应的,通过使用ctags的-x选项,我们可以在标准输出上产生类似格式的输出:
find_cat 403 app_ui.c static cdc_entry find_cat(
我们可以通过使用-ffilename选项将输出重定向到另一个不同的文件中,或是通过指定-a选项将其添加到一个已经存在的文件中。
cxref
cxref程序分析C源代码并且生成一个交叉引用。他显示了每一个符号在程序中何处被提到。他使用标记星号的每一个符号定义位置生成一个排序列表,如下所示:
SYMBOL
calldata
在作者的机子上,前面的输入在程序的源码目录中使用下面的命令来生成的:
$ cxref *.c *.h
但是实际的语法因为版本的不同而不同。查看我们系统的文档或是man手册可以得到更多的信息。
cflow
cflow程序会输出一个函数调用树,这是一个显示函数调用关系的图表。这对于查看程序结构来了解他是如何操作的以及了解对于一个函数有哪些影响是十分有用的。一些版本的cflow可以同时作用于目标文件与源代码。查看手册页我们可以了解更为详细的操作。
下面是由一个cflow版本(cflow-2.0)所获得的例子输出,这个版本的cflow版本是由MartyLeisner维护的,并且可以网上得到。
1
2
3
4
5
6
7
8
9
10
11
从这个输出中我们可以看到main函数调用show_all_lists,而show_all_lists调用display_list,display_list本身调用printf。
这个版本cflow的一个选项就是-i,这会生成一个反转的流程图。对于每一个函数,cflow列出调用他的其他函数。这听起来有些复杂,但是实际上并不是这样。下面是一个例子。
19 display_list {prcc.c 1056}
20
21 exit {}
22
23
24
...
74
75
76
77
78
...
99
100
例如,这告诉我们调用exit的函数有main,show_all_lists与usage。
使用prof/gprof执行性能测试
当我们试着追踪一个程序的性能问题时一个十分有用的技术就是执行性能测试(executionprofiling)。通常被特殊的编译器选项以及辅助程序所支持,一个程序的性能显示他在哪里花费时间。
prof程序(以及其GNU版本gprof)会由性能测试程序运行时所生成的执行追踪文件中输出报告。一个可执行的性能测试是由指定-p选项(对prof)或是-pg选项(对gprof)所生成的:
$ cc -pg -o program program.c
这个程序是使用一个特殊版本的C库进行链接的并且被修改来包含监视代码。对于不同的系统结果也许不同,但是通常是由安排频繁被中断的程序以及记录执行位置来做到的。监视数据被写入当前目录中的一个文件,mon.out(对于gprof为gmon.out)。
$ ./program
$ ls -ls
prof/gprof程序读取这些监视数据并且生成一个报告。查看其手册页可以详细了解其程序选项。下面以gprof输出作为一个例子:
cumulative
断言
在程序的开发过程中,通常使用条件编译的方法引入调试代码,例如printf,但是在一个发布的系统中保留这些信息是不实际的。然而,经常的情况是问题出现与不正确的假设相关的程序操作过程中,而不是代码错误。这些都是"不会发生"的事件。例如,一个函数也许是在认为其输入参数总是在一定范围下而编写的。如果给他传递一些不正确的数据,也许整个系统就会崩溃。
对于这些情况,系统的内部逻辑在哪里需要验证,X/Open提供了assert宏,可以用来测试一个假设是否正确,如果不正确则会停止程序。
#include <assert.h>
void assert(int expression)
assert宏会计算表达式的值,如果不为零,则会向标准错误上输出一些诊断信息,并且调用abort来结束程序。
头文件assert.h依据NDEBUG的定义来定义宏。如果头文件被处理时定义了NDEBUG,assert实质上被定义为空。这就意味着我们可以通过使用-DNDEBUG在编译时关闭断言或是在包含assert.h文件之前包含下面这行:
#define NDEBUG
这种方法的使用是assert的一个问题。如果我们在测试中使用assert,但是却对生产代码而关闭,比起我们测试时的代码,我们的生产代码就不会太安全。在生产代码中保留断言开启状态并不是通常的选择,我们希望我们的代码向用户显示一条不友好的错误assertfailed与一个停止的程序吗?我们也许会认为最好是编写我们自己的检测断言的错误追踪例程,而不必在我们的生产代码中完全禁止。
我们同时要小心在assert断言没有临界效果。例如,如果我们在一个临界效果中调用一个函数,如果移除了断言,在生产代码中就不会出现这个效果。
试验--assert
下面的程序assert.c定义了一个必须传递正值参数的函数。通过使用一个断言可以避免不正常参数的可能。
在包含assert.h头文件和检测参数是否为正的平方根函数之后,我们可以编写如下的函数:
#include <stdio.h>
#include <math.h>
#include <assert.h>
double my_sqrt(double x)
{
}
int main()
{
}
当我们运行这个程序时,我们就会看到当我们传递一个非法值时就会违背这个断言。事实上的断言失败的消息格式会因系统的不同而不同。
$ cc -o assert assert.c -lm
$ ./assert
sqrt +2 = 1.41421
assert: assert.c:7: my_sqrt: Assertion `x >= 0.0’failed.
Aborted
$
工作原理
当我们试着使用一个负数来调用函数my_sqrt时,断言就会失败。assert宏会提供违背断言的文件和行号,以及失败的条件。程序以一个退出陷井结束。这是assert调用abort的结果。
如果我们使用-DNDEBUG选项来编译这个程序,断言就会被编译在外,而当我们由my_sqrt中调用sqrt函数时我们就会得到一个算术错误。
$ cc -o assert -DNDEBUG assert.c -lm
$ ./assert
sqrt +2 = 1.41421
Floating point exception
$
一些最近的算术库版本会返回一个NaN(Not a Number)值来表示一个不可用的结果。
sqrt –2 = nan
内存调试
富含bug而且难于跟踪调试的一个区域就是动态内存分配。如果我们编译一个使用malloc与free来分配内存的程序,很重要的一点就是我们要跟踪我们所分配的内存块,并且保证不要使用已经释放的内存块。
通常,内存是由malloc分配并且赋给一个指针变量的。如果指针变量被修改了,而又没有其他的指针来指向这个内存块,他就会变为不可访问的内存块。这就是一个内存泄露,而且会使得我们程序尺寸变大。如果我们泄露了大量的内存,那么我们的系统就会变慢并且会最终用尽内存。
如果我们在超出一个分配的内存块的结束部分(或是在一个内存块的开始部分)写入数据,我们很有可能会破坏malloc库来跟踪分配所用的数据结构。在这种情况下,在将来的某个时刻,调用malloc,或者甚至是free,就会引起段错误,而我们的程序就会崩溃。跟踪错误发生的精确点是非常困难的,因为很可能他在引起崩溃的事件发生以前很一段时间就已经发生了。
不必奇怪的是,有一些工具,商业或是自由的,可以有助于处理这两种问题类型。例如,有许多不同的malloc与free版本,其中的一些包含额外的代码在分配与回收上进行检测尝试检测一个内存块被释放两次或是其他一些滥用类型的情况。
ElectricFence
ElectricFence 库是由BrucePerens开发的,并且在一些Linux发行版本中作为一个可选的组件来提供,例如RedHat,而且已经可以在网络上获得。他尝试使用Linux的虚拟内存工具来保护malloc与free所使用的内存,从而在内存被破坏时终止程序。
试验--ElectricFence
下面的程序,efence.c,使用malloc分配一个内存块,然后在超出块结束处写入数据。让我们看一下会发生什么情况。
#include <stdio.h>
#include <stdlib.h>
int main()
{
}
当我们编译运行这个程序时,我们并不会看到进一步的行为。然而,似乎malloc所分配的内存区域有一些问题,而我们实际上已经遇到了麻烦。
$ cc -o efence efence.c
$ ./efence
$
然而,如果我们使用ElectricFence库,libefence.a来链接这个程序,我们就会得到一个即时的响应。
$ cc -o efence efence.c -lefence
$ ./efence
Segmentation fault
$
在调试器下运行可以定位这个问题:
$ cc -g -o efence efence.c -lefence
$ gdb efence
Starting program: /home/neil/BLP3/chapter10/efence
[New Thread 1024 (LWP 1869)]
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1024 (LWP 1869)]
0x080484ad in main () at efence.c:10
10
(gdb)
工作原理
Electric替换malloc并且将函数与计算机处理器的虚拟内存特性相关联来阻止非法的内存访问。当这样的访问发生时,就会抛出一个段错误信息从而可以终止程序。
valgrind
valgrind是一个可以检测我们已经讨论过的许多问题的工具。事实上,他可以检测数据访问错误与内存泄露。也许他并没有被包含在我们的Linux发行版本中,但是我们可以在http://developer.kde.org/~sewardj处得到。
程序并不需要使用valgrind重新编译,而我们甚至可以调用一个正在运行的程序的内存访问。他很值得一看,他已经用在主要的开发上,包含KDE版本3。
试验--valgrind
下面的程序,checker.c,分配一些内存,读取超过那块内存限制的位置,在其结束处之外写入数据,然后使其不能访问。
#include <stdio.h>
#include <stdlib.h>
int main()
{
}
要使用valgrind,我们只需要简单的运行valgrind命令,传递我们希望检测的选项,其后是使用其参数运行的程序。
当我们使用valgrind来运行我们的程序时,我们可以看到诊断出许多问题:
$ valgrind --leak-check=yes -v ./checker
==3436== valgrind-1.0.4, a memory error detector for x86GNU/Linux.
==3436== Copyright (C) 2000-2002, and GNU GPL’d, by JulianSeward.
==3436== Estimated CPU clock rate is 452 MHz
==3436== For more details, rerun with: -v
==3436==
==3436== Invalid read of size 1
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436== Invalid write of size 1
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436==
==3436== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0from 0)
==3436== malloc/free: in use at exit: 1024 bytes in 1 blocks.
==3436== malloc/free: 1 allocs, 0 frees, 1024 bytesallocated.
==3436== For counts of detected errors, rerun with: -v
==3436== searching for pointers to 1 not-freed blocks.
==3436== checked 3468724 bytes.
==3436==
==3436== definitely lost: 1024 bytes in 1 blocks.
==3436== possibly lost:
==3436== still reachable: 0 bytes in 0 blocks.
==3436==
==3436== 1024 bytes in 1 blocks are definitely lost in loss record1 of 1
==3436==
==3436==
==3436==
==3436==
==3436==
==3436== LEAK SUMMARY:
==3436==
==3436==
==3436==
==3436== Reachable blocks (those to which a pointer was found) arenot shown.
==3436== To see them, rerun with: --show-reachable=yes
==3436== $
这里我们可以看到错误的读取与写入已经被捕获,而所关注的内存块与他们被分配的位置相关联。我们可以使用调试器在出错点断开程序。
valgrind有许多选项,包含特定的错误类型表达式与内存泄露检测。要检测我们的例子泄露,我们必须使用一个传递给valgrind的选项。当程序结束时要检测内存泄露,我们需要指定 --leak-check=yes。我们可以使用valgrind --help得到一个选项列表。
工作原理
我们的程序在valgrind的控制下执行,这会检测我们程序所执行的各种动作,并且执行许多检测,包括内存访问。如果程序访问一个已分配的内存块并且访问是非法的,valgrind就会输出一条信息。在程序结束时,一个垃圾收集例程就会运行来检测是否在存在分配的内存块没有被释放。这些孤儿内存也会被报告。
小结
在这一章,我们了解了一些调试工具与技术。Linux提供了一些强大的工具可以用于由程序中移除缺陷。我们使用gdb来消除程序中的bug,并且了解了如cflow与splint这样的数据分析工具。最后我们了解了当我们使用动态分配内存时会出现的问题,以及一些用于类似问题诊断的工具,例如ElectricFence与valgrind。
查看运行时数据
———————
一、表达式
二、程序变量
三、数组
四、输出格式
五、查看内存
六、自动显示
七、设置显示选项
八、历史记录
九、GDB环境变量
十、查看寄存器
改变程序的执行
———————
一、修改变量值
二、跳转执行
三、产生信号量
四、强制函数返回
五、强制调用函数
在不同语言中使用GDB
——————————
GDB支持下列语言:C, C++, Fortran, PASCAL,Java, Chill, assembly, 和Modula-2。一般说来,GDB会根据你所调试的程序来确定当然的调试语言,比如:发现文件名后缀为“.c”的,GDB会认为是C程序。文件名后缀为“.C, .cc, .cp, .cpp, .cxx, .c++”的,GDB会认为是C++程序。而后缀是“.f,.F”的,GDB会认为是Fortran程序,还有,后缀为如果是“.s, .S”的会认为是汇编语言。
也就是说,GDB会根据你所调试的程序的语言,来设置自己的语言环境,并让GDB的命令跟着语言环境的改变而改变。比如一些GDB命令需要用到表达式或变量时,这些表达式或变量的语法,完全是根据当前的语言环境而改变的。例如C/C++中对指针的语法是*p,而在Modula-2中则是p^。并且,如果你当前的程序是由几种不同语言一同编译成的,那到在调试过程中,GDB也能根据不同的语言自动地切换语言环境。这种跟着语言环境而改变的功能,真是体贴开发人员的一种设计。
下面是几个相关于GDB语言环境的命令:
如果GDB没有检测出当前的程序语言,那么你也可以手动设置当前的程序语言。使用setlanguage命令即可做到。