调试程序

调试程序

在Linux环境下编程时,系统提供了调试程序的工具,也可以设置断点,从而查看程序在运行中的状态。Linux下最常用的调试工具就是gdb,此外还有一些另外的调试工具,例如断言和内存调试。

gdb调试程序

这里使用一个有问题的排序算法实现来演示用gdb调试程序的过程,其排序算法的代码如下:

#include <stdio.h>

typedef struct{
	//不能使用char * data;
	char data [4096];
	int key;
} item;

item array[] = {
	{"bill", 3},
	{"neil", 4},
	{"john", 2},
	{"rick", 5},
	{"alex", 1}
};

void sort(item a[],int n)
{
	int i = 0, j = 0;
	int s = 1;

	for(;i < n && s != 0; i++){
		s = 0;
		for(j = 0; j < n; j++){	//Bug在这里
			if(a[j].key > a[j+1].key){
				item t = a[j];
				a[j] = a[j+1];
				a[j+1] = t;
				s++;
			}
		}
	}
}

int main(void){
	int i;
	sort(array, 5);
	for(i = 0; i < 5; i++){
		printf("array[%d] = {%s, %d}\n", i, array[i].data, array[i].key);
	}
	return 0;
}

上面的程序根据结构体item中的key字段的大小对结构体数组进行冒泡排序。其中有两个需要注意的点:

  • bug出现在排序算法里面的哪一个for循环,当j = n-1时,j+1=n,这时越界访问了数组,会导致错误segment fault
  • 结构体中data字段是char data [4096],而不是char * data。因为如果使用char * data就会导致一个结构体数组中的结构体大小不够产生Bug,因为操作系统在分配内存的时候是按照一个内存块大小(通常是8KB)来分配的,如果数组中的元素太小,可能越界访问数组的时候还是没有超过系统给该数组分配的内存,虽然访问地址已经超出了数组的最大范围。

如果要使用gdb调试程序,在编译的时候要加上参数g,例如对于上面的程序,要使用命令gcc debug.c -g -o debug进行编译,否则编译出来的程序不能打断点进行调试。

将程序编译好之后,可以使用gdb debug命令进入gdb程序对程序进行调试,其中debug是源代码编译出来的可执行程序的名字。进入调试程序之后会出现下面的提示:

GNU gdb (Debian 8.2.1-2+b1) 8.2.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from debug...done.
(gdb)

这时就可以通过输入各种指令来控制程序的执行了,第一个可能需要的指令是run,这个指令让程序运行,并输出结果,如果程序出现错误,就会打印错误信息,并停止在出现错误的那一行。所以对于这个debug程序,使用run指令:

(gdb) run
Starting program: /media/scholar/软件1/资料/LinuxProgramming/5-Debug/debug 

