C语言入门(21)——使用DBG对C语言进行调试

 

程序中除了一目了然的Bug之外都需要一定的调试手段来分析到底错在哪。到目前为止我们的调试手段只有一种:根据程序执行时的出错现象假设错误原因,然后在代码中适当的位置插入printf,执行程序并分析打印结果,如果结果和预期的一样,就基本上证明了自己假设的错误原因,就可以动手修正Bug了,如果结果和预期的不一样,就根据结果做进一步的假设和分析。
我们介绍一种非常强大的调试工具gdb,可以完全操控程序的运行,并且随时可以查看程序中所有的内部状态,比如各变量的值、传给函数的参数、当前执行的语句位置等。调试的基本思想仍然“分析现象->假设错误原因->产生新的现象去验证假设”这样一个循环,根据现象如何假设错误原因,以及如何设计新的现象去验证假设,这都需要非常严密的分析和思考,如果因为手里有了强大的工具就滥用,而忽视了严谨的思维,往往会治标不治本地修正Bug,导致一个错误现象消失了但Bug仍然存在,甚至是把程序越改越错。

单步执行和跟踪函数调用

看下面的程序:

#include <stdio.h>

int add_range(int low, int high)
{
	int i, sum = 0;
	for (i = low; i <= high; i++)
		sum = sum + i;
	return sum;
}

int main(valoid)
{
	int result[100];
	result[0] = add_range(1, 10);
	result[1] = add_range(1, 100);
	printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
	return 0;
}


 

add_range函数从low加到high,在main函数中首先从1加到10,把结果保存下来,然后从1加到100,再把结果保存下来,最后打印出的两个结果是:

 

结果显然不正确,在小学我们就学了高斯小时候的故事,从1加到100应该是5050。下面通过gbd把错误找出来。

 

在编译时要加上-g选项,生成的目标文件才能用gdb进行调试:

(gdb) -g选项的作用是在目标文件中加入源代码的信息,比如目标文件中第几条机器指令对应源代码的第几行,但并不是把整个源文件嵌入到目标文件中,所以在调试时目标文件时必须保证gdb也能找到源文件。gdb提供一个类似shell的命令行环境,上面的(gdb)就是提示符,在这个提示符下输入help可以查看命令的类别:

 

可以进一步查看某一类别中有哪些命令,例如查看files类别下有哪些命令可以用:

  

现在试试用list命令从第一行开始列出源代码:

 

一次只列10行,如果要从11行开始继续列源代码可以输入

(gdb) list

 

也可以什么都不输直接敲回车,gdb提供了一个很方便的功能,在提示符下直接敲回车表示用适当的参数重复上一条命令。

 

gdb的很多常用命令有简写形式,例如list命令可以写成l,要列一个函数的源代码也可以用函数名做参数:

 

 

gcc的-g选项并不是把源代码嵌入到目标文件中的,在调试目标文件时也需要源文件。我们继续调试。首先用start命令开始执行程序:

 

 

这表示停在main函数中变量定义之后的第一条语句处等待我们发命令,gdb列出这条语句表示它还没执行,并且马上要执行。我们可以用next命令(简写为n)控制这些语句一条一条地执行:

 

用n命令依次执行两行赋值语句和一行打印语句,在执行打印语句时结果立刻打出来了,然后停在return语句之前等待我们发命令。虽然我们完全控制了程序的执行,但仍然看不出哪里错了,因为错误不在main函数而在add_range函数,现在用start命令重新来过,这次用step命令(简写为s)进入函数中去执行:


这次停在了函数中变量定义之后的第一条语句处。在函数中有几种查看状态的办法,backtrace命令(简写为bt)可以查看函数调用的栈帧:

 

可见当前的add_range函数是被main函数调用的,main传进来的参数是low=1,high=10。main函数的栈帧编号为1,add_range的栈帧编号为0。现在可以用info命令(简写为i)查看add_range局部变量的值:

 

 

用s或n往下走几步,然后用print命令(简写为p)打出变量sum的值:

 

根据上面两步的结果我们可以看到,变量sum没有做初始化,所以直接导致后面的运算结果错误。

 

这时候我们已经找到错误原因,可以退出gdb修改源代码了。

 

代码修改如下:

#include <stdio.h>
 
int add_range(int low, int high)
{
         inti, sum = 0;
         for(i = low; i <= high; i++)
                   sum= sum + i;
         returnsum;
}
 
int main(void)
{
         intresult[100];
         result[0]= add_range(1, 10);
         result[1]= add_range(1, 100);
         printf("result[0]=%d\nresult[1]=%d\n",result[0], result[1]);
         return0;
}


 

修改完毕重新编译并运行:

发现结果正确了。

下面是常用的gdb指令:

 

backtrace(或bt) 查看各级函数调用及参数

finish 执行到当前函数返回,然后停下来等待命令

frame(或f) 帧编号 选择栈帧

info(或i) locals 查看当前栈帧局部变量的值

list(或l) 列出源代码,接着上次的位置往下列,每次列10行

list 行号 列出从第几行开始的源代码

list 函数名 列出某个函数的源代码

next(或n) 执行下一行语句

print(或p) 打印表达式的值,通过表达式可以修改变量的值或者调用函数

set var 修改变量的值

start 开始执行程序,停在main函数第一行语句前面等待命令

step(或s) 执行下一行语句,如果有函数调用则进入到函数中