使用GDB(CGDB,DDD,Code::Blocks)调试C语言程序

使用GDB(CGDB,DDD,Code::Blocks)调试C语言程序

当我们进行复杂编程时,程序的调试必不可少,它可以帮助我们寻找程序中的漏洞,是保障计算机信息系统正确性的必不可少的步骤。

程序的调试在IDE中很方便了,但我们在Linux环境下学习C语言,那就需要我们在命令行下使用GDB对C程序进行调试。

工具安装:

  • Ubuntu 下使用sudo apt install -y vim gdb cgdb ddd安装工具
  • openEuler下使用sudo yum install -y vim gdb cgdb ddd安装工具

学习建议:Linux Bash下打开三个标签页

我们提倡在Linux命令行下学习C语言编程。学习时在Ubuntu Bash中通过Ctrl+Shift+T快捷键打开(tab):

  • 一个使用vim编辑代码(推荐同学们使用Vusual Studio Code编辑代码,在命令行中使用code xx.c 进行代码编辑)
  • 一个使用gcc编译运行代码;
  • 一个使用gdb调试代码。

我们可以通过Alt+1,Alt+2,Alt+3三个快捷键进行快速切换。

如下图所示,这样就不用在一个窗口中进行编辑、编译运行和调试的切换了,能提高效率。

如上图, 我们在Linux Bash中输入vim hello_gdb.c编辑调试示例代码:

#include <stdio.h>
int my_add (int i, int j);

void main()
{
        int i = 5;
        int j = 6;
        int sum = 0;
        sum = my_add(i,j);
        printf("%d\n", sum);

        sum = 0;
        for(i=0; i<100; i++)
        {
                sum = sum + 1;
        }
        printf("%d",sum);
}

int my_add (int i, int j)
{
        int sum;
        sum  = i + j;
        return sum;
}

代码编辑完,我们首先按esc,退出编辑模式并进入命令模式,之后按“:w”进行保存而不是“:wq”进行保存退出,这样在编译或调试中遇到问题就可以按Alt+1 进入第一个标签修代码了。

我们按Alt+2进入第二个标签,使用gcc -g hello_gdb.c对程序进行编译。注意gcc中-g参数是为了产生各种调试信息,一定要加上,否则无法调试。

我们按Alt+3进入第三个标签,使用ls命令,可以查看到我们刚刚编译成果的a.out文件

GDB是我们要使用的调试工具,它是命令行中使用的,功能很强大,也会有些问题。

CGDB和DDD都是基于GDB开发的。CGDB也是在命令行中使用的,使用方式与GDB基本一样,只是更方便查看程序代码。

DDD也是GNU开发的,也是基于GDB的,只是使用了可视化界面,不用记忆调试命令,比CGDB更加容易操作。

为了方便展示,接下来我将以CGDB工具为例进行演示,学会了CGDB,DDD自然就会了。

我们接下来在命令行中输入:cgdb a.out 来对刚刚进行编译的程序进行调试。

下面是进入cgdb调试的界面

调试基础

调试程序先要学会设置断点,这样才能让程序停在你感觉有问题的代码处进行排查。学习调试我们要学会设置四种断点:

  • 行断点
  • 函数断点
  • 条件断点
  • 临时断点

我们在GDB中输入help可以查看命令列表:

调试命令 作用
break 在源代码指定的某一行设置断点,其中xxx用于指定具体打断点位置
run 执行被调试的程序,其会自动在第一个断点处暂停执行。
continue 当程序在某一断点处停止后,用该指令可以继续执行,直至遇到断点或者程序结束。
next 令程序一行代码一行代码的执行。
step 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。
until 使程序运行直到退出循环体
print 打印指定变量的值,其中 xxx 指的就是某一变量名。
list 显示源程序代码的内容,包括各行代码所在的行号。
finish 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。
return 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。
jump 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。
quit 终止调试。

在GDB调试中,我们使用break命令来设置断点,其中b是break的缩写。例如,要设置断点,请输入GDB命令:

b main			# 在main函数开始处设置断点
b myadd			# 在myadd函数开始处设置断点
b 10			# 在程序的第10行设置断点

大家调试C代码时,养成通过先b main 设置main函数断点的习惯,防止程序一下子就运行完毕。

如上图,我们输入run命令(缩写:r)运行程序,程序会在main()的第一行i=5处停下。

此时我们可以使用step命令运行main函数下一步代码i=5,使用display i命令查看变量i的值。

如果我们未运行到相应的步骤就查看对应变量的值,就会发生下面这种状况

此时我们使用display命令查看变量j的值,但无法得到正确的值

不使用display命令,我们可以使用print命令查看变量的值

