调试(三)
使用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 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 “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 the program
user-defined — User-defined commands
Type “help” followed by a class name for a list of commands in that class.
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 /* 23 */ if(a[j].key > a[j+1].key) {
(gdb)
如前面一样,我们程序并没有正确运行。当程序失败时,gdb会向我们显示原因以及位置。现在我们可以检测问题背后的原因。
依据于我们的内核,C库,以及编译器选项,我们所看到的程序错误也许有所不同,例如,也许当数组元素交换时是在25行,而不是数组元素比较时的23行。如果是这种情况,我们也许会看到如下的输出:
Program received signal SIGSEGV, Segmentation fault.
0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
25 /* 25 */ a[j] = a[j+1];
我们仍然可以遵循如下的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,是一个数组元素与另一个数组元素进行比较的地方。
/* 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行发生错误,我们系统只有在交互元素时才会检测到一个超过数组边界的读取,执行
/* 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 4091 times>, 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 /* 18 */ int s = 1;
19 /* 19 */
20 /* 20 */ for(; i < n && s != 0; i++) {
21 /* 21 */ s = 0;
22 /* 22 */ for(j = 0; j < n; j++) {
23 /* 23 */ if(a[j].key > a[j+1].key) {
24 /* 24 */ item t = a[j];
25 /* 25 */ a[j] = a[j+1];
26 /* 26 */ a[j+1] = t;
27 /* 27 */ s++;
(gdb)
我们可以看到在22行循环设置为当变量j小于n时才会执行。在这个例子中,n为5,所以j的最终值为4,总是小1。4会使得a[4]与a[5]进行比较并且有可能进行交换。这个问题的解决方法就是修正循环的结束条件为j < n-1。
让我们做出修改,将这个新程序称之为debug4.c,重新编译,并再次运行。
/* 22 */ 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 is hit
condition — Specify breakpoint number N to break only if COND is true
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 /* 20 */ for(; i < n && s != 0; i++) {
我们可以输出数组值并且使用cont可以使得程序继续执行。这个会使得程序继续运行直到遇到下一个断点,在这个例子中,直到他再次执行到20行。在任何时候我们都可以有多个活动断点。
(gdb) print array[0]
$1 = {data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}
要输出多个连续的项目,我们可以使用@<number>结构使得gdb输出多个数组元素。要输出array的所有五个元素,我们可以使用
(gdb) print array[0]@5
$2 = {{data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}, {
data = “neil”, ‘/000’ <repeats 4091 times>, key = 4}, {
data = “john”, ‘/000’ <repeats 4091 times>, key = 2}, {
data = “rick”, ‘/000’ <repeats 4091 times>, key = 5}, {
data = “alex”, ‘/000’ <repeats 4091 times>, key = 1}}
注意,输出已经进行简单的处理从而使其更易于阅读。因为这是第一次循环,数组并没有发生变量。当我们允许程序继续执行,我们可以看到当处理执行时array的成功修改:
(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:20
20 /* 20 */ for(; i < n && s != 0; i++) {
(gdb) print array[0]@5
$3 = {{data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}, {
data = “john”, ‘/000’ <repeats 4091 times>, key = 2}, {
data = “neil”, ‘/000’ <repeats 4091 times>, key = 4}, {
data = “alex”, ‘/000’ <repeats 4091 times>, key = 1}, {
data = “rick”, ‘/000’ <repeats 4091 times>, key = 5}}
(gdb)
我们可以使用display命令来设置gdb当程序在断点处停止时自动显示数组:
(gdb) display array[0]@5
1: array[0] @ 5 = {{data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}, {
data = “john”, ‘/000’ <repeats 4091 times>, key = 2}, {
data = “neil”, ‘/000’ <repeats 4091 times>, key = 4}, {
data = “alex”, ‘/000’ <repeats 4091 times>, key = 1}, {
data = “rick”, ‘/000’ <repeats 4091 times>, key = 5}}
而且我们可以修改断点,从而他只是简单的显示我们所请求的数据并且继续执行,而不是停止程序。在这样做,我们可以使用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 /* 20 */ for(; i < n && s != 0; i++) {
1: array[0] @ 5 = {{data = “john”, ‘/000’ <repeats 4091 times>, key = 2}, {
data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}, {
data = “alex”, ‘/000’ <repeats 4091 times>, key = 1}, {
data = “neil”, ‘/000’ <repeats 4091 times>, key = 4}, {
data = “rick”, ‘/000’ <repeats 4091 times>, key = 5}}
Breakpoint 1, sort (a=0x8049684, n=2) at debug4.c:20
20 /* 20 */ for(; i < n && s != 0; i++) {
1: array[0] @ 5 = {{data = “john”, ‘/000’ <repeats 4091 times>, key = 2}, {
data = “alex”, ‘/000’ <repeats 4091 times>, key = 1}, {
data = “bill”, ‘/000’ <repeats 4091 times>, key = 3}, {
data = “neil”, ‘/000’ <repeats 4091 times>, key = 4}, {
data = “rick”, ‘/000’ <repeats 4091 times>, key = 5}}
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的减小。
/* 30 */ n--;
这是一个利用在每一次外层循环结束时array的最大元素都会位于底部的事实来优化程序的尝试,所以就会有更少的排序。但是,正如我们所看到的,这是与外层循环的接口,并且造成了问题。最简单的修正方法就是删除引起问题的行。让我们通过使用调试器来应用补丁测试这个修正是否有效。
使用调试进行补丁
我们已经看到了我们可以使用调试器来设置断点与检测变量的值。通过使用带动作的断点,我们可以在修改源代码与重新编译之前试验一个修正,称之为补丁。在这个例子中,我们需要在30行设置断点,并且增加变量n。然后,当30行执行,这个值将不会发生变化。
让我们从头启动程序。首先,我们必须删除我们的断点与显示。我们可以使用info命令来查看我们设置了哪些断点与显示:
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1: y array[0] @ 5
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0804835d in sort at debug4.c:20
breakpoint already hit 4 times
cont
我们可以禁止这些或是完全删除他们。如果我们禁止他们,那么我们可以在以后需要他们时重新允许这些设置:
(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 /* 30 */ n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30 /* 30 */ n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30 /* 30 */ n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30 /* 30 */ n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30 /* 30 */ n--;
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)
这个程序运行结束并且会输出正确的结果。现在我们可以进行修正并且继续使用更多的数据进行测试。