C语言入门(21)——使用DBG对C语言进行调试
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) 执行下一行语句,如果有函数调用则进入到函数中