Linux环境下的C/C++基础调试技术

1.调试技术的几个准则

惊喜准则:找到错误是一种惊喜,心理上不要畏惧而是要怀着感恩的心去面对。
从小处开始准则:刚开始测试的使用从小处着手,暂时不涉及边界数据,虽然这样可能会掩盖一些Bug,但是这样或许能查到最主要的Bug,例如你的程序包含了一个巨大的循环体,最容易发现的Bug在第一个循环或第二次循环执行的时候。
自顶向下准则:优先选择step over而不是step into,以节省时间。
Segmentation Fault准则:出现段错误时,第一个想到的不应该是printf而是Debugger,因为在调试器中你能看到你的哪一行代码导致了错误,更重要的是你可以通过backtrace等工具得到更多有用的信息。
折半查找准则:在寻找bug时可以充分利用编辑器等工具来进行折半查找,具体在后边有例子说明。
2.Linux下代码调试工具

主要使用的GDB,以及基于GDB的图形化工具,如DDD或eclipse,选择上看个人习惯了。

命令行式的GDB启动较快,可以在ssh终端下使用,操作简洁,并且在调试GUI程序时不会崩溃,但较之图形化则在单步调试或设置断点时非常不方便。

当然你可以使用Vim等编辑器的插件或者补丁(clewn or vimGDB)来弥补这一缺憾,并且在GDB6.1以上的版本你可以使用GDB -tui这个模式(或者在GDB的命令行模式下按CTRL-x-a)打开一个类似于图形界面的文本界面模式,在这个界面中你可以使用上下键查看源代码(CTRL-P 和 CTRL-N完成输入过的命令的查看).

或者你还可以使用cGDB这个工具(很庆幸这个项目在停止了三年后又有人开始维护了),这个工具是将GDB用curses包装了一下,提供了一些很好用的feature(Esc和i键在代码和命令框间切换;在代码框中支持vim型的操作;在命令框中支持tab键补全命令;在移动到想加入断点的行(行号为高亮白色)直接用空格键,设定好后行号会变红;)。另外,在调试C-S程序时推荐使用eclipse。

在本文中,重点介绍ddd的操作,因为这个工具即结合了GDB命令行和图形界面的操作。其余请参阅各个工具的手册。

3.GDB命令行最基本操作

设置断点:b LineNumber
运行程序:r args1 args2 ...
彻底终止程序:kill
单步执行:n(TIPs1:可以按回车重复上一次操作,在单步调试时这个feature很有用)。
单步进入:s
继续执行:c
设置临时断点:tb LineNumber 可以理解为一次性断点,与断点不同,临时断点只在第一次执行时起作用。
查看变量:p
设置观察点:
w Expression,当Expression是一个变量名时,这个变量变化时会停止执行;你也可以使用条件来限定,比如w (z>28),当z大于28时,程序停止。注意观察点一般使用在更大范围上的变量,而不是本地变量,因为在局部变量上设置的观察点在局部结束时(比如该变量所在的函数执行结束时)就被取消了。
当然这并不包含main的情况,因为main函数执行结束后程序就结束了。
查看栈帧:
栈帧指的是在一个函数调用时,该函数调用的运行信息(包含本地变量、参数以及函数被调用的位置)存储的地方。每当一个函数被调用时, 【6688电子商务网站 www.6688.cc 】一个新的帧就被系统压入一个由系统维护的帧,在这个栈的顶端是现在正在运行的函数信息,当该函数调用结束时被弹出并析构。
在GDB中,frame 0为当前帧,frame 1为当前帧的父帧,frame 2为父帧的父帧,等等,用down命令则是反向的。这是一个很有用的信息,因为在早期的一些帧中的信息可能会给你一些提示。
backtrace查看整个帧栈
注意:在帧中来回并不影响程序的执行。
实例:插入排序算法调试

用伪代码描述这个过程如下:

Linux环境下的C/C++基础调试技术

拟调试代码如下:

//

// insertion sort,

//

// usage:  insert_sort num1 num2 num3 ..., where the numi are the numbers to

// be sorted

int x[10],  // input array

    y[10],  // workspace array 

    num_inputs,  // length of input array

    num_y = 0;  // current number of elements in y

void get_args(int ac, char **av)

{  int i;

   num_inputs = ac - 1;

   for (i = 0; i < num_inputs; i++)

      x[i] = atoi(av[i+1]);

}

void scoot_over(int jj)

{  int k;

   for (k = num_y-1; k > jj; k++)

      y[k] = y[k-1];

}

void insert(int new_y)

{  int j;

   if (num_y = 0)  { // y empty so far, easy case

      y[0] = new_y;

      return;

   }

   // need to insert just before the first y

   // element that new_y is less than

   for (j = 0; j < num_y; j++)  {

      if (new_y < y[j])  {

         // shift y[j], y[j+1],... rightward

         // before inserting new_y

         scoot_over(j);

         y[j] = new_y;

         return;

      }

   }

}

void process_data()

{

   for (num_y = 0; num_y < num_inputs; num_y++)

      // insert new y in the proper place

      // among y[0],...,y[num_y-1]

      insert(x[num_y]);

}