Program received signal SIGSEGV, Segmentation fault.
sort (a=0x404040 <array>, n=5) at debug.c:25
25                              if(a[j].key > a[j+1].key){
(gdb) 

可以看到程序指出了出现错误的地方,并且让程序停止在了第25行,这时我们可以使用list命令,查看程序停止位置附近的代码:

(gdb) list
20              int s = 1;
21
22              for(;i < n && s != 0; i++){
23                      s = 0;
24                      for(j = 0; j < n; j++){
25                              if(a[j].key > a[j+1].key){
26                                      item t = a[j];
27                                      a[j] = a[j+1];
28                                      a[j+1] = t;
29                                      s++;

还可以在list命令后面加上行号来查看某一行附近的代码,例如想查看第十行附近的代码,使用list 10命令:

(gdb) list 10
5               char data [4096];
6               int key;
7       } item;
8
9       item array[] = {
10              {"bill", 3},
11              {"neil", 4},
12              {"john", 2},
13              {"rick", 5},
14              {"alex", 1}

这样,我们就定位了代码中出现错误的位置。此外我们还可以通过backtrace命令来查看函数调用的栈信息,这样就可以看到代码是怎样通过一层层的函数调用执行到这个地方的,例如对于这个程序:

(gdb) backtrace
#0  sort (a=0x404040 <array>, n=5) at debug.c:25
#1  0x0000000000401359 in main () at debug.c:37

如果需要了解更多gdb中可以使用的命令,可以使用help命令,其效果如下:

(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 all" for the list of all commands.
Type "help" followed by command name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

这里只是列出了命令的分类,有断点相关的指令breakpoints,也有栈相关的指令stack,输入help加种类名,就可以查看该类别下的所有命令,例如要查看断点相关的指令,只需要输入help breakpoints

(gdb) help breakpoints
Making program stop at certain points.

List of commands:

awatch -- Set a watchpoint for an expression
break -- Set breakpoint at specified location
break-range -- Set a breakpoint for an address range
catch -- Set catchpoints to catch events
catch assert -- Catch failed Ada assertions
catch catch -- Catch an exception
catch exception -- Catch Ada exceptions
catch exec -- Catch calls to exec
catch fork -- Catch calls to fork
catch handlers -- Catch Ada exceptions
catch load -- Catch loads of shared libraries
catch rethrow -- Catch an exception
catch signal -- Catch signals by their names and/or numbers
catch syscall -- Catch system calls by their names
catch throw -- Catch an exception
catch unload -- Catch unloads of shared libraries
catch vfork -- Catch calls to vfork
clear -- Clear breakpoint at specified location
commands -- Set commands to be executed when the given breakpoints are hit
condition -- Specify breakpoint number N to break only if COND is true
delete -- Delete some breakpoints or auto-display expressions
--Type <RET> for more, q to quit, c to continue without paging--

因为指令太多,一页打印不下,还可以按回车键查看更多。

我们调试程序的时候用的最多的功能就是断点,下面修改程序,让其能够正确执行,在利用断点功能查看程序运行的过程。修改程序的时候,只需要修改出错地方for循环的控制变量n变成n-1即可。修改完之后重新编译,记得加上g选项。

编译完成后即可用gdb程序调试程序,这时如果执行run命令,程序可以正常执行,这样程序就不会中途停止。像这样:

(gdb) run
Starting program: /media/scholar/软件1/资料/LinuxProgramming/5-Debug/debug 
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
[Inferior 1 (process 6676) exited normally]

修改之后程序能正常运行完,并输出正确结果,所以程序不会中途停止,但是我们可以打断点来让程序在断点出暂停。使用break num可以让程序在第num行暂停。

冒泡排序通过n-1次迭代,每次让最大的元素到达顶部,从而经过n-1次迭代之后,数组会被排序,为了观察这一过程,我们在程序的第23行打断点(使用命令break 23),让程序在每一次迭代之前先暂停,以便查看冒泡的过程:

(gdb) break 23
Breakpoint 1 at 0x401154: file debug.c, line 23.

添加了断点之后,可以通过info break来查看添加的所有断点:

(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401154 in sort at debug.c:23

还可以通过delete break指令删除断点,如果后面不带序号,则删除所有断点,否则删除序号指定的断点:

(gdb) delete break 1

有了断点之后,我们再用run命令运行程序,程序就会在断点处停下来:

(gdb) run
Starting program: /media/scholar/软件1/资料/LinuxProgramming/5-Debug/debug 

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;

可以看到程序在第23行暂停了,并输出该行的代码。这个时候我们可以通过print命令来查看变量的值,print命令之后基本上可以接所有C语言中的变量,例如变量名s,数组元素a[0],还有特殊的a[0]@5查看数组从04的所有元素。例如:

(gdb) print i
$2 = 0

可以看到当前i = 0

当获取到我们当前想要的信息之后,我们就可以使用cont命令让程序继续执行,在gdb中按回车就会执行上一次执行的内容,所以可以很方便的迅速到达下一个断点:

(gdb) cont
Continuing.

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;

这是for循环第二次到达这个地方,我们可以继续通过指令查看当前的变量信息和控制程序运行。

如果我们要关注某一个变量在每次执行到断点时值的变化,可以添加一个display来实现,display指的是每次执行到断点是要打印的内容,例如,如果我们要查看经过每次冒泡之后数组的状态,就可以添加这样一个display来实现,display命令后面接的内容跟print一样,如果要查看整个a数组,就是这样display a[0]@5

# 开始调试程序
(gdb) run
Starting program: /media/scholar/软件1/资料/LinuxProgramming/5-Debug/debug 

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;

# 添加display
(gdb) display a[0] @ 5
3: a[0] @ 5 = {{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}}

# 让程序继续执行   
(gdb) cont
Continuing.

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
3: a[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}}

# 按回车,执行上一条命令
(gdb) 
Continuing.

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
3: a[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}}


# 按回车,执行上一条命令
(gdb) 
Continuing.

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
3: a[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}}


# 按回车,执行上一条命令
(gdb) 
Continuing.

Breakpoint 2, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
3: a[0] @ 5 = {{data = "alex", '\000' <repeats 4091 times>, key = 1}, {
    data = "john", '\000' <repeats 4091 times>, key = 2}, {data = "bill", '\000' <repeats 4091 times>, 
    key = 3}, {data = "neil", '\000' <repeats 4091 times>, key = 4}, {
    data = "rick", '\000' <repeats 4091 times>, key = 5}}


# 按空格,执行上一条命令
(gdb) 
Continuing.
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
[Inferior 1 (process 7373) exited normally]

这样每次只需要输入cout命令,就可以让程序自动打印想要查看的信息。displaybreak一样,可以设置和删除,同样使用info命令来查看已经设置的display,例如info display,也可以用delete删除display,其用法跟break一样。

虽然上面的操作已经够简单,每次执行到断点,只需要按空格就可以让程序继续执行,并打印想要知道的信息,但是如果程序到达断点的次数太多,每次都按空格还是有点繁琐,于是gdb提供了commands命令,这个命令使程序每次到达断点的时候都自动执行一系列命令,知道程序执行完毕。我们可以用这个命令让程序自动打印a数组,这一次连空格都不用按!!!

# 先设置断点,让程序能够正确暂停
(gdb) break 23
Breakpoint 1 at 0x401154: file debug.c, line 23.

# 运行程序
(gdb) run
Starting program: /media/scholar/软件1/资料/LinuxProgramming/5-Debug/debug 

Breakpoint 1, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;

# 设置自动打印
(gdb) display a[0]@5
1: a[0]@5 = {{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}}

# 使用commands命令添加每次到1断点时执行的指令,用end结尾表示输入结束
(gdb) commands 1
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
>cont
>end

# 让程序继续执行,在这之后由于设置了自动执行cont命令,所以之后的程序再碰到断点也不会暂停
(gdb) cont
Continuing.

Breakpoint 1, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
1: a[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}}

Breakpoint 1, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
1: a[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=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
1: a[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}}

Breakpoint 1, sort (a=0x404040 <array>, n=5) at debug.c:23
23                      s = 0;
--Type <RET> for more, q to quit, c to continue without paging--
1: a[0]@5 = {{data = "alex", '\000' <repeats 4091 times>, key = 1}, {
    data = "john", '\000' <repeats 4091 times>, key = 2}, {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] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
[Inferior 1 (process 7553) exited normally]

断言

在进行程序开发的时候,我们总是在各种假设的基础上进行开发的,例如传入一个函数的变量不为零,又或者该函数一定会返回一个非零的值。但是如果程序没有按照我们实现设想的方式去执行,担忧不至于导致程序崩溃,这样就会导致程序输出错误的结果。但又因为程序还是没有崩溃,执行完了,所以这样的错误会变得很难捕捉,这样的错误也称为逻辑错误。

断言的作用就是捕捉这些逻辑错误,例如一个本应该输出非零值的程序却输出了零,我们就让程序退出。这样就能让程序及时终止,以发现错误。

X/Open提供了断言函数assert()来实现这种功能,它的函数定义如下:

#include <assert.h>

void assert(int expression);

这个函数判断参数中的表达式是否成立,如果成立,则继续让程序执行,函数不做任何事。否则,便调用abort()函数来终止程序的运行,并输出一些错误信息。

此外,这个函数是否工作还取决于是否定义了宏NDEBUG,也就是Not Debug,如果定义了这个宏,则assert()将停止工作。所以我们可以在真正运行程序的时候加上这个宏,而在需要测试的时候将这个宏删除,以让assert()在正确的时间工作。

下面是一个例子:

#include <stdio.h>
#include <math.h>
#include <assert.h>

double sq(double x){
	assert(x >= 0.0);
	return sqrt(x);
}

int main(void){
	printf("sqrt +4: %lf\n", sq(4));
	printf("sqrt -4: %lf\n", sq(-4));
	return 0;
}

gcc编译的時候记得加上-lm选项,否则链接程序会找不到sqrt()函数。编译并运行程序会得出这样的报错:

sqrt +4: 2.000000
a: assert.c:6: sq: Assertion `x >= 0.0' failed.
已放弃

原因是当输入一个负数给sq()函数时,函数里面的断言失败了,于是终止了程序的运行,并打印出错误信息。如果不使用这样的断言,程序会运行完成,但是会给出错误的结果。这里可以用上面说过的NDEBUG宏让assert()失效,只要在编译文件的时候加入-DNDEBUG即可添加宏:

gcc assert.c -DNDEBUG -lm -o a

编译程序并运行之后会得到这样的输出:

sqrt +4: 2.000000
sqrt -4: -nan

可以看到程序虽然正常运行完了,没有崩溃,但是给出了错误的结果。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值