1.调试技术的几个准则
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命令行最基本操作
实例:插入排序算法调试
用伪代码描述这个过程如下:
拟调试代码如下:
//
// 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)
我们可以看到程序停止在第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单步调试发现我们跳到了该函数的出口处:
此时我们看看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函数:
我们发现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
0Program 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进行调试。
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.csdn.net/gnuhpc
1.让程序停下来的三种模式
注:
- GDB文档中统称这三种程序暂停手段为breakpoint,例如在GDB的delete命令的帮助手册中就是这么描述的,它实际上指代的是这三种暂停手段,本文中以breakpoints统称三种模式,以中文进行分别称呼。
- GDB执行程序到断点(成为断点被hit)时,它并没有执行断点指向的那一行,而是将要指向断点指向的那一行。
- GDB是以机器指令为单位进行执行的,并非是以程序代码行来进行的,这个可能会带来一些困惑,下文有例子详述。
2.GDB breakpoints的查看
命令:i b = info breakpoints。返回列表每一列的含义如下:
3.GDB 程序控制的设置
1: int main(void)
2: {
3: int i;
4: i=3;
5: return 0;
6: }
我们不使用编译器优化进行编译,然后加载到GDB中,如下:
$ gcc -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x6: file test1.c, line 4.
我们发现显然#4并非是main函数的入口,这是因为这一行是该函数第一行虽然产生了机器码,但是GDB并不认为这对调试有帮助,于是它就将断点设置在第一行对调试有帮助的代码上。
我们使用编译器优化再进行编译,情况会更加令人困惑,如下:
$ gcc -O9 -g3 -Wall -Wextra -o test1 test1.c
$ gdb test1
(gdb) break main
Breakpoint 1 at 0x3: file test1.c, line 6.
GCC发现i变量一直就没有使用,所以通过优化直接忽略了,于是程序第一行产生机器码的代码恰好是main函数的最后一行。
因此,建议在不影响的情况下,程序调试时将编译器优化选项关闭。
1: #include
2:
3: int main(int argc, char **argv)
4: {
5: int x = 30;
6: int y = 10;
7:
8: x = y;
9:
10: return 0;
11: }这是个非常简单的程序,在main函数入口处断点被hit后我们可以设置rwatch x进行变量监视。
1: ...previous code...
2: int i = 9999;
3: while (i--) {
4: printf("i is %d/n", i);
5: ... lots of code ...
6: }
7: ...future code...
1: #include
2: void foo() {
3: printf("inside foo()");
4: int x = 6;
5: x += 2;
6: }
7:
8: int main() {
9: int x = 0;
10: x = x+2;
11: foo();
12: printf("x = %d/n", x);
13: x = 4;
14: return(0);
15: }
16:我们编译一下然后在main函数处设置断点,然后使用record命令开始记录,这是使用反向调试必须的开始步骤,最后进行两步单步调试,打印x的值:
(gdb) b main
Breakpoint 1 at 0x804840d: file test.c, line 9.
(gdb) record
Process record: the program is not being run.
(gdb) r
Starting program: /home/gnuhpc/test
Breakpoint 1, main () at test.c:9
9 int x = 0;
(gdb) record
(gdb) n
10 x = x+2;
(gdb)
11 foo();
(gdb) print x
$1 = 2
(gdb) reverse-next
10 x = x+2;
(gdb) p x
$2 = 0
(gdb) b 15
Breakpoint 2 at 0x8048441: file test.c, line 15.
(gdb) c
Continuing.
inside foo()x = 2
Breakpoint 2, main () at test.c:15
15 }
(gdb) b foo
Breakpoint 3 at 0x80483ea: file test.c, line 3.
(gdb) reverse-continue
Continuing.
Breakpoint 3, foo () at test.c:3
3 printf("inside foo()");
(gdb) watch x
Hardware watchpoint 4: x
(gdb) reverse-continue
Continuing.
Hardware watchpoint 4: x
Old value = 6
New value = 134513384
foo () at test.c:4
4 int x = 6;
(gdb) n
Hardware watchpoint 4: x
Old value = 134513384
New value = 6
foo () at test.c:5
5 x += 2;
commands breakpoint-id
...
commands
...
end
1: #include
2: int fibonacci(int n);
3: int main(void)
4: {
5: printf("Fibonacci(3) is %d./n", fibonacci(3));
6: return 0;
7: }
8: int fibonacci(int n)
9: {
10: if(n<=0||n==1)
11: return 1;
12: else
13: return fibonacci(n-1) + fibonacci(n-2);
14: }由于这是一个递归函数,我们为了追寻递归,要查看程序以什么顺序调用fibonacci()传入的值,当然你可以使用printf进行查看,只是这个方法 看起来很土。我们如下进行调试:(gdb) break fibonacci然后设置命令列表:(gdb) command 1
(gdb) define print_and_go
Redefine command "print_and_go"? (y or n) y
Type commands for definition of "print_and_go".
End with a line saying just "end".
>printf $arg0, $arg1
>continue
>end
使用的时候非常方便:
(gdb) commands 1
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just "end".
>silent
>print_and_go "fibonacci() was passed %d/n" n
>end
附注:后文将介绍catchpoint的用法和实例,在此略过。
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.csdn.net/gnuhpc
本文首先以一个二叉树插入算法的实现作为例子说明GDB查看程序数据的相关方法,代码如下:
1: // bintree.c: routines to do insert and sorted print of a binary tree
2:
3: #include
4: #include
5:
6: struct node {
7: int val; // stored value
8: struct node *left; // ptr to smaller child
9: struct node *right; // ptr to larger child
10: };
11:
12: typedef struct node *nsp;
13:
14: nsp root;
15:
16: nsp makenode(int x)
17: {
18: nsp tmp;
19:
20: tmp = (nsp) malloc(sizeof(struct node));
21: tmp->val = x;
22: tmp->left = tmp->right = 0;
23: return tmp;
24: }
25:
26: void insert(nsp *btp, int x)
27: {
28: nsp tmp = *btp;
29:
30: if (*btp == 0) {
31: *btp = makenode(x);
32: return;
33: }
34:
35: while (1)
36: {
37: if (x < tmp->val) {
38:
39: if (tmp->left != 0) {
40: tmp = tmp->left;
41: } else {
42: tmp->left = makenode(x);
43: break;
44: }
45:
46: } else {
47:
48: if (tmp->right != 0) {
49: tmp = tmp->right;
50: } else {
51: tmp->right = makenode(x);
52: break;
53: }
54:
55: }
56: }
57: }
58:
59:
60: void printtree(nsp bt)
61: {
62: if (bt == 0) return;
63: printtree(bt->left);
64: printf("%d/n",bt->val);
65: printtree(bt->right);
66: }
67:
68:
69: int main(int argc, char *argv[])
70: {
71: root = 0;
72: for (int i = 1; i < argc; i++)
73: insert(&root, atoi(argv[i]));
74: printtree(root);
75: }
在调试这个二叉树插入程序的时候,我们会非常关心insert方法的执行情况,在进入那个while(1)循环后,我们可能会做以下的操作:
(gdb) p tmp->val
$1=12
(gdb) p tmp->left
$2 = (struct node *) 0x8049698
(gdb) p tmp->right
$3 = (struct node *) 0x0
这个操作显得累赘又麻烦,我们可以有以下的改进措施:
1.直接打印结构体tmp:
(gdb) p *tmp
$4 = {val = 12, left = 0x8049698, right = 0x0}
2.使用display命令:我们在#37设置断点,然后运行程序,待程序运行至该断点停下后使用display = disp 命令对某一个变量进行监视(之所以这样做是因为这个变量必须存在在该栈帧上,也就是说调试的时候这个变量的确被创建并且没有被销毁),程序以后只要一停止 就打印这个变量的值在屏幕上:
(gdb) disp *tmp
1: *tmp = {val = 12, left = 0x8049698, right = 0x0}
(gdb) c
Continuing.
Breakpoint 1, insert (btp=0x804967c, x=5) at bintree.c:37
37 if (x < tmp->val) {
1: *tmp = {val = 8, left = 0x0, right = 0x0}
也可以使用dis disp 1使这个监视动作失效(enable disp 1则恢复),undisp 1为删除。info display为查看当前所有自动打印点相关的信息
3.使用命令列表:在上篇中已经叙述,在此不再赘述。
4.使用call命令:我们在代码中已经有了一个打印整个树的函数printtree,使用call命令我们可以直接利用代码中的方法进行变量监视,在每次insert完成的时候调用printtree对二叉树进行打印:
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just "end".
>printf "*********** current tree ***********"
>call printtree(root)
>end
5.使用DDD的Data Window图形化表示:单击右键在root这个变量上然后选择display *root,每次在#37行停下时,在Data Window内对整个树的都有图形化表示,在左右子树上,你可以使用右键单击然后选择Display *()来显示。(Tips:你可以以--separate参数启动DDD,这样每个Window都是独立的,你可以获得更大的视野)。
补充:
1.打印数组:p *pointer@number_of_elements,其中number_of_elements表示显示pointer这个变量中的几个成员。另外一种方式是类型转换,例如下列程序:
1: int *x;
2: main()
3: {
4: x = (int *) malloc(25*sizeof(int));
5: x[3] = 12;
6: }
除了可以使用:
(gdb) p *x@25
$1 = {0, 0, 0, 12, 0 }
我们还可以使用:
(gdb) p (int [25]) *x
$2 = {0, 0, 0, 12, 0 }
2.打印本地变量:info locals,会打印当前栈帧的本地变量。
3.以不同形式打印变量:p/paramenters variable parameters 可以是 x 表示打印变量以十六进制表示,f为浮点,c为character,s为string。
4.打印历史查看过的变量:使用$number,而只使用$表示上一个变量。
(gdb) p tmp->left
$1 = (struct node *) 0x80496a8
(gdb) p *(tmp->left)
$2 = {val = 5, left = 0x0, right = 0x0}
(gdb) p *$1
$3 = {val = 5, left = 0x0, right = 0x0}(gdb) p tmp->left
$1 = (struct node *) 0x80496a8
(gdb) p *$
$2 = {val = 5, left = 0x0, right = 0x0}
5.修改被调试程序运行时的变量值:set x = 12。
6.利用自定义变量方便调试:例如,
1: int w[4] = {12,5,8,29};
2: main()
3: {
4: w[2] = 88;
5: }
我们设置i,然后利用这个变量对这个数组进行遍历:
(gdb) set $i = 0
(gdb) p w[$i++]
$1=12
(gdb)
$2=5
(gdb)
$3=88
(gdb)
$4=29
7.强制类型打印
p {type}address:把address指定的内存解释为type类型(类似于强制转型,更加强)
8.设置一些常见选项
参考文献:
《Art of Debugging》
《Linux® Debugging and Performance Tuning: Tips and Techniques》
Author:gnuhpc
WebSite:blog.csdn.net/gnuhpc