GDB调试
本小节将介绍Linux系统中使用的一种非常强大的调试工具——gdb。gdb可以逐条执行程序、操控程序的运行,并且随时可以查看程序中所有的内部状态,如各变量的值、传给函数的参数、当前执行的语句位置等,藉此判断代码中的逻辑错误。掌握了gdb的使用方法,Linux用户将能使用更多灵活的方式去调试程序。
下面将结合几个初学者易犯的错误实例,来讲解如何使用gdb调试程序。
案例37:本案例的代码实现一个针对数组的排序程序,其中包含:初始化数组、数组排序和数组打印这三部分功能。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <time.h>
4 #define N 5 //定义数组长度
5 void init_arr(int *arr, int len) //生成随机数数组
6 {
7 int i = 0;
8 for (i = 0; i < len; i++) {
9 arr[i] = rand() % 20 + 1;
10 }
11 }
12 void select_sort(int *arr, int len) //选择排序算法
13 {
14 int i, j, k, tmp;
15 for (i = 0; i< len-1; i++) {
16 k = j;
17 for (j = i+1; j < len; j++) {
18 if (arr[k] > arr[j])
19 k = j;
20 }
21 if (i != k) {
22 tmp = arr[i];
23 arr[i] = arr[k];
24 arr[k] = tmp;
25 }
26 }
27 }
28 void print_arr(int *arr, int len) //打印数组
29 {
30 int i;
31 for (i = 0; i < len; i++)
32 printf("arr[%d] = %d\n", i, arr[i]);
33 }
34 int main(void)
35 {
36 int arr[N];
37 srand(time(NULL)); //生成随机数种子
38 init_arr(arr, N); //生成数组
39 print_arr(arr, N); //打印原始数组
40 select_sort(arr, N); //数组排序
41 printf("----------- after sort ------------\n");
42 print_arr(arr, N); //打印排序后的数组
43 return 87;
44 }
执行此段程序,结果如下:
arr[0] = 10
arr[1] = 19
arr[2] = 20
arr[3] = 20
arr[4] = 12
----------- after sort ------------
arr[0] = 10
arr[1] = 12
arr[2] = 19
arr[3] = 20
arr[4] = 20
程序顺利执行,但按照预期,打印结果“after sort”所在行之后的部分,应为一个有序序列,输出结果显然并非如此,这说明程序的逻辑出现了错误。此时我们可以启用gdb调试工具,在代码中设置端点,逐步执行程序,再根据程序中变量值的变化,判断错误原因。
在启用gdb调试工具之前,首先需要在待调试的程序代码中加入调试信息。实现此操作的方法如下:
[itheima@localhost ~]$ gcc gdbtest.c –o app –g
即在gcc编译的基础上,添加选项“-g”,此时将会生成一个带有调试信息的可执行文件app。输出直接编译产生的文件gdbtset和带有调试信息的可执行文件app的详细信息,会发现文件app要比gdbtest大,多出的内容将用于程序调试。
之后便可使用gdb调试此段程序,使用的命令如下:
(gdb) gdb app
执行该命令之后,系统会输出gdb的版本号及其它相关信息,此时的命令提示由“[itheima@localhost ~]$”变为“(gdb)”。
与C语言等的调试步骤相同,在调试之前,需要先在代码中设置断点,因此应先列出程序代码。列出程序代码的命令如下:
list 行号
该命令表示,从指定行开始,列出所有代码。使用此命令后,程序中的前10行代码将被显示在屏幕上,此时可以使用“l”继续列出代码。
根据之前程序输出的结果,可以粗略判断出有错的代码应在排序函数中,因此可以在排序函数中设置断点。设置断点的命令如下:
b 行号
该命令表示在对应行设置一个断点。
假设想要查看代码中已经设置的断点,可以使用“info”命令,该命令的格式如下:
info b
执行此命令后,对应代码中设置的断点信息将会显示在屏幕上。此时程序中已设置断点的信息如下:
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040064f in select_sort
at gdbtest.c:20
该信息中主要包括:断点编号Num、断点状态Enb、断点地址Address以及断点在程序中所处的位置。
在设置断点时还可以指定条件,例如若想在i=5时设置断点,可以使用以下命令:
b 22 if i = 5
该命令表示当“i=5”时,在代码第22行设置一个断点,此时使用“info b”命令查看断点信息,显示结果如下:
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040064f in select_sort
at gdbtest.c:20
2 breakpoint keep y 0x0000000000400661 in select_sort
at gdbtest.c:22
stop only if i = 5
此时在第二个断点的相关信息之后,显示“stop only if i = 5”,表示当程序执行到i=5时,断点才会生效。
在断点的信息中,有一项为“Enb”,当此项显示为“y”时,表示断点生效。此项可通过命令“disable”设置为“n”,表示断点无效,其使用格式如下:
disable Num
其中的参数“Num”表示断点的编号。若要将断点的“Enb”状态重新修改为“y”,可以使用命令“enable”。
若在调试的过程中,发现设置的某些断点意义不大,可以将断点删除。删除断点的命令为“delete”,其使用格式如下:
delete Num
断点设置好之后,便可以再次运行程序,查看调试信息了。在gdb中运行程序的命令为“run”,输入此命令,程序将开始执行。
在遇到断点时,程序会停止,此时可以使用命令“p”查看当前状态下代码中变量的值,该命令的使用方法如下:
p 变量名
若希望程序继续向下执行,可以使用命令“s”,s即“step”,表示单步执行。使用命令s则会进入C函数内部,因C函数作为标准函数库,基本都不会出现错误,此时可以使用命令“n”跳过库函数检查。另外使用命令“finish”,也可以跳出当前函数,继续往下执行。
使用命令p时,变量的值仅会输出一次,若想在执行的过程中跟踪某个变量的值,使用这种方法显然比较麻烦。gdb中还提供了另外一个命令“display”,该命令的用法与p相同,但是程序每往下执行一句,需要跟踪的变量的值就会被输出一次。使用命令“undisplay”可以取消跟踪。
分析程序,发现在select_sort函数中需要跟踪的变量只有3个,即:i、j、k。使用display命令跟踪这三个变量。发现在程序执行的过程中,变量k的值一直为1,而正常情况下k应保存外层循环i的值,因此可以判断k的赋值应该有问题。观察代码,发现代码19行应为“k=i”。
若想结束调试,可以使用“continue”结束当前断点调试,再使用“quit”命令退出调试,回到命令窗口。
虽然gdb调试工具很强大,但其工作原理仍遵循“分析现象->假设错误原因->产生新现象->验证假设”这一基本思想。透过现象深入分析错误原因,针对假设的原因设计验证方法等,都需要严密的分析和思考,因此切不可过于依赖工具,而忽视了严谨思维的重要性。