想要查看源程序运行到了什么位置,我们可以查看程序上方箭头指向到了哪一行。
如下图所示,该程序运行到第9行,注意,这里所指第9行的意思是指程序将要运行到第9行,但还没有运行。同时还要注意的是,第9行是一个函数调用。

我们继续输入step,我们发现代码跳入16行函数体中了:

一般说来,调试时遇到函数调用,我们先看调用结果对不对,结果正确,说明函数没有问题,就不用进入函数体了; 函数调用结果不对,我们才需要进入函数体进行调试。单步跟踪命令nextstep在执行一般语句时没有区别,在执行有函数调用的语句时:

  • next会把函数执行完,
  • step会进入函数体,

所以在调试时,单步执行我们要优先使用next,这样可以直接判断函数是否出错,效率比较高。

现在已经进入函数体了,这时我们可以运行finish把函数执行完,finish命令的效果是:结束当前正在执行的函数,并在跳出函数后暂停程序的执行。运行返回调用处,后面执行一般语句,你发现nextstep没有区别。

第13行到第16行是个循环,这两条语句单步执行起来有点费劲。

我们可以通过tb 17在第17行设个断点,然后运行ccont命令就会一下子把循环运行完并停在第十二行。tb 17在第17行设置一个临时断点,只用一次就没了。cont是continue的缩写,功能是运行到下一个断点处停止。

在GDB调试中,我们可以使用tbreak命令来设置1次性断点,就是说该断点使程序暂停之后,会自动消失。

如下图,首先我们输入delete命令来清空所有断点,也可以使用clear命令加对应行号,然后使用tbreak 9命令,在第9行设置临时断点


之后我们使用run命令,发现程序运行到第9行之后仍会停下来,但与break命令设置的断点不同的是,该断点使用过1次后主动消失了。

调试的时候如果只使用单步跟踪效率就太低了,比如一个奇怪的问题只有在循环中特殊值出现,
我们就可以使用条件断点了,例如,如果问题出现在i == 80 处,我们就需要用到条件断点了。
这里我们输入命令b 15 if i == 80,在第15行处创建一个条件断点。

如上图,我们设置条件断点后,输入run命令执行程序,发现程序会在断点处停下来,此时我们用print命令查看变量i的值,果然是我们设置的条件80。

如果我们想要快速跳出循环,这时我们就可以使用until命令。
until命令的效果是使程序运行直到退出循环体。

如下图,我们先使用delete删除断点,然后步进入循环内部,打印 i 的值我们可以得到i = 81

之后我们输入until命令,发现已经跳出循环,这时我们再进入循环,打印 i 的值,发现 i 的值已经改变为82,说明until命令可以让程序执行完程序所在的当次循环并停下。

最后,我们可以使用quitexit退出GDB

递归的学习

递归算法是一种直接或间接地调用自身的算法。在编写程序时,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。

递归用于解决形式相同,规模不同的问题,能用递归解决的问题都可以转化为循环。递归把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。用递归思想写出的程序往往十分简洁易懂。

递归程序有两个要点:递归公式和结束条件。我们以求整数的阶乘为例:


有了公式,代码就容易写出来了:

#include <stdio.h>
int fact(int n);
void main()
{
        int sum;
        sum = fact(5);
        printf("%d\n",sum);
}

int fact(int n)
{
        if(n==0)
        {
                return 1;
        }
        else
        {
                return n * fact(n-1);
        }
}

fact(5)的递推过程如下图:

进入调试,我们在函数fact()处设置断点,开始运行:

函数调用一次就会形成一个栈帧,我们在GDB中使用backtrace命令显示栈帧,用updown命令可以在栈帧之间跳转,此时使用cgdb就可以通过上方栈指针的变化明显感受到压栈和弹栈的操作。

举一反三

ddd

我们可以在命令行中使用ddd a.out 启动调试

ddd的界面如下图所示,在gdb,cgdb中的命令被一个工具条代替,不用记忆命令了,调试起来就方便了,与gdb,cgdb不同的是,设置断点要用右键选中行,然后选择Set BreakPoint

问题:如何设置四种断点?欢迎在评论区留言。

code::blocks

code::blocks中可以通过菜单Debug-> Start/Continue 或者快捷键F8启动调试,把鼠标光标放到放到左边的行号上,右键弹出菜单设置断点。左上有调试工具栏,可以完成gdb,cgdb的调试任务。

问题:如何设置四种断点?欢迎在评论区留言。

特别感谢

感谢梁辰鱼同学帮忙改写。

参考资料


欢迎关注“rocedu”微信公众号(手机上长按二维码)

做中教,做中学,实践中共同进步!

rocedu



如果你觉得本文对你有帮助,请点一下左下角的“好文要顶”和“收藏该文


posted @ 2022-09-21 05:35  娄老师  阅读(1949)  评论(2编辑  收藏  举报