调试
- 1、什么样的程序才能调试
- 2、初识gdb和cgdb
- 3、cgdb调试指令
- 3.1、断点
- 3.2、逐过程与逐语句
- 3.3、变量的观察
- 3.4、跳出循环的方法
- 3.5、其它指令
- 4、三种调试技巧
- 4.1、watch
- 4.2、set var
- 4.3、条件断点
1、什么样的程序才能调试
比如我们编写一个1~100求和的小程序,编译出test.exe文件:
#include<stdio.h>intSum(ints,inte){intret=0;for(inti=s;i<=e;++i){ret+=i;}returnret;}intmain(){intstart=1;intend=100;printf("I will begin.\n");intn=Sum(start,end);printf("running done, result is: [%d-%d]=%d\n",start,end,n);return0;}使用gdb开始调试:gdb [可执行文件名]:
但是,我们会发现,除了运行代码和退出调试,我们无法对当前程序进行调试:
这是因为,我们编译test.c的时候,并没有生成调试信息。想要生成调试信息,我们编译源文件的时候就需要带上-g选项:
这样得到的可执行文件,就可以进行调试了,因为带上了调试信息。意味着只有携带了调试信息的可执行文件,才能进行调试。还记得我们在vs下编写的代码有Debug和Release两个版本的选项吗?我们可以认为,不带调试信息的可执行文件是Debug版本的软件,而携带调试信息的可执行文件是Release版本的软件。
这里我们也可以发现,带上调试信息的可执行文件,文件大小相比不带调试信息的文件,变大了一些:
2、初识gdb和cgdb
使用gdb进行代码调试:gdb [可执行文件名]
这时,我们就可以展示代码、打断点、运行代码、单步调试、进入函数…输入quit可以退出调试。
但是,我们使用gdb的时候,必须手动打印出调试信息,才能观察调试过程。gdb的使用,似乎不比我们之前使用vs 2022下的调试器方便。
这时我们就可以使用另一个调试器:cgdb。执行:cgdb [可执行文件名]
cgdb下,调试控制台与代码展示是分离的,这就比较像vs 2022很方便我们观察;同时我们也能看到断点、指示运行到哪的小箭头,等等。
向上面这些调试器,本质上就是帮助我们找到问题所在的辅助工具。
3、cgdb调试指令
gdb调试太麻烦了,我们更常使用更方便的cgdb。
l: list,展示代码。
r: run,运行代码。
3.1、断点
断点的作用,是进行区域化的代码执行,帮助我们一步步缩小问题的范围,以确定问题的位置。
==b+行号: 给指定行打上断点。==我们也可以指定文件打断点。
info b: 查看断点信息。
d [断点编号]: 删除指定行的断点。
直接删除断点不是一个好习惯。我们建议保留调试痕迹,这时可以对断点进行使能。
disable [断点编号]: 禁用指定断点。
enable [断点编号]: 启用指定断点。
断点的编号设置也有点意思。我们打上三个断点:
我们删除断点2,然后再在代码19行把断点加回来。我们就会发现:
代码19行的断点倒是加回来了,然而断点的编号变为了4。原来,在一个调试周期中,断点的编号是线性递增的。上面示例中,删除了断点2,又加上了一个断点,那么断点的编号就不是2了而是4。
c: continue,执行到下一个断点,如果下面没有断点了就运行到结束。
我们不仅可以指定行号打上断点,还可以给函数打上断点:
b + [函数名]: 对函数的函数体第一行打上断点。
3.2、逐过程与逐语句
之前使用vs的时候,我们使用快捷键F10就可以逐过程执行代码,即遇到函数调用直接当作语句执行;使用F11就可以逐语句执行代码,即遇到函数调用就进入函数。
Linux下的逐过程执行指令是n(next),即遇到函数调用会直接跳过函数。要想进入函数内部,我们可以执行指令s(step)。
n, s指令的作用是:小范围内进行一步一步调试,进一步定位问题所在。
3.3、变量的观察
vs下有监视窗口,我们可以在监视窗口中输入一些变量,进而观察这些变量的变化情况。
Linux下cgdb中也有观察变量的指令:p(print)。
还有,p不仅可以可以打印变量,也可以打印表达式:
然而p指令用得也不方便:想查看变量值必须执行一次p指令,即p指令只能临时查看变量。
我们期望指定的变量能一直显示,那就执行指令:display + [变量名]。
如果我们想取消display显示的变量,执行指令:undisplay + [编号]。
3.4、跳出循环的方法
假设我们排查问题的时候进入了一个循环。当我们确定这个循环没有问题的时候,我们就想跳出循环。
第一种方法,就是在循环外打一个断点,然后执行c指令:
第一种方法可能比较麻烦。我们可以使用第二种方法,就是执行命令:until + [行号]。执行until语句,就会执行到指定行位置上。比如我们until指定到第11行,就可以跳出循环。
对于until,我们要注意三点:
- until只能向下跳。如果强行向上跳,默认执行当前函数直到结束。
- until不能跨函数。如果强行跨函数跳跃,默认执行当前函数直到结束。
- until会自动跳过空格。
我们还有第三种方法,就是执行指令:finish。finish作用到main函数以后调用的函数,会直接执行到函数结束,返回;而finish作用到main函数则会报警告:
3.5、其它指令
bt: 查看函数调用情况。
info i: 查看与进程相关的信息。
info locals: 查看当前作用域内的局部变量。
4、三种调试技巧
4.1、watch
watch指令可以监视指定的变量,一旦变量有变化,watch就会回显变化。
比如我们监视ret:
info b查看断点,我们会发现多了一个watchpoint,专门针对的是变量ret:
ret在Sum函数中自然是不断变化的。当ret变化时,watch就会回显ret的变化:
那么watch的作用就很明显了:watch可以用来对比变量变化的前后关系。其实watch还有一个作用,那就是监视不能被改变的变量,常用于监视不能随意改变的标志值,以及指针。
4.2、set var
我们对上面的求和小程序做一点改动,使得Sum既可以求1 ~ 100的和,也可以求-1 ~ -100的和:
#include<stdio.h>intSum(ints,inte){intflag=0;//int flag = -1;//int flag = 1;intret=0;for(inti=s;i<=e;++i){ret+=i;}returnret*flag;}intmain(){intstart=1;intend=100;printf("I will begin.\n");intn=Sum(start,end);printf("running done, result is: [%d-%d]=%d\n",start,end,n);return0;}运行上面代码,发现结果为0,不是我们的预期值。调试发现,flag被设置成了0。
当前我们验证问题的做法是:退出调试,修改代码,重新调试,以验证确实是flag出现了问题。可是这样做太麻烦了。
这时我们就可以使用set var指令:
set var的作用是方便我们验证问题的原因,应用于:
- 有标志值的地方(flag)
- if的条件上。比如某段代码中,程序执行始终不进入其中一个if判断语句,这时我们就可以使用set var修改if的判断条件。
4.3、条件断点
在一个循环周期中,我们发现前面很多步操作都没问题,就是某几步循环出现了一些小毛病。所以我们期望跳过正常的循环操作,定位到不正常的循环操作。这时我们就可以使用条件断点。
对于上面的循环,假设前29次循环正常,第30次循环出现了问题。那么我们就需要定位到第30次循环,执行:b 12 if i==30。接着continue到下一个断点,我们就会发现,此时i确实变为了30,我们就来到了第30次循环。
条件断点的存在,使得我们能够进行符合某种条件的Debug。
当前12行断点不存在,我们才执行上面的语句。那要是12行断点已经存在了呢?执行condition [断点编号] i==30。
温馨小提示
我们可能会遇到,调试时乱点了什么东西,之后调试命令行就什么也打不出来了。这时我们只需按i,即可恢复。