gdb
软件发布的模式有debug和release。
linux下我们编译好的代码,无法直接调试。
因为gcc/g++默认的工作模式是release模式。
我们在Makefile里翻译的时候加上-g选项:
这样我们也能生成一个可执行程序,且对比之前生成的release版本,可以看到体积变大了一些。
而如果是release版本:
可以看到不包含调试信息。
程序要调试,必须是debug模式。也就是说编译时要加-g选项
gdb未来调试程序必须调试携带调试信息的exe
gdb
gdb的学习分为三步走:快速认识gdb;gdb命令的学习;解决一下gdb难用的问题,三个debug技巧。
正文
那么我们现在在名为gdb的目录下创建一个mycode.c文件,写这样一段程序:
然后我们如果普通地去编译这段代码就是:
gcc mycode.c -o mycode
,然后因为我们在for循环括号中进行了定义变量,无法编译成功:
所以我们需要在编译时手动加上c99的标准:
gcc mycode.c -o mycode -std=c99
这样就能编译成功了,但是我们不想要release版本的可执行程序,而想要有调试信息的debug版本,所以我们还需要带上一个-g选项:
现在我们就能gdb mycode
,进入gdb环境了:
怎么退出gdb环境呢?
quit
进入gdb后第一件事,我们要查看我们所有的源代码:
list/l/l 文件名/l 行号
通过list或者简写l,就可以查看当前所有的源代码;
我们也可以通过l后面带文件名来指定我们要看的文件;
我们还可以在l后面带上行号 ,来指定从某一行开始显示,比如带个1就可以从头开始显示,但它不会显示全,我们再按下回车后会接着显示:
b 行号------打断点
我们通过b后面跟行号,可以给这一行打上断点;
也可以b先跟文件名,再跟行号,这样打断点,b mycode.c:20
;
还可以b先跟文件名,再跟函数名,这样打断点,b mycode.c:main
在VS中,我们是通过f9来打断点。
在我们打了断点的行上,还会发现字体直接变红了:
但是我们有直接查看断点的命令。
info b------查看断点
d 断点编号------删除断点
与打断点时的b跟着行号不同,我们不能d跟着行号来删除断点,而是要d跟着断点的编号来删除断点。
这个编号就是我们在info b命令之后看到的编号。
在删掉所有的断点,再次打断点并info我们会发现:
即使前3个断点我们都已经取消了,我们再重新打断点时仍然编号为4而不是从1重新开始,因为:
gdb不退出,断点编号依次递增。
退出重新gdb进来后打断点,就可以发现断点重新从1开始编号了。
r------让程序运行起来
全称是run,相当于f5,也就是调试并运行(我们一般会先打上断点)
(ctrl+f5在VS中是运行不调试)
我们可以看到程序运行起来之后自动在我们打了断点的这一行停下来了。
c------让程序不停下来直接跑完
cgdb可以动态呈现我们的代码
这个需要我们先安装一下
(centos7):sudo yum install -y cgdb
可以看到,cgdb的效果是上面是代码,下面是gdb的调试。
逐过程(VS下的f10;把函数当整体直接跳过)
然后我们按下n (全称是next)
可以看到,就运行到第17行了,可以看出是逐过程。
然后我们再按一次r,会发现并没有直接跑完程序,而是停在return 0;
之前,并且询问我们要不要重新调试:
按下y,发现又重新运行并再次停在了断点处:
逐语句(VS下的f11;进入函数具体调试)
s(全称是step)
可以发现,现在我们就进到Sum函数里了:
然后我们反复按s,就可以往下逐语句运行
其实我们不需要反复s也可以一条条往下执行,按回车就可以,这是因为gdb会记录最新的一条输入指令,按回车就是执行最近的那条指令。
小tips
注意gdb作为命令时后面跟的是可执行文件而不是源文件,否则我们没法成功进入gdb环境:
gdb mycode
每调用一次函数其实就是在把函数的栈帧结构入栈。
我们可以使用bt
来进行简单地查看调用栈。
从上面两张图,我们看到,result是临时变量除了作用域Sum函数会消耗,怎么还能把值给n?这是因为会先把值给寄存器,而我们说返回值具有常性也是因为这个寄存器无法修改。
我们把程序做一下反汇编:
在其中我们会看到如果我们的函数是有返回值的,每次函数调用完总是有这样一条mov
把eax寄存器的值mov到栈底寄存器加上偏移量里
栈底寄存器加上偏移量就是函数内部栈上的一个临时变量。
eax里放的就是函数的返回值。
我们可以看看Sum函数的反汇编里
return语句就是翻译成把rbp减4放到eax里。其实就是把返回值放到寄存器里。
然后未来再将通过寄存器把值写到返回值的变量里。
刚才那个寄存器加上偏移量就是main函数里的变量n
这条语句做了两件事。
call函数,然后将寄存器的值给变量n。
finish
我们可以在s进入调用的函数内部后输入finish直接结束函数调用,回到原本的函数里:
此时正要做的是把寄存器的值给n。
p
跟变量名
在这个时候我们进行p n
,可以查看此时n里面的值:
可以发现是个随机值,然后我们再按一次n,也就是next,逐过程,然后再次p n
会发现就变成了函数返回值了。
断点禁用功能
断点可以被使能!
使之能,使之不能。
有时我们需要留下断点的痕迹。
这个Enb就是enable的意思。下面的y是yes的意思。
要注意的是,只有打断点时使用的是行号,接下来所有对断点的操作使用的都是编号。
disable 1
就是将编号为1的断点禁用。
这样可以看到Enb下面变成n也就是no了
再使能被禁用的断点,用的就是enable
加编号
以上都只是一些基础命令,并没有涉及到真正的调试。
调试的本质
1.找到问题(最核心)
2.查看代码上下文
3.解决问题
所以gdb的很多命令是帮我们找到问题的。
假如我们现在不知道代码在哪出错了,我们在20/25/30行各打了断点,也就是我们通过断点将代码分为了三大块。
然后我们按下r开始执行并调试,它就会来到第一个断点处停下。
而如果在这个过程中没有出错,就意味着这段代码区间并没有出错。
所以我们要再运行直到下一个断点,在VS中我们是按f5,那么在gdb中我们是再使用c
命令,c是continue的缩写
它就会运行20-25行这段代码
++所以,断点的本质是将代码进行块级别的划分。++所以就可以以块为单位进行快速定位区域。
在通过s进入函数Sum后我们一直按n,一直在循环,但是我们如果确定了问题不在循环里但是又不想用finish直接走完该函数,那么怎么跳出循环呢?
我们也可以自由地跳转到指定行
until
行号
我们就可以跳到12行(前面的循环就执行完了)。
(关于运行的指令)
r是相当于f5,运行到第一个断点处停下来,再按f5是从头运行(会询问)
c是运行接下来的代码直到下一个断点,continue (注意和r的区别)
finish进入函数后直接走完这个函数退出来
until是跳到指定行(把前面的执行完)
s是逐语句调试
n是逐过程调试
长查看
我们用p
可以查看对应的变量值或者内存,但是我们再按下n也就是接着往下调试时这些值不会跟着调试走:
那么怎么样才能长显示这些值,相当于在VS中打开监视窗口呢?
display
也可以带上取地址值,查看地址
可以发现我们每display
一个值或地址,它就被打上了一个编号,然后当我们再次n的时候,它就按照编号倒序给我们显示了。
所以这个display我们就叫做查看上下文数据。
如果现在我们不想要某个值的监视,可以用undisplay
,但是直接这样是不允许的:
而是要使用编号来undisplay
如果我们已经出了某个监视值的作用域,那么我们再n
的时候,这个值就会自己消失了。
p
直接计算表达式
比如p 1+1+2
:
查看一个函数内所有定义的临时变量
info locals
技巧
上面已经讲完了关于gdb基础的命令,接下来再讲一下关于gdb的几个技巧。
1.watch
这个可以帮我们查看某个值的变化,当++变化的时候就告诉我们++
我们可以看到,会说是hardware watchpoint,也可以通过info b查看了
然后我们按n再继续调试,可以看到变化的时候确实提醒我们了,会告诉我们老的值新的值。
调试技巧:
如果有一些值我们不期望修改,就可以watch它,看是否是因为它变化了导致的问题。
2.set var确定问题原因
假如现在我们已经找到了问题出在flag为0,但是我们不想回到源代码去修改再重新编译,可以直接在调试期间修改吗?
可以用set var flag=1
就直接把它改成1了
3.条件断点
一个断点我们可以删掉或者使不能来不让其触发,但若是我们既不想删也不想使之不能呢?
我们想让这个断点在某种条件下去触发。
比如我们想知道,当i=10,result的值是多少
我们在打这个条件断点时就要把条件带上:
b 13 if i == 10
然后info b也可以看到这个断点及其条件了
我们使用r从头调试,在第一个断点处停下后,我们此时按c
就会来到我们打的条件断点处,此时我们p一下i的值看看,会发现就是10.
要删除watch或者条件断点,都是一样的用d加编号(info b看到的那个编号)进行删除
给已存在的断点新增触发条件
condition 4 i=20
我们就可以给我们的4号断点设置上条件,i=20的时候再停下来
这个条件断点不如前几个常用。
还有就是gdb不如cgdb试用。
cgdb也有自己的一些语法,就像vim一样
如果cgdb中输入Esc再配合上下键,就可以翻阅我们的代码了
再按i就可以回到调试界面
这是cgdb的分屏操作
(有可能会卡死可能需要kill)