void print_results()

{  int i;

   for (i = 0; i < num_inputs; i++)

      printf("%d\n",y[i]);

}

int main(int argc, char ** argv)

{  get_args(argc,argv);

   process_data();

   print_results();

}
我们编译一下:

gcc -g -Wall -o insert_sort ins.c

注意我们要使用-g选项告诉编译器在可执行文件中保存符号表——我们程序中变量和代码对应的内存地址。

现在我们开始运行一下,我们使用“从小处开始准则”,首先使用两个数进行测试:

./insert_sort 12 5

我们发现该程序没有退出,貌似进入了一个死循环。我们开始使用ddd调试这个程序:

ddd insert_sort

运行程序,传入两个参数:

r 12 5

此时程序一直运行不退出,按Ctrl+C暂停程序的执行

(GDB) r 12 5
^C
Program received signal SIGINT, Interrupt.
0x080484ff in insert (new_y=3) at insert_sort.c:45
/home/gnuhpc/MyCode/Debug/Chapter_01/insert_sort/pg_019/insert_sort.c:45:939:beg:0x80484ff
(GDB)

Linux环境下的C/C++基础调试技术

我们可以看到程序停止在第49行。我们看一下num_y现在的值:

(GDB) p num_y
$1 = 1

这里的$1指的是你要GDB告诉你的第一个变量。找到了这个地方后,我们看看在num_y=1时都发生了什么,我们在insert函数(第27行)设置断点(你也可以直接使用break insert在这个函数的入口设置断点),并且设置GDB在断点1处(你可以通过info break命令查看断点)只当num_y==1时才停止:

(GDB) b 27
Breakpoint 1 at 0x80484a1: file insert_sort.c, line 27.
(GDB) condition 1 num_y==1
(GDB)

上述命令也可以使用break if合一:

(GDB) break 27 if num_y==1

然后再运行程序,随后用n单步调试发现我们跳到了该函数的出口处:

Linux环境下的C/C++基础调试技术

此时我们看看num_y的值,以便查看到底这个for循环执行的情况。

(GDB) p num_y
$2 = 0

此时的情况是我们进入这个函数时num_y为1,但是现在num_y为0,在中间这个变量被改变了。现在你知道Bug就在30-36行间。同时,通过单步调试你发现31-33行被跳过了,34、35行为注释,那么Bug就在第30或第36行间了。

我们现在仔细看这两行就能得出结论了:30行有个典型的if判断条件写成赋值的错误,致命的是这个变量是全局变量,直接导致49行的for循环变量一直被重置。我们修改后重新编译(可以另开一个编辑器,不用退出ddd),然后再运行

(GDB) r 12 5
5
0

虽然没有了死循环,但是结果还是不对的。

请注意,初始的时候数组y是空的,在#49进行第一次循环时,y[0]应该为12,在第二个循环中,程序应该挪动12为5腾出位置插入,但是此时这个结果看上去是5取代了12。

此时单步调试进入for循环,#37看y[0]的值,的确是12。我们执行到scoot_over函数时,根据自顶向下准则我们单步跳过,继续执行到#41,看看结果对错再决定是不是要单步进入scoot_over函数:

Linux环境下的C/C++基础调试技术

我们发现12根本就没有被移动,说明scoot_over函数有问题,我们去掉insert函数入口的断点,在scoot_over入口处设置断点,当num_y=1的时候终止:b scoot_over if num_y==1。进一步单步调试后发现这个#23的for循环就没有执行。

(GDB) p jj
$12 = 0
(GDB) p k
$13 = 0

我们看到是因为没有满足for循环条件而不能进入循环。在这里12应该从y[0]移动到y[1],那么我们确定是循环的初始化错误,应该为k = num_y,将这个地方修改后编译运行,程序出现段错误。我们清空所有的断点,然后在

(GDB) r

Program received signal SIGSEGV, Segmentation fault.
0x08048483 in scoot_over (jj=0) at insert_sort.c:24
(GDB)

这里指出在24行出现seg fault,那么要么k超过了数组界限,要么k-1为负的。打印一下k的值,我们就发现:

(GDB) p k
$14 = 992
(GDB)

远远超过k应该有的值。查看num_y 为1,说明在处理第二个要排序的数时出错,再打印jj值,发现为0,就发现我们的for循环k++应该改为k—。

编译运行,发现ok。但是运行多个数据就又出错了:

(GDB) r 12 5 19 22 6 1
1
5
6
12
0
0

Program exited with code 06.
(GDB)

我们看到结果中从19开始的排序都有问题,我们在for (j = 0; j < num_y; j++)  这一行行设置断点,条件为new_y==19的时候:

(GDB) break 36 if new_y==19
Breakpoint 10 at 0x80484b1: file insert_sort.c, line 36.
(GDB)

单步调试就发现我们没有对当要插入的元素大于所有元素时进行处理。在#44后加入y[num_y] = new_y;重新编译,运行程序正确,至此,我们通过一个简单的例子演示了一下如何使用GDB进行调试。